summaryrefslogtreecommitdiff
path: root/internal/repl
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-23 22:54:29 +0200
committerPaul Buetow <paul@buetow.org>2026-03-23 22:54:29 +0200
commit55b5f40cc1f2d42bc3a277f60a3798742ce29170 (patch)
tree28f88b79792f4ba9f87efc1d341b54cf7b3963d1 /internal/repl
parent833f7c17e67e4b2b54e881bd5df05f2e4b07b2ae (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.go56
-rw-r--r--internal/repl/repl_completer_test.go20
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
}