From 5b243507cf296b6d0aa7fd4e7e48c580b23ff4ab Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Fri, 25 Jul 2025 21:27:01 -0400 Subject: [PATCH] Show grammar line numbers for regex errors --- lib/propane/pattern.rb | 2 +- lib/propane/regex.rb | 21 +++++++++--------- spec/propane/regex_spec.rb | 44 +++++++++++++++++++------------------- spec/propane_spec.rb | 12 +++++++++++ 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/lib/propane/pattern.rb b/lib/propane/pattern.rb index 71e1330..634ae27 100644 --- a/lib/propane/pattern.rb +++ b/lib/propane/pattern.rb @@ -55,7 +55,7 @@ class Propane @line_number = options[:line_number] @modes = options[:modes] @ptypename = options[:ptypename] - regex = Regex.new(@pattern) + regex = Regex.new(@pattern, @line_number) regex.nfa.end_state.accepts = self @nfa = regex.nfa end diff --git a/lib/propane/regex.rb b/lib/propane/regex.rb index 5d47d58..0564584 100644 --- a/lib/propane/regex.rb +++ b/lib/propane/regex.rb @@ -4,12 +4,13 @@ class Propane attr_reader :unit attr_reader :nfa - def initialize(pattern) + def initialize(pattern, line_number) @pattern = pattern.dup + @line_number = line_number @unit = parse_alternates @nfa = @unit.to_nfa if @pattern != "" - raise Error.new(%[Unexpected "#{@pattern}" in pattern]) + raise Error.new(%[Line #{@line_number}: unexpected "#{@pattern}" in pattern]) end end @@ -41,7 +42,7 @@ class Propane mu = MultiplicityUnit.new(last_unit, min_count, max_count) au.replace_last!(mu) else - raise Error.new("#{c} follows nothing") + raise Error.new("Line #{@line_number}: #{c} follows nothing") end when "|" au.new_alternate! @@ -59,7 +60,7 @@ class Propane def parse_group au = parse_alternates if @pattern[0] != ")" - raise Error.new("Unterminated group in pattern") + raise Error.new("Line #{@line_number}: unterminated group in pattern") end @pattern.slice!(0) au @@ -70,7 +71,7 @@ class Propane index = 0 loop do if @pattern == "" - raise Error.new("Unterminated character class") + raise Error.new("Line #{@line_number}: unterminated character class") end c = @pattern.slice!(0) if c == "]" @@ -84,13 +85,13 @@ class Propane elsif c == "-" && @pattern[0] != "]" begin_cu = ccu.last_unit unless begin_cu.is_a?(CharacterRangeUnit) && begin_cu.code_point_range.size == 1 - raise Error.new("Character range must be between single characters") + raise Error.new("Line #{@line_number}: character range must be between single characters") end if @pattern[0] == "\\" @pattern.slice!(0) end_cu = parse_backslash unless end_cu.is_a?(CharacterRangeUnit) && end_cu.code_point_range.size == 1 - raise Error.new("Character range must be between single characters") + raise Error.new("Line #{@line_number}: character range must be between single characters") end max_code_point = end_cu.code_point else @@ -116,7 +117,7 @@ class Propane elsif max_count.to_s != "" max_count = max_count.to_i if max_count < min_count - raise Error.new("Maximum repetition count cannot be less than minimum repetition count") + raise Error.new("Line #{@line_number}: maximum repetition count cannot be less than minimum repetition count") end else max_count = nil @@ -124,13 +125,13 @@ class Propane @pattern = pattern [min_count, max_count] else - raise Error.new("Unexpected match count at #{@pattern}") + raise Error.new("Line #{@line_number}: unexpected match count following {") end end def parse_backslash if @pattern == "" - raise Error.new("Error: unfollowed \\") + raise Error.new("Line #{@line_number}: error: unfollowed \\") else c = @pattern.slice!(0) case c diff --git a/spec/propane/regex_spec.rb b/spec/propane/regex_spec.rb index 397f577..d40eaba 100644 --- a/spec/propane/regex_spec.rb +++ b/spec/propane/regex_spec.rb @@ -2,14 +2,14 @@ class Propane RSpec.describe Regex do it "parses an empty expression" do - regex = Regex.new("") + regex = Regex.new("", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0].size).to eq 0 end it "parses a single character unit expression" do - regex = Regex.new("a") + regex = Regex.new("a", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -19,7 +19,7 @@ class Propane end it "parses a group with a single character unit expression" do - regex = Regex.new("(a)") + regex = Regex.new("(a)", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -33,7 +33,7 @@ class Propane end it "parses a *" do - regex = Regex.new("a*") + regex = Regex.new("a*", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -47,7 +47,7 @@ class Propane end it "parses a +" do - regex = Regex.new("a+") + regex = Regex.new("a+", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -61,7 +61,7 @@ class Propane end it "parses a ?" do - regex = Regex.new("a?") + regex = Regex.new("a?", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -75,7 +75,7 @@ class Propane end it "parses a multiplicity count" do - regex = Regex.new("a{5}") + regex = Regex.new("a{5}", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -89,7 +89,7 @@ class Propane end it "parses a minimum-only multiplicity count" do - regex = Regex.new("a{5,}") + regex = Regex.new("a{5,}", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -103,7 +103,7 @@ class Propane end it "parses a minimum and maximum multiplicity count" do - regex = Regex.new("a{5,8}") + regex = Regex.new("a{5,8}", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -118,7 +118,7 @@ class Propane end it "parses an escaped *" do - regex = Regex.new("a\\*") + regex = Regex.new("a\\*", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -131,7 +131,7 @@ class Propane end it "parses an escaped +" do - regex = Regex.new("a\\+") + regex = Regex.new("a\\+", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -144,7 +144,7 @@ class Propane end it "parses an escaped \\" do - regex = Regex.new("\\\\d") + regex = Regex.new("\\\\d", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -157,7 +157,7 @@ class Propane end it "parses a character class" do - regex = Regex.new("[a-z_]") + regex = Regex.new("[a-z_]", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -175,7 +175,7 @@ class Propane end it "parses a negated character class" do - regex = Regex.new("[^xyz]") + regex = Regex.new("[^xyz]", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -207,7 +207,7 @@ class Propane end it "parses - as a plain character at beginning of a character class" do - regex = Regex.new("[-9]") + regex = Regex.new("[-9]", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -221,7 +221,7 @@ class Propane end it "parses - as a plain character at end of a character class" do - regex = Regex.new("[0-]") + regex = Regex.new("[0-]", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -237,7 +237,7 @@ class Propane end it "parses - as a plain character at beginning of a negated character class" do - regex = Regex.new("[^-9]") + regex = Regex.new("[^-9]", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -252,7 +252,7 @@ class Propane end it "parses . as a plain character in a character class" do - regex = Regex.new("[.]") + regex = Regex.new("[.]", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -267,7 +267,7 @@ class Propane end it "parses - as a plain character when escaped in middle of character class" do - regex = Regex.new("[0\\-9]") + regex = Regex.new("[0\\-9]", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -286,7 +286,7 @@ class Propane end it "parses alternates" do - regex = Regex.new("ab|c") + regex = Regex.new("ab|c", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 2 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -296,7 +296,7 @@ class Propane end it "parses a ." do - regex = Regex.new("a.b") + regex = Regex.new("a.b", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 1 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit @@ -307,7 +307,7 @@ class Propane end it "parses something complex" do - regex = Regex.new("(a|)*|[^^]|\\|v|[x-y]+") + regex = Regex.new("(a|)*|[^^]|\\|v|[x-y]+", 1) expect(regex.unit).to be_a Regex::AlternatesUnit expect(regex.unit.alternates.size).to eq 4 expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit diff --git a/spec/propane_spec.rb b/spec/propane_spec.rb index 0ef11ab..fae6b65 100644 --- a/spec/propane_spec.rb +++ b/spec/propane_spec.rb @@ -249,6 +249,18 @@ EOF expect(results.status).to eq 0 end + it "shows error line number for unmatched left curly brace" do + write_grammar < a; +EOF + results = run_propane(capture: true) + expect(results.stderr).to match /Line 3: unexpected match count following \{/ + expect(results.status).to_not eq 0 + end + %w[d c].each do |language| context "#{language.upcase} language" do