From 44715b99f4f5f54f6b4681de896f3fe8274fcbc8 Mon Sep 17 00:00:00 2001 From: Grant Hutchins Date: Tue, 23 Jun 2026 17:20:33 -0500 Subject: [PATCH] Raise Unit::ParseError instead of built-in SyntaxError Malformed unit expressions (unbalanced parentheses, dangling operators) previously raised Ruby's built-in SyntaxError. That is the wrong class for a bad argument value: SyntaxError is a ScriptError, so it is not a StandardError and slips through a plain `rescue`, and it conflates a malformed input string with a Ruby-level parse failure. Introduce Unit::ParseError < ArgumentError and raise it from the expression parser instead. A malformed unit string is a bad argument, so ArgumentError is the correct lineage: it is a StandardError (caught by a bare rescue) and is distinct from Unit::IncompatibleUnitError (a TypeError), keeping value errors and type errors separate. --- lib/unit/class.rb | 1 + lib/unit/system.rb | 8 ++++---- spec/error_spec.rb | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/unit/class.rb b/lib/unit/class.rb index 5635d52..aae71cf 100644 --- a/lib/unit/class.rb +++ b/lib/unit/class.rb @@ -2,6 +2,7 @@ class Unit < Numeric attr_reader :value, :normalized, :unit, :system + class ParseError < ArgumentError; end class IncompatibleUnitError < TypeError; end def initialize(value, unit, system) diff --git a/lib/unit/system.rb b/lib/unit/system.rb index 3783b39..82bc124 100644 --- a/lib/unit/system.rb +++ b/lib/unit/system.rb @@ -83,7 +83,7 @@ def validate_unit(units) # Unrecognized glyphs survive lexing (see +SYMBOL+) and fail loudly as an # "Undefined unit" +TypeError+ during validation rather than being dropped. # - # Raises +SyntaxError+ on unbalanced parentheses. + # Raises +Unit::ParseError+ on unbalanced parentheses. def parse_unit(expr) stack, result, implicit_mul = [], [], false expr.to_s.scan(TOKENIZER).each do |tok| @@ -92,7 +92,7 @@ def parse_unit(expr) implicit_mul = false elsif tok == ')' compute(result, stack.pop) while !stack.empty? && stack.last != '(' - raise(SyntaxError, 'Unexpected token )') if stack.empty? + raise(Unit::ParseError, 'Unexpected token )') if stack.empty? stack.pop implicit_mul = true elsif OPERATOR.key?(tok) @@ -151,12 +151,12 @@ def symbol_to_unit(symbol) def compute(result, op) b = result.pop - a = result.pop || raise(SyntaxError, "Unexpected token #{op}") + a = result.pop || raise(Unit::ParseError, "Unexpected token #{op}") result << case op when '*' then a + b when '/' then a + Unit.power_unit(b, -1) when '^' then Unit.power_unit(a, b[0][1]) - else raise SyntaxError, "Unexpected token #{op}" + else raise Unit::ParseError, "Unexpected token #{op}" end end diff --git a/spec/error_spec.rb b/spec/error_spec.rb index d0e55a1..8e6a3b9 100644 --- a/spec/error_spec.rb +++ b/spec/error_spec.rb @@ -49,4 +49,18 @@ end end + describe "parse failures for a malformed unit expression" do + it "raises Unit::ParseError for a dangling operator" do + expect { Unit(1, "m").in!("m//s") }.to raise_error(Unit::ParseError, "Unexpected token /") + end + + it "raises Unit::ParseError for an unbalanced opening parenthesis" do + expect { Unit(1, "m").in!("(s") }.to raise_error(Unit::ParseError, "Unexpected token (") + end + + it "raises Unit::ParseError for an unbalanced closing parenthesis" do + expect { Unit(1, "m").in!(")") }.to raise_error(Unit::ParseError, "Unexpected token )") + end + end + end