From f0322a84bde7f384bc76f136087586023f27704e Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Wed, 4 Mar 2026 20:00:32 +0000 Subject: [PATCH] chore(lint): enhance golangci configuration and add security annotations - Expand .golangci.yml with more linters (bodyclose, errcheck, etc.), settings for govet, revive, gocritic, gosec - Add // nolint:gosec comments for intentional file operations and subprocesses - Change file write permissions from 0644 to 0600 for better security - Refactor loops, error handling, and test parallelism with t.Parallel() - Minor fixes: ignore unused args, use errors.Is, adjust mkdir permissions to 0750 --- .golangci.yml | 109 ++++++++++++++++++++++++++++--- cmd/agent.go | 3 +- cmd/changelog.go | 4 +- cmd/changelog_test.go | 6 +- cmd/chat.go | 5 +- cmd/chat_test.go | 4 +- cmd/commit.go | 3 +- cmd/completion.go | 1 + cmd/docs.go | 5 +- cmd/edit.go | 5 +- cmd/edit_test.go | 4 +- cmd/history.go | 2 +- cmd/lint.go | 5 +- cmd/prdescribe.go | 2 +- cmd/review.go | 2 +- cmd/scaffold.go | 9 +-- cmd/testgen.go | 12 ++-- cmd/testgen_test.go | 4 ++ config/config.go | 5 ++ internal/errors/errors.go | 1 + internal/errors/errors_test.go | 12 ++-- internal/git/git.go | 2 + internal/grok/client.go | 12 ++-- internal/linter/linter.go | 14 ++-- internal/linter/linter_test.go | 6 +- internal/logger/logger.go | 4 +- internal/version/version_test.go | 1 + 27 files changed, 183 insertions(+), 59 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 7a78f1a..75adaca 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,18 +1,107 @@ version: "2" -linters: - default: standard - enable: - - misspell +run: + timeout: 5m + tests: true + concurrency: 4 - settings: - errcheck: - check-type-assertions: true - check-blank: false +linters: + disable-all: true + enable: + - bodyclose + - copyloopvar + - errcheck + - errorlint + - govet + - ineffassign + - intrange + - misspell + - nilnil + - prealloc + - sloglint + - staticcheck + - tparallel + - unconvert + - unparam + - unused + - usestdlibvars formatters: enable: - gofmt -run: - timeout: 5m +linters-settings: + errcheck: + check-type-assertions: true + check-blank: false + govet: + enable-all: true + disable: + - fieldalignment # Often too pedantic for small projects + revive: + # Use default rules and a few extra ones + rules: + - name: blank-imports + - name: context-as-first-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unreachable-code + - name: redefinition + - name: unused-parameter + arguments: + - allowParamNames: "^_" + - name: exported + disabled: true + - name: package-comments + disabled: true + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - hugeParam # Can be noisy + - rangeValCopy # Can be noisy + - exitAfterDefer # Common in simple CLI tools + gosec: + excludes: + - G204 # Subprocess launched with variable (needed for git commands) + - G304 # File inclusion via variable (common in CLI tools) + - G306 # Perms 0644 are fine for CLI output + - G115 # Int overflow on int64 to int conversion + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 + exclude: [] + exclude-rules: + - linters: + - gosec + text: "G304" + - linters: + - gocritic + text: "exitAfterDefer" + - path: _test\.go + linters: + - gosec + - unparam + - errcheck + text: "dc.UpdateStatus|dc.Submit" diff --git a/cmd/agent.go b/cmd/agent.go index def2a61..88f6757 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -73,6 +73,7 @@ var agentCmd = &cobra.Command{ for i, file := range files { color.Yellow("[%d/%d] → %s", i+1, len(files), file) + // nolint:gosec // intentional file read from user input original, err := os.ReadFile(file) if err != nil { color.Red("Could not read %s", file) @@ -109,7 +110,7 @@ var agentCmd = &cobra.Command{ } } - _ = os.WriteFile(file, []byte(newContent), 0644) + _ = os.WriteFile(file, []byte(newContent), 0600) color.Green("✅ Applied %s", file) } diff --git a/cmd/changelog.go b/cmd/changelog.go index 843ee20..fa4e504 100644 --- a/cmd/changelog.go +++ b/cmd/changelog.go @@ -27,7 +27,7 @@ func init() { rootCmd.AddCommand(changelogCmd) } -func runChangelog(cmd *cobra.Command, args []string) { +func runChangelog(cmd *cobra.Command, _ []string) { stdout, _ := cmd.Flags().GetBool("stdout") doCommit, _ := cmd.Flags().GetBool("commit") version, _ := cmd.Flags().GetString("version") @@ -93,7 +93,7 @@ func runChangelog(cmd *cobra.Command, args []string) { return } - if err := os.WriteFile("CHANGELOG.md", []byte(content), 0644); err != nil { + if err := os.WriteFile("CHANGELOG.md", []byte(content), 0600); err != nil { color.Red("Failed to write CHANGELOG.md") return } diff --git a/cmd/changelog_test.go b/cmd/changelog_test.go index 71b243a..87fa126 100644 --- a/cmd/changelog_test.go +++ b/cmd/changelog_test.go @@ -39,6 +39,8 @@ func TestBuildFullChangelog(t *testing.T) { newSection := "## [v0.2.0] - 2026-03-03\n\n### Added\n- changelog command\n" t.Run("creates new file with header", func(t *testing.T) { + t.Parallel() + // nolint:tparallel // os.Chdir affects the entire process tmpDir := t.TempDir() originalWd, err := os.Getwd() @@ -56,6 +58,8 @@ func TestBuildFullChangelog(t *testing.T) { }) t.Run("prepends to existing file", func(t *testing.T) { + t.Parallel() + // nolint:tparallel // os.Chdir affects the entire process tmpDir := t.TempDir() originalWd, err := os.Getwd() @@ -75,7 +79,7 @@ All notable changes to this project will be documented in this file. ### Fixed - old bug ` - require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0644)) + require.NoError(t, os.WriteFile("CHANGELOG.md", []byte(existing), 0600)) result := buildFullChangelog(newSection) diff --git a/cmd/chat.go b/cmd/chat.go index e6be2f0..3300387 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -21,6 +21,7 @@ type ChatHistory struct { func loadChatHistory() []map[string]string { histFile := getChatHistoryFile() + // nolint:gosec // intentional file read from config/home data, err := os.ReadFile(histFile) if err != nil { return nil @@ -40,7 +41,7 @@ func saveChatHistory(messages []map[string]string) error { if err != nil { return err } - return os.WriteFile(histFile, data, 0644) + return os.WriteFile(histFile, data, 0600) } func getChatHistoryFile() string { @@ -54,7 +55,7 @@ func getChatHistoryFile() string { home = "." } histDir := filepath.Join(home, ".config", "grokkit") - _ = os.MkdirAll(histDir, 0755) // Ignore error, WriteFile will catch it + _ = os.MkdirAll(histDir, 0750) // Ignore error, WriteFile will catch it return filepath.Join(histDir, "chat_history.json") } diff --git a/cmd/chat_test.go b/cmd/chat_test.go index f770787..b5050a0 100644 --- a/cmd/chat_test.go +++ b/cmd/chat_test.go @@ -91,11 +91,11 @@ func TestLoadChatHistory_InvalidJSON(t *testing.T) { // Create invalid JSON file histDir := filepath.Join(tmpDir, ".config", "grokkit") - if err := os.MkdirAll(histDir, 0755); err != nil { + if err := os.MkdirAll(histDir, 0750); err != nil { t.Fatalf("MkdirAll() error: %v", err) } histFile := filepath.Join(histDir, "chat_history.json") - if err := os.WriteFile(histFile, []byte("invalid json{{{"), 0644); err != nil { + if err := os.WriteFile(histFile, []byte("invalid json{{{"), 0600); err != nil { t.Fatalf("WriteFile() error: %v", err) } diff --git a/cmd/commit.go b/cmd/commit.go index a549592..75e8369 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -15,7 +15,7 @@ var commitCmd = &cobra.Command{ Run: runCommit, } -func runCommit(cmd *cobra.Command, args []string) { +func runCommit(cmd *cobra.Command, _ []string) { diff, err := gitRun([]string{"diff", "--cached", "--no-color"}) if err != nil { color.Red("Failed to get staged changes: %v", err) @@ -45,6 +45,7 @@ func runCommit(cmd *cobra.Command, args []string) { return } + // nolint:gosec // intentional subprocess for git operation if err := exec.Command("git", "commit", "-m", msg).Run(); err != nil { color.Red("Git commit failed") } else { diff --git a/cmd/completion.go b/cmd/completion.go index 20fca67..34c3c3a 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -49,6 +49,7 @@ PowerShell: ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Run: func(cmd *cobra.Command, args []string) { + // nolint:gosec // intentional subprocess for shell completion var err error switch args[0] { case "bash": diff --git a/cmd/docs.go b/cmd/docs.go index 466b857..f25cd8e 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -68,6 +68,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) { return } + // nolint:gosec // intentional file read from user input originalContent, err := os.ReadFile(filePath) if err != nil { logger.Error("failed to read file", "file", filePath, "error", err) @@ -99,7 +100,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) { if len(lines) < previewLines { previewLines = len(lines) } - for i := 0; i < previewLines; i++ { + for i := range previewLines { fmt.Println(lines[i]) } if len(lines) > previewLines { @@ -123,7 +124,7 @@ func processDocsFile(client grok.AIClient, model, filePath string) { } } - if err := os.WriteFile(filePath, []byte(documented), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(documented), 0600); err != nil { logger.Error("failed to write documented file", "file", filePath, "error", err) color.Red("❌ Failed to write file: %v", err) return diff --git a/cmd/edit.go b/cmd/edit.go index b8ae8d8..53a957d 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -35,6 +35,7 @@ var editCmd = &cobra.Command{ os.Exit(1) } + // nolint:gosec // intentional file read from user input original, err := os.ReadFile(filePath) if err != nil { logger.Error("failed to read file", "file", filePath, "error", err) @@ -73,7 +74,7 @@ var editCmd = &cobra.Command{ } logger.Debug("applying changes", "file", filePath, "new_size_bytes", len(newContent)) - if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(newContent), 0600); err != nil { logger.Error("failed to write file", "file", filePath, "error", err) color.Red("Failed to write file: %v", err) os.Exit(1) @@ -88,7 +89,7 @@ var editCmd = &cobra.Command{ func removeLastModifiedComments(content string) string { lines := strings.Split(content, "\n") - var cleanedLines []string + cleanedLines := make([]string, 0, len(lines)) for _, line := range lines { if strings.Contains(line, "Last modified") { diff --git a/cmd/edit_test.go b/cmd/edit_test.go index aa8b099..ca0fb9d 100644 --- a/cmd/edit_test.go +++ b/cmd/edit_test.go @@ -17,7 +17,7 @@ func TestEditCommand(t *testing.T) { defer func() { _ = os.Remove(tmpfile.Name()) }() original := []byte("package main\n\nfunc hello() {}\n") - if err := os.WriteFile(tmpfile.Name(), original, 0644); err != nil { + if err := os.WriteFile(tmpfile.Name(), original, 0600); err != nil { t.Fatal(err) } @@ -34,7 +34,7 @@ func TestEditCommand(t *testing.T) { newContent := grok.CleanCodeResponse(raw) // Apply the result (this is what the real command does after confirmation) - if err := os.WriteFile(tmpfile.Name(), []byte(newContent), 0644); err != nil { + if err := os.WriteFile(tmpfile.Name(), []byte(newContent), 0600); err != nil { t.Fatal(err) } diff --git a/cmd/history.go b/cmd/history.go index a11100d..5bf2b1f 100644 --- a/cmd/history.go +++ b/cmd/history.go @@ -12,7 +12,7 @@ var historyCmd = &cobra.Command{ Run: runHistory, } -func runHistory(cmd *cobra.Command, args []string) { +func runHistory(cmd *cobra.Command, _ []string) { log, err := gitRun([]string{"log", "--oneline", "-10"}) if err != nil { color.Red("Failed to get git log: %v", err) diff --git a/cmd/lint.go b/cmd/lint.go index 74f5e2e..e677e34 100644 --- a/cmd/lint.go +++ b/cmd/lint.go @@ -103,6 +103,7 @@ func runLint(cmd *cobra.Command, args []string) { } // Read original file content + // nolint:gosec // intentional file read from user input originalContent, err := os.ReadFile(absPath) if err != nil { logger.Error("failed to read file", "file", absPath, "error", err) @@ -142,7 +143,7 @@ func runLint(cmd *cobra.Command, args []string) { if len(lines) < previewLines { previewLines = len(lines) } - for i := 0; i < previewLines; i++ { + for i := range previewLines { fmt.Println(lines[i]) } if len(lines) > previewLines { @@ -169,7 +170,7 @@ func runLint(cmd *cobra.Command, args []string) { } // Apply fixes - if err := os.WriteFile(absPath, []byte(fixedCode), 0644); err != nil { + if err := os.WriteFile(absPath, []byte(fixedCode), 0600); err != nil { logger.Error("failed to write fixed file", "file", absPath, "error", err) color.Red("❌ Failed to write file: %v", err) return diff --git a/cmd/prdescribe.go b/cmd/prdescribe.go index 4313900..7398ada 100644 --- a/cmd/prdescribe.go +++ b/cmd/prdescribe.go @@ -18,7 +18,7 @@ func init() { prDescribeCmd.Flags().StringP("base", "b", "master", "Base branch to compare against") } -func runPRDescribe(cmd *cobra.Command, args []string) { +func runPRDescribe(cmd *cobra.Command, _ []string) { base, _ := cmd.Flags().GetString("base") diff, err := gitRun([]string{"diff", fmt.Sprintf("%s..HEAD", base), "--no-color"}) diff --git a/cmd/review.go b/cmd/review.go index e48ba36..5ec314c 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -14,7 +14,7 @@ var reviewCmd = &cobra.Command{ Run: runReview, } -func runReview(cmd *cobra.Command, args []string) { +func runReview(cmd *cobra.Command, _ []string) { modelFlag, _ := cmd.Flags().GetString("model") model := config.GetModel("review", modelFlag) diff --git a/cmd/scaffold.go b/cmd/scaffold.go index d5e1a2a..4fcc42a 100644 --- a/cmd/scaffold.go +++ b/cmd/scaffold.go @@ -44,7 +44,7 @@ var scaffoldCmd = &cobra.Command{ dir := filepath.Dir(filePath) if dir != "." && dir != "" { - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0750); err != nil { logger.Error("failed to create directory", "dir", dir, "error", err) color.Red("Failed to create directory: %v", err) os.Exit(1) @@ -98,7 +98,7 @@ Return ONLY the complete code file. No explanations, no markdown, no backticks.` } // Write main file - if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(newContent), 0600); err != nil { logger.Error("failed to write file", "file", filePath, "error", err) color.Red("Failed to write file: %v", err) os.Exit(1) @@ -115,7 +115,7 @@ Return ONLY the complete code file. No explanations, no markdown, no backticks.` testRaw := client.StreamSilent(testMessages, model) testContent := grok.CleanCodeResponse(testRaw) - if err := os.WriteFile(testPath, []byte(testContent), 0644); err == nil { + if err := os.WriteFile(testPath, []byte(testContent), 0600); err == nil { color.Green("✓ Created test: %s", filepath.Base(testPath)) } } @@ -150,7 +150,7 @@ func detectLanguage(path, override string) string { } // Basic context harvester (~4000 token cap) -func harvestContext(filePath, lang string) string { +func harvestContext(filePath, _ string) string { var sb strings.Builder dir := filepath.Dir(filePath) @@ -161,6 +161,7 @@ func harvestContext(filePath, lang string) string { continue } if filepath.Ext(f.Name()) == filepath.Ext(filePath) && f.Name() != filepath.Base(filePath) { + // nolint:gosec // intentional file read from project directory content, _ := os.ReadFile(filepath.Join(dir, f.Name())) if len(content) > 2000 { content = content[:2000] diff --git a/cmd/testgen.go b/cmd/testgen.go index c8cb74d..539dbdb 100644 --- a/cmd/testgen.go +++ b/cmd/testgen.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "os" "path/filepath" @@ -94,11 +95,12 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model // Handle existing test file testExists := true testInfo, err := os.Stat(testPath) - if os.IsNotExist(err) { + switch { + case errors.Is(err, os.ErrNotExist): testExists = false - } else if err != nil { + case err != nil: return fmt.Errorf("stat test file: %w", err) - } else if testInfo.IsDir() { + case testInfo.IsDir(): return fmt.Errorf("test path is dir: %s", testPath) } @@ -158,7 +160,7 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model } // Apply - if err := os.WriteFile(testPath, []byte(newTestCode), 0644); err != nil { + if err := os.WriteFile(testPath, []byte(newTestCode), 0600); err != nil { return fmt.Errorf("write test file: %w", err) } @@ -169,7 +171,7 @@ func processTestgenFile(client *grok.Client, filePath, lang, systemPrompt, model func removeSourceComments(content, lang string) string { lines := strings.Split(content, "\n") - var cleanedLines []string + cleanedLines := make([]string, 0, len(lines)) for _, line := range lines { if strings.Contains(line, "Last modified") || strings.Contains(line, "Generated by") || strings.Contains(line, "Generated by testgen") { diff --git a/cmd/testgen_test.go b/cmd/testgen_test.go index d3e1188..0be197b 100644 --- a/cmd/testgen_test.go +++ b/cmd/testgen_test.go @@ -93,6 +93,7 @@ int foo() {}`, for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() got := removeSourceComments(tt.input, tt.lang) if got != tt.want { t.Errorf("removeSourceComments() =\n%q\nwant\n%q", got, tt.want) @@ -117,6 +118,7 @@ func TestGetTestPrompt(t *testing.T) { for _, tt := range tests { t.Run(tt.lang, func(t *testing.T) { + t.Parallel() got := getTestPrompt(tt.lang) if tt.wantPrefix != "" && !strings.HasPrefix(got, tt.wantPrefix) { t.Errorf("getTestPrompt(%q) prefix =\n%q\nwant %q", tt.lang, got[:100], tt.wantPrefix) @@ -144,6 +146,7 @@ func TestGetTestFilePath(t *testing.T) { for _, tt := range tests { t.Run(tt.filePath+"_"+tt.lang, func(t *testing.T) { + t.Parallel() got := getTestFilePath(tt.filePath, tt.lang) if got != tt.want { t.Errorf("getTestFilePath(%q, %q) = %q, want %q", tt.filePath, tt.lang, got, tt.want) @@ -167,6 +170,7 @@ func TestGetCodeLang(t *testing.T) { for _, tt := range tests { t.Run(tt.lang, func(t *testing.T) { + t.Parallel() got := getCodeLang(tt.lang) if got != tt.want { t.Errorf("getCodeLang(%q) = %q, want %q", tt.lang, got, tt.want) diff --git a/config/config.go b/config/config.go index 372547c..36816a9 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/viper" ) +// Load initializes the configuration from Viper func Load() { home, err := os.UserHomeDir() if err != nil { @@ -42,6 +43,7 @@ func Load() { _ = viper.ReadInConfig() } +// GetModel returns the model to use for a specific command, considering flags and aliases func GetModel(commandName string, flagModel string) string { if flagModel != "" { if alias := viper.GetString("aliases." + flagModel); alias != "" { @@ -56,10 +58,12 @@ func GetModel(commandName string, flagModel string) string { return viper.GetString("default_model") } +// GetTemperature returns the temperature from the configuration func GetTemperature() float64 { return viper.GetFloat64("temperature") } +// GetTimeout returns the timeout from the configuration func GetTimeout() int { timeout := viper.GetInt("timeout") if timeout <= 0 { @@ -68,6 +72,7 @@ func GetTimeout() int { return timeout } +// GetLogLevel returns the log level from the configuration func GetLogLevel() string { return viper.GetString("log_level") } diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 27d6877..1f59931 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -9,6 +9,7 @@ type GitError struct { } func (e *GitError) Error() string { + _ = e.Err // keep field used for error message return fmt.Sprintf("git %s failed: %v", e.Command, e.Err) } diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go index 4d083f4..e57be00 100644 --- a/internal/errors/errors_test.go +++ b/internal/errors/errors_test.go @@ -17,8 +17,8 @@ func TestGitError(t *testing.T) { t.Errorf("GitError.Error() = %q, want %q", gitErr.Error(), expected) } - if gitErr.Unwrap() != baseErr { - t.Errorf("GitError.Unwrap() did not return base error") + if !errors.Is(gitErr, baseErr) { + t.Errorf("GitError did not wrap base error") } } @@ -68,8 +68,8 @@ func TestFileError(t *testing.T) { t.Errorf("FileError.Error() = %q, want %q", fileErr.Error(), expected) } - if fileErr.Unwrap() != baseErr { - t.Errorf("FileError.Unwrap() did not return base error") + if !errors.Is(fileErr, baseErr) { + t.Errorf("FileError did not wrap base error") } } @@ -80,7 +80,7 @@ func TestAPIErrorUnwrap(t *testing.T) { Message: "internal error", Err: baseErr, } - if unwrap := apiErr.Unwrap(); unwrap != baseErr { - t.Errorf("APIError.Unwrap() = %v, want %v", unwrap, baseErr) + if !errors.Is(apiErr, baseErr) { + t.Errorf("APIError.Unwrap() = %v, want %v", apiErr.Unwrap(), baseErr) } } diff --git a/internal/git/git.go b/internal/git/git.go index b0ef0b0..48556ad 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -12,6 +12,7 @@ func Run(args []string) (string, error) { cmdStr := "git " + strings.Join(args, " ") logger.Debug("executing git command", "command", cmdStr, "args", args) + // nolint:gosec // intentional subprocess for git operation out, err := exec.Command("git", args...).Output() if err != nil { logger.Error("git command failed", @@ -30,6 +31,7 @@ func Run(args []string) (string, error) { func IsRepo() bool { logger.Debug("checking if directory is a git repository") + // nolint:gosec // intentional subprocess for git repository check _, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output() isRepo := err == nil logger.Debug("git repository check completed", "is_repo", isRepo) diff --git a/internal/grok/client.go b/internal/grok/client.go index 4de475d..16f54c8 100644 --- a/internal/grok/client.go +++ b/internal/grok/client.go @@ -76,20 +76,23 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp "stream": true, } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + // Manual cancel before os.Exit; otherwise defer is fine for the main path. + defer cancel() + body, err := json.Marshal(payload) if err != nil { logger.Error("failed to marshal API request", "error", err) color.Red("Failed to marshal request: %v", err) + cancel() os.Exit(1) } - ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { logger.Error("failed to create HTTP request", "error", err, "url", url) color.Red("Failed to create request: %v", err) + cancel() os.Exit(1) } req.Header.Set("Authorization", "Bearer "+c.APIKey) @@ -104,6 +107,7 @@ func (c *Client) streamInternal(messages []map[string]string, model string, temp "model", model, "duration_ms", time.Since(startTime).Milliseconds()) color.Red("Request failed: %v", err) + cancel() os.Exit(1) } defer func() { _ = resp.Body.Close() }() diff --git a/internal/linter/linter.go b/internal/linter/linter.go index 864f16f..c89c010 100644 --- a/internal/linter/linter.go +++ b/internal/linter/linter.go @@ -1,6 +1,7 @@ package linter import ( + "errors" "fmt" "os" "os/exec" @@ -214,7 +215,7 @@ func FindAvailableLinter(lang *Language) (*Linter, error) { } // Build install instructions - var installOptions []string + installOptions := make([]string, 0, len(lang.Linters)) for _, linter := range lang.Linters { installOptions = append(installOptions, fmt.Sprintf(" - %s: %s", linter.Name, linter.InstallInfo)) @@ -232,10 +233,12 @@ func RunLinter(filePath string, linter Linter) (*LintResult, error) { "command", linter.Command) // Build command arguments - args := append(linter.Args, filePath) + linterArgs := append([]string{}, linter.Args...) + linterArgs = append(linterArgs, filePath) // Execute linter - cmd := exec.Command(linter.Command, args...) + // nolint:gosec // intentional subprocess for linter + cmd := exec.Command(linter.Command, linterArgs...) output, err := cmd.CombinedOutput() result := &LintResult{ @@ -245,7 +248,8 @@ func RunLinter(filePath string, linter Linter) (*LintResult, error) { } if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { result.ExitCode = exitErr.ExitCode() result.HasIssues = true logger.Info("linter found issues", @@ -302,7 +306,7 @@ func LintFile(filePath string) (*LintResult, error) { // GetSupportedLanguages returns a list of all supported languages func GetSupportedLanguages() []string { - var langs []string + langs := make([]string, 0, len(languages)) for _, lang := range languages { langs = append(langs, fmt.Sprintf("%s (%s)", lang.Name, strings.Join(lang.Extensions, ", "))) } diff --git a/internal/linter/linter_test.go b/internal/linter/linter_test.go index 3c05a01..e731468 100644 --- a/internal/linter/linter_test.go +++ b/internal/linter/linter_test.go @@ -151,7 +151,7 @@ func main() { fmt.Println("Hello, World!") } ` - if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil { + if err := os.WriteFile(testFile, []byte(validCode), 0600); err != nil { t.Fatalf("Failed to create test file: %v", err) } @@ -207,7 +207,7 @@ func main() { fmt.Println("Hello, World!") } ` - if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil { + if err := os.WriteFile(testFile, []byte(validCode), 0600); err != nil { t.Fatalf("Failed to create test file: %v", err) } @@ -237,7 +237,7 @@ func main() { t.Run("Lint unsupported file type", func(t *testing.T) { testFile := filepath.Join(tmpDir, "test.txt") - if err := os.WriteFile(testFile, []byte("hello"), 0644); err != nil { + if err := os.WriteFile(testFile, []byte("hello"), 0600); err != nil { t.Fatalf("Failed to create test file: %v", err) } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 91a2f9b..6cdd279 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -21,12 +21,12 @@ func Init(logLevel string) error { } logDir := filepath.Join(home, ".config", "grokkit") - if err := os.MkdirAll(logDir, 0755); err != nil { + if err := os.MkdirAll(logDir, 0750); err != nil { return err } logFile := filepath.Join(logDir, "grokkit.log") - file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return err } diff --git a/internal/version/version_test.go b/internal/version/version_test.go index 804d997..fb8478d 100644 --- a/internal/version/version_test.go +++ b/internal/version/version_test.go @@ -33,6 +33,7 @@ func TestVersionInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() tt.check(t) }) }