Skip to content

Commit d28843d

Browse files
cpcloudclaude
andcommitted
fix(config): resolve golangci-lint failures
- errcheck: explicitly discard f.Close() error in EnsureConfigFile - errorlint: use errors.As instead of type assertion for HaltError - goconst: suppress "windows" constant warning (standard runtime value) - gocritic appendAssign: use copy+append instead of append to different slice - gosec G301/G302: tighten permissions to 0750/0600 for config dir/file - gosec G304: nolint for OpenFile/ReadFile on known-safe paths - gosec G306: tighten test WriteFile permissions to 0600 - noctx: use exec.CommandContext instead of exec.Command - testifylint: use assert.Positive instead of assert.Greater with 0 - wrapcheck: wrap all errors from external packages (fmt.Fprintln, io.Writer.Write, shlex.Split, exec.Cmd.Run) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fbe79c1 commit d28843d

7 files changed

Lines changed: 63 additions & 32 deletions

File tree

cmd/micasa/main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,17 @@ func (cmd *configEditCmd) Run() error {
226226
if err != nil {
227227
return err
228228
}
229-
c := exec.Command(name, args...) //nolint:gosec // user-controlled editor from $VISUAL/$EDITOR
229+
c := exec.CommandContext(
230+
context.Background(),
231+
name,
232+
args...) //nolint:gosec // user-controlled editor from $VISUAL/$EDITOR
230233
c.Stdin = os.Stdin
231234
c.Stdout = os.Stdout
232235
c.Stderr = os.Stderr
233-
return c.Run()
236+
if err := c.Run(); err != nil {
237+
return fmt.Errorf("run editor: %w", err)
238+
}
239+
return nil
234240
}
235241

236242
func (cmd *backupCmd) Run() error {

cmd/micasa/main_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,23 +206,23 @@ func TestConfigCmd(t *testing.T) {
206206
configPath := filepath.Join(tmpDir, "micasa", "config.toml")
207207
info, statErr := os.Stat(configPath)
208208
require.NoError(t, statErr, "config file should have been created")
209-
assert.Greater(t, info.Size(), int64(0), "config file should not be empty")
209+
assert.Positive(t, info.Size(), "config file should not be empty")
210210
})
211211

212212
t.Run("EditExistingConfig", func(t *testing.T) {
213213
tmpDir := t.TempDir()
214214
dir := filepath.Join(tmpDir, "micasa")
215-
require.NoError(t, os.MkdirAll(dir, 0o755))
215+
require.NoError(t, os.MkdirAll(dir, 0o750))
216216
configPath := filepath.Join(dir, "config.toml")
217217
original := "[locale]\ncurrency = \"EUR\"\n"
218-
require.NoError(t, os.WriteFile(configPath, []byte(original), 0o644))
218+
require.NoError(t, os.WriteFile(configPath, []byte(original), 0o600))
219219

220220
cmd := exec.CommandContext(t.Context(), bin, "config", "edit")
221221
cmd.Env = envWithEditor(tmpDir, noopEditor())
222222
out, err := cmd.CombinedOutput()
223223
require.NoError(t, err, "config edit failed: %s", out)
224224

225-
content, readErr := os.ReadFile(configPath)
225+
content, readErr := os.ReadFile(configPath) //nolint:gosec // test reads its own temp file
226226
require.NoError(t, readErr)
227227
assert.Equal(t, original, string(content), "existing config should be untouched")
228228
})

internal/config/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1341,7 +1341,7 @@ func writeConfigPerm(t *testing.T, content string, perm os.FileMode) string {
13411341
}
13421342

13431343
func TestPermissionWarningWithAPIKey(t *testing.T) {
1344-
if runtime.GOOS == "windows" {
1344+
if runtime.GOOS == "windows" { //nolint:goconst // standard runtime value
13451345
t.Skip("os.Chmod is a no-op on Windows")
13461346
}
13471347
path := writeConfigPerm(t, `[llm]

internal/config/edit.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ func EditorCommand(configPath string) (string, []string, error) {
4545
if len(parts) == 0 {
4646
return "", nil, fmt.Errorf("editor command resolved to empty")
4747
}
48-
args := append(parts[1:], configPath)
48+
args := make([]string, len(parts)-1, len(parts))
49+
copy(args, parts[1:])
50+
args = append(args, configPath)
4951
return parts[0], args, nil
5052
}
5153

@@ -54,17 +56,21 @@ func EditorCommand(configPath string) (string, []string, error) {
5456
// O_CREATE|O_EXCL to atomically check-and-create, avoiding TOCTOU races.
5557
func EnsureConfigFile(path string) error {
5658
dir := filepath.Dir(path)
57-
if err := os.MkdirAll(dir, 0o755); err != nil {
59+
if err := os.MkdirAll(dir, 0o750); err != nil {
5860
return fmt.Errorf("create config directory %s: %w", dir, err)
5961
}
60-
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
62+
f, err := os.OpenFile(
63+
path,
64+
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
65+
0o600,
66+
) //nolint:gosec // path from config.Path(), not user input
6167
if err != nil {
6268
if errors.Is(err, os.ErrExist) {
6369
return nil
6470
}
6571
return fmt.Errorf("create config file %s: %w", path, err)
6672
}
67-
defer f.Close()
73+
defer func() { _ = f.Close() }()
6874
if _, err := f.WriteString(ExampleTOML()); err != nil {
6975
return fmt.Errorf("write initial config: %w", err)
7076
}

internal/config/edit_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestEditorCommand_SimpleEditor(t *testing.T) {
5959
}
6060

6161
func TestEditorCommand_QuotedPath(t *testing.T) {
62-
if runtime.GOOS == "windows" {
62+
if runtime.GOOS == "windows" { //nolint:goconst // standard runtime value
6363
t.Skip("POSIX quoting not applicable on Windows")
6464
}
6565
t.Setenv("VISUAL", "")
@@ -76,24 +76,24 @@ func TestEnsureConfigFile_CreatesNewFile(t *testing.T) {
7676
err := EnsureConfigFile(path)
7777
require.NoError(t, err)
7878

79-
content, err := os.ReadFile(path)
79+
content, err := os.ReadFile(path) //nolint:gosec // test reads its own temp file
8080
require.NoError(t, err)
8181
assert.Contains(t, string(content), "[llm]")
8282
assert.Contains(t, string(content), "model =")
8383
}
8484

8585
func TestEnsureConfigFile_ExistingFileUntouched(t *testing.T) {
8686
dir := filepath.Join(t.TempDir(), "micasa")
87-
require.NoError(t, os.MkdirAll(dir, 0o755))
87+
require.NoError(t, os.MkdirAll(dir, 0o750))
8888

8989
path := filepath.Join(dir, "config.toml")
9090
original := []byte("[locale]\ncurrency = \"EUR\"\n")
91-
require.NoError(t, os.WriteFile(path, original, 0o644))
91+
require.NoError(t, os.WriteFile(path, original, 0o600))
9292

9393
err := EnsureConfigFile(path)
9494
require.NoError(t, err)
9595

96-
content, err := os.ReadFile(path)
96+
content, err := os.ReadFile(path) //nolint:gosec // test reads its own temp file
9797
require.NoError(t, err)
9898
assert.Equal(t, original, content)
9999
}

internal/config/edit_unix.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
55

66
package config
77

8-
import "github.com/google/shlex"
8+
import (
9+
"fmt"
10+
11+
"github.com/google/shlex"
12+
)
913

1014
// SplitEditorCommand splits an editor command string into argv using POSIX
1115
// shell splitting, correctly handling quoted paths and escape sequences.
1216
func SplitEditorCommand(cmd string) ([]string, error) {
13-
return shlex.Split(cmd)
17+
parts, err := shlex.Split(cmd)
18+
if err != nil {
19+
return nil, fmt.Errorf("split editor command: %w", err)
20+
}
21+
return parts, nil
1422
}
1523

1624
// editorFallbacks returns Unix-appropriate fallback editor names.

internal/config/query.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bytes"
88
"context"
99
"encoding/json"
10+
"errors"
1011
"fmt"
1112
"io"
1213
"strings"
@@ -62,7 +63,8 @@ func (c Config) Query(w io.Writer, filter string) error {
6263
break
6364
}
6465
if err, ok := v.(error); ok {
65-
if haltErr, isHalt := err.(*gojq.HaltError); isHalt {
66+
var haltErr *gojq.HaltError
67+
if errors.As(err, &haltErr) {
6668
if haltErr.ExitCode() == 0 {
6769
return nil
6870
}
@@ -151,14 +153,20 @@ func writeValue(w io.Writer, v any) error {
151153
case []any:
152154
return writeJSON(w, val)
153155
case nil:
154-
_, err := fmt.Fprintln(w, "null")
155-
return err
156+
if _, err := fmt.Fprintln(w, "null"); err != nil {
157+
return fmt.Errorf("write null: %w", err)
158+
}
159+
return nil
156160
case string:
157-
_, err := fmt.Fprintln(w, val)
158-
return err
161+
if _, err := fmt.Fprintln(w, val); err != nil {
162+
return fmt.Errorf("write string: %w", err)
163+
}
164+
return nil
159165
default:
160-
_, err := fmt.Fprintln(w, v)
161-
return err
166+
if _, err := fmt.Fprintln(w, v); err != nil {
167+
return fmt.Errorf("write value: %w", err)
168+
}
169+
return nil
162170
}
163171
}
164172

@@ -171,12 +179,13 @@ func writeTOML(w io.Writer, m map[string]any) error {
171179
return fmt.Errorf("encode TOML: %w", err)
172180
}
173181
out := bytes.TrimRight(buf.Bytes(), "\n")
174-
_, err := w.Write(out)
175-
if err != nil {
176-
return err
182+
if _, err := w.Write(out); err != nil {
183+
return fmt.Errorf("write TOML: %w", err)
184+
}
185+
if _, err := fmt.Fprintln(w); err != nil {
186+
return fmt.Errorf("write TOML newline: %w", err)
177187
}
178-
_, err = fmt.Fprintln(w)
179-
return err
188+
return nil
180189
}
181190

182191
// writeJSON encodes an array as JSON.
@@ -185,6 +194,8 @@ func writeJSON(w io.Writer, v []any) error {
185194
if err != nil {
186195
return fmt.Errorf("encode array: %w", err)
187196
}
188-
_, err = fmt.Fprintln(w, string(data))
189-
return err
197+
if _, err = fmt.Fprintln(w, string(data)); err != nil {
198+
return fmt.Errorf("write JSON: %w", err)
199+
}
200+
return nil
190201
}

0 commit comments

Comments
 (0)