diff --git a/.golangci.yml b/.golangci.yml index a4a6a77..dab57df 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,29 +1,89 @@ version: "2" +output: + sort-order: + - file linters: default: none enable: + - bidichk - bodyclose - - dogsled - - dupl + - depguard - errcheck - - exhaustive - - goconst + - forbidigo + - gocheckcompilerdirectives - gocritic - - gocyclo - - goprintffuncname - - gosec - govet - ineffassign - - misspell + - mirror + - modernize - nakedret - - noctx + - nilnil - nolintlint - - rowserrcheck + - perfsprint + - revive - staticcheck + - testifylint - unconvert - unparam - unused - - whitespace + - usestdlibvars + - usetesting + - wastedassign + settings: + depguard: + rules: + main: + deny: + - pkg: io/ioutil + desc: use os or io instead + - pkg: golang.org/x/exp + desc: it's experimental and unreliable + - pkg: github.com/pkg/errors + desc: use builtin errors package instead + nolintlint: + allow-unused: false + require-explanation: true + require-specific: true + gocritic: + enabled-checks: + - equalFold + disabled-checks: [] + revive: + severity: error + rules: + - name: blank-imports + - name: constant-logical-expr + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: empty-lines + - name: error-return + - name: error-strings + - name: exported + - name: identical-branches + - name: if-return + - name: increment-decrement + - name: modifies-value-receiver + - name: package-comments + - name: redefines-builtin-id + - name: superfluous-else + - name: time-naming + - name: unexported-return + - name: var-declaration + - name: var-naming + disabled: true + staticcheck: + checks: + - all + testifylint: {} + usetesting: + os-temp-dir: true + perfsprint: + concat-loop: false + govet: + enable: + - nilness + - unusedwrite exclusions: generated: lax presets: @@ -31,19 +91,24 @@ linters: - common-false-positives - legacy - std-error-handling - paths: - - third_party$ - - builtin$ - - examples$ + rules: + - linters: + - errcheck + - staticcheck + - unparam + path: _test\.go +issues: + max-issues-per-linter: 0 + max-same-issues: 0 formatters: enable: - gofmt - gofumpt - - goimports - golines + settings: + gofumpt: + extra-rules: true exclusions: generated: lax - paths: - - third_party$ - - builtin$ - - examples$ +run: + timeout: 10m diff --git a/cmd/commit.go b/cmd/commit.go index daec522..22935c7 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -5,6 +5,7 @@ import ( "fmt" "html" "io" + "maps" "os" "path" "strings" @@ -136,9 +137,7 @@ var commitCmd = &cobra.Command{ data := util.Data{} // Add template variables if vars := util.ConvertToMap(templateVars); len(vars) > 0 { - for k, v := range vars { - data[k] = v - } + maps.Copy(data, vars) } // Add template variables from file diff --git a/cmd/textarea.go b/cmd/textarea.go index 045c5c2..c50a5ae 100644 --- a/cmd/textarea.go +++ b/cmd/textarea.go @@ -32,7 +32,7 @@ func initialPrompt(value string) model { ti.InsertString(value) maxWidth := 0 - for _, line := range strings.Split(value, "\n") { + for line := range strings.SplitSeq(value, "\n") { if len(line) > maxWidth { maxWidth = len(line) } @@ -57,7 +57,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - switch msg.Type { //nolint:exhaustive + switch msg.Type { //nolint:exhaustive // only handling specific key types case tea.KeyEsc: if m.textarea.Focused() { m.textarea.Blur() diff --git a/cmd/version.go b/cmd/version.go index 656687b..316a357 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -89,7 +89,7 @@ func printVersion(format string, v VersionInfo) error { // Print each row with colored label and default value color for _, row := range rows { blue.Print(row[0]) - fmt.Printf(" %s\n", row[1]) + color.White(" %s\n", row[1]) } return nil } diff --git a/core/transport/transport_test.go b/core/transport/transport_test.go index 7451699..bf88088 100644 --- a/core/transport/transport_test.go +++ b/core/transport/transport_test.go @@ -20,7 +20,7 @@ func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) if m.resp != nil || m.err != nil { return m.resp, m.err } - return &http.Response{StatusCode: 200, Body: http.NoBody, Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Request: req}, nil } func TestDefaultHeaderTransport_CustomHeaders(t *testing.T) { @@ -34,7 +34,12 @@ func TestDefaultHeaderTransport_CustomHeaders(t *testing.T) { AppName: "myapp", AppVersion: "1.2.3", } - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://example.com", nil) + req, _ := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + "http://example.com", + nil, + ) resp, err := tr.RoundTrip(req) if err != nil { t.Fatalf("RoundTrip error: %v", err) @@ -65,7 +70,12 @@ func TestDefaultHeaderTransport_EmptyHeadersAndAppInfo(t *testing.T) { AppName: "", AppVersion: "", } - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://example.com", nil) + req, _ := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + "http://example.com", + nil, + ) resp, err := tr.RoundTrip(req) if err != nil { t.Fatalf("RoundTrip error: %v", err) @@ -91,7 +101,12 @@ func TestDefaultHeaderTransport_OriginErrorPropagation(t *testing.T) { AppName: "", AppVersion: "", } - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://example.com", nil) + req, _ := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + "http://example.com", + nil, + ) resp, err := tr.RoundTrip(req) if resp != nil && resp.Body != nil { resp.Body.Close() @@ -111,7 +126,12 @@ func TestDefaultHeaderTransport_MultipleHeaderValues(t *testing.T) { AppName: "app", AppVersion: "v", } - req, _ := http.NewRequestWithContext(context.Background(), "GET", "http://example.com", nil) + req, _ := http.NewRequestWithContext( + context.Background(), + http.MethodGet, + "http://example.com", + nil, + ) resp, err := tr.RoundTrip(req) if err != nil { t.Fatalf("RoundTrip error: %v", err) diff --git a/git/git.go b/git/git.go index decde6f..520d0f6 100644 --- a/git/git.go +++ b/git/git.go @@ -3,7 +3,6 @@ package git import ( "context" "errors" - "fmt" "os" "os/exec" "path" @@ -138,7 +137,7 @@ func (c *Command) commit(ctx context.Context, val string) *exec.Cmd { "commit", "--no-verify", "--signoff", - fmt.Sprintf("--message=%s", val), + "--message=" + val, } if c.isAmend { @@ -234,7 +233,11 @@ func (c *Command) InstallHook(ctx context.Context) error { } // Write the hook file with executable permissions (0o755) - return os.WriteFile(target, content, 0o755) //nolint:gosec + return os.WriteFile( + target, + content, + 0o755, + ) //nolint:gosec // hook file needs executable permissions } // UninstallHook removes the prepare-commit-msg hook if it exists. diff --git a/git/git_test.go b/git/git_test.go index 940f3d7..01d0867 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -25,24 +25,12 @@ func TestCanExecuteGitDiff(t *testing.T) { tmpDir := t.TempDir() // Change to the temporary directory - originalDir, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get current directory: %v", err) - } - defer func() { - if err := os.Chdir(originalDir); err != nil { - t.Logf("Failed to restore directory: %v", err) - } - }() - - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to change directory: %v", err) - } + t.Chdir(tmpDir) cmd := New() ctx := context.Background() - err = cmd.CanExecuteGitDiff(ctx) + err := cmd.CanExecuteGitDiff(ctx) if err == nil { t.Error("CanExecuteGitDiff() should fail in a non-git directory") } @@ -63,24 +51,12 @@ func TestCanExecuteGitDiff(t *testing.T) { defer os.RemoveAll(tmpDir) // Change to the subdirectory - originalDir, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get current directory: %v", err) - } - defer func() { - if err := os.Chdir(originalDir); err != nil { - t.Logf("Failed to restore directory: %v", err) - } - }() - - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to change directory: %v", err) - } + t.Chdir(tmpDir) cmd := New() ctx := context.Background() - err = cmd.CanExecuteGitDiff(ctx) + err := cmd.CanExecuteGitDiff(ctx) if err != nil { t.Errorf( "CanExecuteGitDiff() should succeed in a git repository subdirectory, got error: %v", diff --git a/git/hook.go b/git/hook.go index 2cd958a..2b92933 100644 --- a/git/hook.go +++ b/git/hook.go @@ -26,7 +26,7 @@ const ( // init initializes the Git hook templates by loading them from embedded files. // If there's an error loading the templates, the function logs a fatal error and terminates the program. -func init() { //nolint:gochecknoinits +func init() { //nolint:gochecknoinits // required to load embedded templates at startup if err := util.LoadTemplates(files); err != nil { log.Fatal(err) } diff --git a/prompt/prompt.go b/prompt/prompt.go index 9c5d8a8..4ad2abc 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -23,7 +23,7 @@ const ( ) // Initializes the prompt package by loading the templates from the embedded file system. -func init() { //nolint:gochecknoinits +func init() { //nolint:gochecknoinits // required to load embedded templates at startup if err := util.LoadTemplates(templatesFS); err != nil { log.Fatal(err) } diff --git a/provider/anthropic/anthropic.go b/provider/anthropic/anthropic.go index f05ebb5..4e8ceb0 100644 --- a/provider/anthropic/anthropic.go +++ b/provider/anthropic/anthropic.go @@ -42,11 +42,14 @@ func (c *Client) Completion(ctx context.Context, content string) (*core.Response if err != nil { var e *anthropic.APIError if errors.As(err, &e) { - fmt.Printf("Messages error, type: %s, message: %s", e.Type, e.Message) - } else { - fmt.Printf("Messages error: %v\n", err) + return nil, fmt.Errorf( + "messages error, type: %s, message: %s: %w", + e.Type, + e.Message, + err, + ) } - return nil, err + return nil, fmt.Errorf("messages error: %w", err) } usage := core.Usage{ @@ -97,11 +100,14 @@ func (c *Client) CompletionStream( if err != nil { var e *anthropic.APIError if errors.As(err, &e) { - fmt.Printf("Messages error, type: %s, message: %s", e.Type, e.Message) - } else { - fmt.Printf("Messages error: %v\n", err) + return nil, fmt.Errorf( + "messages error, type: %s, message: %s: %w", + e.Type, + e.Message, + err, + ) } - return nil, err + return nil, fmt.Errorf("messages error: %w", err) } if writeErr != nil { diff --git a/provider/openai/openai.go b/provider/openai/openai.go index c4b21df..3a8c426 100644 --- a/provider/openai/openai.go +++ b/provider/openai/openai.go @@ -223,7 +223,7 @@ func (c *Client) completion( return nil, err } if len(r.Choices) == 0 { - return nil, fmt.Errorf("no choices returned from API") + return nil, errors.New("no choices returned from API") } // Support reasoning models: prefer Content, fallback to ReasoningContent if empty resp.Content = r.Choices[0].Message.Content diff --git a/proxy/proxy.go b/proxy/proxy.go index 809b882..baf60ca 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -2,6 +2,7 @@ package proxy import ( "crypto/tls" + "errors" "fmt" "net/http" "net/url" @@ -58,7 +59,7 @@ type defaultHeaderTransport struct { // delegating the actual round-trip to the original transport. func (t *defaultHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { if t.origin == nil { - return nil, fmt.Errorf("origin RoundTripper is nil") + return nil, errors.New("origin RoundTripper is nil") } for key, values := range t.header { for _, value := range values { @@ -82,12 +83,14 @@ func (t *defaultHeaderTransport) RoundTrip(req *http.Request) (*http.Response, e func New(opts ...Option) (*http.Client, error) { cfg := newConfig(opts...) if cfg == nil { - return nil, fmt.Errorf("configuration is nil") + return nil, errors.New("configuration is nil") } // Create a new HTTP transport with optional TLS configuration. tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.insecure}, //nolint:gosec + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: cfg.insecure, + }, //nolint:gosec // user-configured skip verify option } // Create a new HTTP client with the specified timeout. diff --git a/util/api_key_helper.go b/util/api_key_helper.go index bfcc877..0e1ea9a 100644 --- a/util/api_key_helper.go +++ b/util/api_key_helper.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -63,7 +64,7 @@ func readCache(helperCmd string) (*apiKeyCache, error) { data, err := os.ReadFile(cachePath) if err != nil { if os.IsNotExist(err) { - return nil, nil // Cache doesn't exist yet + return nil, nil //nolint:nilnil // nil cache indicates cache miss, not an error } return nil, fmt.Errorf("failed to read cache file: %w", err) } @@ -75,7 +76,7 @@ func readCache(helperCmd string) (*apiKeyCache, error) { // Verify the helper command matches if cache.HelperCmd != helperCmd { - return nil, nil // Cache is for a different command + return nil, nil //nolint:nilnil // nil cache indicates cache miss, not an error } return &cache, nil @@ -156,7 +157,7 @@ func GetAPIKeyFromHelperWithCache( refreshInterval time.Duration, ) (string, error) { if helperCmd == "" { - return "", fmt.Errorf("api_key_helper command is empty") + return "", errors.New("api_key_helper command is empty") } // Try to read from cache diff --git a/util/api_key_helper_test.go b/util/api_key_helper_test.go index 5d37654..64cc667 100644 --- a/util/api_key_helper_test.go +++ b/util/api_key_helper_test.go @@ -233,9 +233,7 @@ func TestGetAPIKeyFromHelperWithCache_WithCaching(t *testing.T) { tmpDir := t.TempDir() // Override home directory for testing - originalHome := os.Getenv("HOME") - os.Setenv("HOME", tmpDir) - defer os.Setenv("HOME", originalHome) + t.Setenv("HOME", tmpDir) // Use a counter file to generate different values each time the command runs counterFile := filepath.Join(tmpDir, "counter.txt") @@ -269,13 +267,11 @@ func TestGetAPIKeyFromHelperWithCache_CacheExpiration(t *testing.T) { tmpDir := t.TempDir() // Override home directory for testing - originalHome := os.Getenv("HOME") - os.Setenv("HOME", tmpDir) - defer os.Setenv("HOME", originalHome) + t.Setenv("HOME", tmpDir) // Create a counter file that we'll update manually counterFile := filepath.Join(tmpDir, "counter2.txt") - command := fmt.Sprintf("cat %s", counterFile) + command := fmt.Sprintf("cat %q", counterFile) // Write initial value if err := os.WriteFile(counterFile, []byte("value1"), 0o600); err != nil { @@ -319,9 +315,7 @@ func TestGetAPIKeyFromHelperWithCache_DifferentCommands(t *testing.T) { tmpDir := t.TempDir() // Override home directory for testing - originalHome := os.Getenv("HOME") - os.Setenv("HOME", tmpDir) - defer os.Setenv("HOME", originalHome) + t.Setenv("HOME", tmpDir) cmd1 := "echo 'key-one'" cmd2 := "echo 'key-two'" @@ -355,9 +349,7 @@ func TestGetAPIKeyFromHelperWithCache_CacheFilePermissions(t *testing.T) { tmpDir := t.TempDir() // Override home directory for testing - originalHome := os.Getenv("HOME") - os.Setenv("HOME", tmpDir) - defer os.Setenv("HOME", originalHome) + t.Setenv("HOME", tmpDir) command := "echo 'test-permissions'" diff --git a/util/api_key_helper_unix.go b/util/api_key_helper_unix.go index 3cec66f..62ca648 100644 --- a/util/api_key_helper_unix.go +++ b/util/api_key_helper_unix.go @@ -5,6 +5,7 @@ package util import ( "bytes" "context" + "errors" "fmt" "os/exec" "strings" @@ -22,7 +23,7 @@ import ( // Security note: The returned API key is sensitive and should not be logged. func GetAPIKeyFromHelper(ctx context.Context, helperCmd string) (string, error) { if helperCmd == "" { - return "", fmt.Errorf("api_key_helper command is empty") + return "", errors.New("api_key_helper command is empty") } // Create context with timeout if not already set @@ -65,7 +66,7 @@ func GetAPIKeyFromHelper(ctx context.Context, helperCmd string) (string, error) } apiKey := strings.TrimSpace(stdout.String()) if apiKey == "" { - return "", fmt.Errorf("api_key_helper command returned empty output") + return "", errors.New("api_key_helper command returned empty output") } return apiKey, nil diff --git a/util/api_key_helper_windows.go b/util/api_key_helper_windows.go index e22cc93..7b8dadb 100644 --- a/util/api_key_helper_windows.go +++ b/util/api_key_helper_windows.go @@ -5,6 +5,7 @@ package util import ( "bytes" "context" + "errors" "fmt" "os/exec" "strings" @@ -70,7 +71,7 @@ func assignProcessToJob(job windows.Handle, pid int) (windows.Handle, error) { // Security note: The returned API key is sensitive and should not be logged. func GetAPIKeyFromHelper(ctx context.Context, helperCmd string) (string, error) { if helperCmd == "" { - return "", fmt.Errorf("api_key_helper command is empty") + return "", errors.New("api_key_helper command is empty") } // Create context with timeout if not already set @@ -135,7 +136,7 @@ func GetAPIKeyFromHelper(ctx context.Context, helperCmd string) (string, error) } apiKey := strings.TrimSpace(stdout.String()) if apiKey == "" { - return "", fmt.Errorf("api_key_helper command returned empty output") + return "", errors.New("api_key_helper command returned empty output") } return apiKey, nil diff --git a/util/api_key_helper_windows_test.go b/util/api_key_helper_windows_test.go index 584dfbd..b7ad2f5 100644 --- a/util/api_key_helper_windows_test.go +++ b/util/api_key_helper_windows_test.go @@ -334,7 +334,7 @@ func TestGetAPIKeyFromHelper_Windows_MultipleInvocations(t *testing.T) { results := make(chan string, 3) errors := make(chan error, 3) - for i := 0; i < 3; i++ { + for i := range 3 { go func(n int) { result, err := GetAPIKeyFromHelper( context.Background(), @@ -350,7 +350,7 @@ func TestGetAPIKeyFromHelper_Windows_MultipleInvocations(t *testing.T) { // Collect results successCount := 0 - for i := 0; i < 3; i++ { + for range 3 { select { case result := <-results: if !strings.HasPrefix(result, "test-key-") { diff --git a/util/template.go b/util/template.go index 1e4d3de..7f7d95c 100644 --- a/util/template.go +++ b/util/template.go @@ -10,7 +10,7 @@ import ( ) // Data defines a custom type for the template data. -type Data map[string]interface{} +type Data map[string]any var ( templates = make(map[string]*template.Template) @@ -19,7 +19,7 @@ var ( // NewTemplateByString parses a template from a string and executes it with the provided data. // It returns the resulting string or an error if the template parsing or execution fails. -func NewTemplateByString(format string, data map[string]interface{}) (string, error) { +func NewTemplateByString(format string, data map[string]any) (string, error) { t, err := template.New("message").Parse(format) if err != nil { return "", err @@ -36,7 +36,7 @@ func NewTemplateByString(format string, data map[string]interface{}) (string, er // processTemplate processes the template with the given name and data. // It returns the resulting bytes.Buffer or an error if the template execution fails. -func processTemplate(name string, data map[string]interface{}) (*bytes.Buffer, error) { +func processTemplate(name string, data map[string]any) (*bytes.Buffer, error) { t, ok := templates[name] if !ok { return nil, fmt.Errorf("template %s not found", name) @@ -53,14 +53,14 @@ func processTemplate(name string, data map[string]interface{}) (*bytes.Buffer, e // GetTemplateByString returns the parsed template as a string. // It returns an error if the template processing fails. -func GetTemplateByString(name string, data map[string]interface{}) (string, error) { +func GetTemplateByString(name string, data map[string]any) (string, error) { tpl, err := processTemplate(name, data) return tpl.String(), err } // GetTemplateByBytes returns the parsed template as a byte slice. // It returns an error if the template processing fails. -func GetTemplateByBytes(name string, data map[string]interface{}) ([]byte, error) { +func GetTemplateByBytes(name string, data map[string]any) ([]byte, error) { tpl, err := processTemplate(name, data) return tpl.Bytes(), err } diff --git a/util/template_test.go b/util/template_test.go index db6471a..a2bc4e5 100644 --- a/util/template_test.go +++ b/util/template_test.go @@ -4,12 +4,13 @@ import ( "bytes" "embed" "html/template" + "maps" "os" "testing" ) func TestNewTemplateByString(t *testing.T) { - data := map[string]interface{}{ + data := map[string]any{ "Name": "John Doe", } @@ -26,11 +27,9 @@ func TestNewTemplateByString(t *testing.T) { } func TestNewTemplateByStringWithCustomVars(t *testing.T) { - data := map[string]interface{}{} + data := map[string]any{} vars := ConvertToMap([]string{"Name=John Doe", "Message=Hello"}) - for k, v := range vars { - data[k] = v - } + maps.Copy(data, vars) expected := "Hello, John Doe! Hello" diff --git a/util/util_test.go b/util/util_test.go index 99bbea6..0192200 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -11,7 +11,7 @@ func TestIsCommandAvailable(t *testing.T) { name string cmd string want bool - setup func() error + setup func(t *testing.T) }{ { name: "command exists", @@ -27,9 +27,10 @@ func TestIsCommandAvailable(t *testing.T) { name: "command exists in path", cmd: "git", want: true, - setup: func() error { + setup: func(t *testing.T) { + t.Helper() // Add /usr/local/bin to PATH for this test case - return os.Setenv("PATH", "/usr/local/bin:"+os.Getenv("PATH")) + t.Setenv("PATH", "/usr/local/bin:"+os.Getenv("PATH")) }, }, } @@ -37,9 +38,7 @@ func TestIsCommandAvailable(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if tc.setup != nil { - if err := tc.setup(); err != nil { - t.Fatalf("failed to set up test case: %v", err) - } + tc.setup(t) } got := IsCommandAvailable(tc.cmd) diff --git a/version/version.go b/version/version.go index d8a8102..f5db24d 100644 --- a/version/version.go +++ b/version/version.go @@ -1,7 +1,7 @@ package version var ( - App string = "CodeGPT" + App = "CodeGPT" Version string GitCommit string BuildTime string