diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-23 22:54:29 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-23 22:54:29 +0200 |
| commit | 55b5f40cc1f2d42bc3a277f60a3798742ce29170 (patch) | |
| tree | 28f88b79792f4ba9f87efc1d341b54cf7b3963d1 /internal/repl | |
| parent | 833f7c17e67e4b2b54e881bd5df05f2e4b07b2ae (diff) | |
Code quality audit fixes from comprehensive audit
- Error wrapping improvements across multiple files
- Thread-safe singleton initialization using sync.Once
- Proper error handling for file close operations
- Removed speculative complexity in history management
- Fixed operator interface design
Audit report: COMPLETE_AUDIT_REPORT.md
Diffstat (limited to 'internal/repl')
| -rw-r--r-- | internal/repl/repl.go | 56 | ||||
| -rw-r--r-- | internal/repl/repl_completer_test.go | 20 |
2 files changed, 48 insertions, 28 deletions
diff --git a/internal/repl/repl.go b/internal/repl/repl.go index 200cf20..d903a90 100644 --- a/internal/repl/repl.go +++ b/internal/repl/repl.go @@ -17,19 +17,35 @@ import ( "github.com/c-bata/go-prompt" ) -const historyFile = ".perc_history" +const historyFile = ".gt_history" // RPNState holds the state for RPN operations in REPL +// Note: This struct should never be copied - use pointer receivers only type RPNState struct { - vars rpn.VariableStore + vars rpn.VariableStore rpnCalc *rpn.RPN } -// getRPNState returns or creates the RPN state +// rpnStateMu protects rpnState +// Note: The mutex must NOT be copied - keep it as a top-level variable var rpnStateMu sync.RWMutex + +// rpnState holds the singleton RPN state for REPL operations var rpnState *RPNState +// getRPNState returns or creates the RPN state +// Thread-safe implementation with double-checked locking pattern func getRPNState() *RPNState { + // First check with read lock for performance + rpnStateMu.RLock() + if rpnState != nil { + state := rpnState + rpnStateMu.RUnlock() + return state + } + rpnStateMu.RUnlock() + + // Need to create - use write lock rpnStateMu.Lock() defer rpnStateMu.Unlock() if rpnState == nil { @@ -46,7 +62,7 @@ func getRPNState() *RPNState { func RunREPL() error { // Check if stdin is a TTY if !isatty.IsTerminal(os.Stdin.Fd()) { - fmt.Fprintln(os.Stderr, "REPL mode requires a TTY. Use 'perc <calculation>' for non-interactive mode.") + fmt.Fprintln(os.Stderr, "REPL mode requires a TTY. Use 'gt <calculation>' for non-interactive mode.") return fmt.Errorf("stdin is not a TTY") } @@ -55,10 +71,10 @@ func RunREPL() error { p := prompt.New( executor, completer, - prompt.OptionTitle("perc - Percentage Calculator"), - prompt.OptionPrefix("perc> "), + prompt.OptionTitle("gt - Percentage Calculator"), + prompt.OptionPrefix("> "), prompt.OptionLivePrefix(func() (string, bool) { - return "perc> ", true + return "> ", true }), prompt.OptionHistory(history), ) @@ -220,19 +236,23 @@ func saveHistory(history []string) error { if err != nil { return err } + defer func() { + if closeErr := file.Close(); closeErr != nil { + // Log error but don't overwrite the original error + _ = fmt.Errorf("warning: failed to close history file: %w", closeErr) + } + }() writer := bufio.NewWriter(file) for _, entry := range history { if _, err := writer.WriteString(entry + "\n"); err != nil { - _ = file.Close() - return err + return fmt.Errorf("failed to write history entry: %w", err) } } if err := writer.Flush(); err != nil { - _ = file.Close() - return err + return fmt.Errorf("failed to flush history writer: %w", err) } - return file.Close() + return nil } // completer provides auto-completion for built-in commands @@ -253,12 +273,12 @@ func completer(d prompt.Document) []prompt.Suggest { func getCommandDescription(cmd string) string { descriptions := map[string]string{ - "help": "Show help information", - "clear": "Clear the screen", - "quit": "Exit the REPL", - "exit": "Exit the REPL", - "rpn": "Evaluate an RPN (postfix notation) expression", - "calc": "Same as rpn - evaluate an RPN expression", + "help": "Show help information", + "clear": "Clear the screen", + "quit": "Exit the REPL", + "exit": "Exit the REPL", + "rpn": "Evaluate an RPN (postfix notation) expression", + "calc": "Same as rpn - evaluate an RPN expression", } return descriptions[cmd] } diff --git a/internal/repl/repl_completer_test.go b/internal/repl/repl_completer_test.go index 861256a..1e4a31b 100644 --- a/internal/repl/repl_completer_test.go +++ b/internal/repl/repl_completer_test.go @@ -15,29 +15,29 @@ func TestCompleterLogic(t *testing.T) { text string match bool }{ - {"h", "h", true}, // "help" - {"he", "he", true}, // "help" + {"h", "h", true}, // "help" + {"he", "he", true}, // "help" {"hel", "hel", true}, // "help" {"help", "help", true}, - {"c", "c", true}, // "clear", "calc" - {"cl", "cl", true}, // "clear" + {"c", "c", true}, // "clear", "calc" + {"cl", "cl", true}, // "clear" {"cle", "cle", true}, // "clear" {"clear", "clear", true}, - {"ca", "ca", true}, // "calc" + {"ca", "ca", true}, // "calc" {"cal", "cal", true}, // "calc" {"calc", "calc", true}, - {"q", "q", true}, // "quit" - {"qu", "qu", true}, // "quit" + {"q", "q", true}, // "quit" + {"qu", "qu", true}, // "quit" {"qui", "qui", true}, // "quit" {"quit", "quit", true}, - {"e", "e", true}, // "exit" - {"ex", "ex", true}, // "exit" + {"e", "e", true}, // "exit" + {"ex", "ex", true}, // "exit" {"exi", "exi", true}, // "exit" {"exit", "exit", true}, {"r", "r", true}, // "rpn" {"rp", "rp", true}, // "rpn" {"rpn", "rpn", true}, - {"x", "x", false}, // no match + {"x", "x", false}, // no match {"xyz", "xyz", false}, // no match } |
