diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ccef2..0843b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Unreleased + +### New Features + +- Warn about unused rules (rules not reachable from any start rule). The + warnings are written to the log file and, with `-w`, are printed to stderr + and treated as fatal errors. + ## v5.2.0 ### New Features diff --git a/README.md b/README.md index f64f29b..de6c21d 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ Propane is typically invoked from the command-line as `./propane`. debugging a grammar. --version Show program version and exit. -w Treat warnings as errors. This option will treat shift/reduce - conflicts as fatal errors and will print them to stderr in - addition to the log file. + conflicts and unused rules as fatal errors and will print + them to stderr in addition to the log file. The user must specify the path to a Propane input grammar file and a path to an output file. diff --git a/doc/user_guide.md b/doc/user_guide.md index 8c49637..79d35fd 100644 --- a/doc/user_guide.md +++ b/doc/user_guide.md @@ -43,8 +43,8 @@ Propane is typically invoked from the command-line as `./propane`. debugging a grammar. --version Show program version and exit. -w Treat warnings as errors. This option will treat shift/reduce - conflicts as fatal errors and will print them to stderr in - addition to the log file. + conflicts and unused rules as fatal errors and will print + them to stderr in addition to the log file. The user must specify the path to a Propane input grammar file and a path to an output file. diff --git a/lib/propane/cli.rb b/lib/propane/cli.rb index 5a5b5af..bc0a703 100644 --- a/lib/propane/cli.rb +++ b/lib/propane/cli.rb @@ -10,8 +10,8 @@ Options: debugging a grammar. --version Show program version and exit. -w Treat warnings as errors. This option will treat shift/reduce - conflicts as fatal errors and will print them to stderr in - addition to the log file. + conflicts and unused rules as fatal errors and will print + them to stderr in addition to the log file. EOF class << self diff --git a/lib/propane/parser.rb b/lib/propane/parser.rb index 56432b9..19d902c 100644 --- a/lib/propane/parser.rb +++ b/lib/propane/parser.rb @@ -50,6 +50,7 @@ class Propane build_reduce_actions! build_tables! + check_unused_rules! write_log! errormessage = "" if @errors.size > 0 @@ -68,6 +69,34 @@ class Propane private + # Record a warning for each user-defined rule that cannot be reached from + # any start rule. + # + # @return [void] + def check_unused_rules! + reachable = Set.new + pending = @grammar.rules[0...@grammar.start_rules.length].map(&:rule_set) + while rule_set = pending.pop + next if reachable.include?(rule_set) + reachable << rule_set + rule_set.rules.each do |rule| + rule.components.each do |component| + if component.is_a?(RuleSet) && !reachable.include?(component) + pending << component + end + end + end + end + @rule_sets.each do |name, rule_set| + # Skip rule sets generated internally (real start rules and optional + # component rules). + next if name.start_with?("$") || rule_set.optional? + unless reachable.include?(rule_set) + @warnings << "Unused rule #{name} (defined on line #{rule_set.rules.first.line_number})" + end + end + end + def build_tables! @state_table = [] @shift_table = [] diff --git a/spec/propane_spec.rb b/spec/propane_spec.rb index 471a22e..5d5dea9 100644 --- a/spec/propane_spec.rb +++ b/spec/propane_spec.rb @@ -267,6 +267,50 @@ EOF expect(File.binread("spec/run/testparser.log")).to match %r{Shift/Reduce conflict \(state \d+\) between token b and rule As2\? \(defined on line 4\)} end + it "warns on unused rules" do + write_grammar < a; +Unused -> b Other?; +Other -> a; +EOF + results = run_propane(capture: true) + expect(results.stderr).to eq "" + expect(results.status).to eq 0 + log = File.binread("spec/run/testparser.log") + expect(log).to match %r{Unused rule Unused \(defined on line 4\)} + expect(log).to match %r{Unused rule Other \(defined on line 5\)} + expect(log).to_not match %r{Unused rule (Start|Other\?)} + end + + it "errors on unused rules with -w" do + write_grammar < a Used; +Used -> b; +Unused -> b; +EOF + results = run_propane(extra_args: %w[-w], capture: true) + expect(results.stderr).to match %r{Unused rule Unused \(defined on line 5\)} + expect(results.stderr).to_not match %r{Unused rule (Start|Used)\b} + expect(results.status).to_not eq 0 + end + + it "does not warn about rules reachable from any of multiple start rules" do + write_grammar < a; +Other -> b; +EOF + results = run_propane(extra_args: %w[-w], capture: true) + expect(results.stderr).to eq "" + expect(results.status).to eq 0 + end + it "errors on duplicate field aliases in a rule" do write_grammar <