Skip to content

Commit

Permalink
Correctly identify all known void value expressions
Browse files Browse the repository at this point in the history
Use the Parser library and bail on shitty regexes
  • Loading branch information
JoshCheek committed Jun 16, 2013
1 parent 1db75a6 commit 259a6f9
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 36 deletions.
2 changes: 2 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,12 @@ Known Issues

* `BEGIN/END` breaks things and I probably won't take the time to fix it, becuase it's nontrivial and its really meant for command-line scripts, but there is currently a cuke for it
* Heredocs aren't recorded. It might actually be possible if the ExpressionList were to get smarter
* Conflicts with libs that have `at_exit` blocks (e.g. minitest/autorun)

Todo
====

* Move as much of the SyntaxAnalyzer as possible over to Parser and ditch Ripper altogether
* Refactor ExpressionList/SeeingIsBelieving to store lines in an array instead of as a string, so everyone doesn't magically need to know when to chomp
* Make friends who actually know how to parse Ruby syntax (omg, teach me Ripper, pls, it will make this lib so much better, you have no idea O.o)

Expand Down
25 changes: 16 additions & 9 deletions lib/seeing_is_believing/syntax_analyzer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'ripper'
require 'parser/current'

class SeeingIsBelieving
class SyntaxAnalyzer < Ripper::SexpBuilder
Expand Down Expand Up @@ -156,15 +157,21 @@ def on_comment(*)

# RETURNS

# this is conspicuosuly inferior, but I can't figure out how to actually parse it
# see: http://www.ruby-forum.com/topic/4409633
def self.void_value_expression?(code)
return true if /(^|\s)(?:return|next|redo|retry|break)([^\w\n]|\n?\z).*?\n?\z/ =~ code

lines = code.split("\n")
if /^if/ =~ lines[0].strip
return /return/ =~ lines[0] if lines.length == 1 # err on the side of conservativity
void_value_expression?(lines[-2])
def self.void_value_expression?(code_or_ast)
ast = code_or_ast
ast = Parser::CurrentRuby.parse(code_or_ast) if code_or_ast.kind_of? String

case ast && ast.type
when :begin, :resbody
void_value_expression?(ast.children[-1])
when :rescue, :ensure
ast.children.any? { |child| void_value_expression? child }
when :if
void_value_expression?(ast.children[1]) || void_value_expression?(ast.children[2])
when :return, :next, :redo, :retry, :break
true
else
false
end
end

Expand Down
1 change: 1 addition & 0 deletions seeing_is_believing.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Gem::Specification.new do |s|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]

s.add_dependency "parser", "~> 1.4"
s.add_development_dependency "rake", "~> 10.0.3"
s.add_development_dependency "rspec", "~> 2.12.0"
s.add_development_dependency "cucumber", "~> 1.2.1"
Expand Down
181 changes: 154 additions & 27 deletions spec/syntax_analyzer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,48 +171,175 @@
end
end

shared_examples_for 'single line void_value_expression?' do |keyword|
it "`#{keyword}` returns true when the expression ends in #{keyword}", t:true do
shared_examples_for 'single line void_value_expression?' do |keyword, options={}|
specify "`#{keyword}` returns true when the expression ends in #{keyword} without an argument" do
described_class.void_value_expression?("#{keyword}").should be_true
described_class.void_value_expression?("#{keyword}(1)").should be_true
described_class.void_value_expression?("#{keyword} 1").should be_true
described_class.void_value_expression?("#{keyword} 1\n").should be_true
described_class.void_value_expression?("#{keyword} 1 if true").should be_true
described_class.void_value_expression?("#{keyword} 1 if false").should be_true
described_class.void_value_expression?("#{keyword} if true").should be_true
described_class.void_value_expression?("o.#{keyword}").should be_false
described_class.void_value_expression?(":#{keyword}").should be_false
described_class.void_value_expression?(":'#{keyword}'").should be_false
described_class.void_value_expression?("'#{keyword}'").should be_false
described_class.void_value_expression?("def a\n#{keyword} 1\nend").should be_false
described_class.void_value_expression?("-> {\n#{keyword} 1\n}").should be_false
described_class.void_value_expression?("Proc.new {\n#{keyword} 1\n}").should be_false
described_class.void_value_expression?("#{keyword}_something").should be_false
described_class.void_value_expression?("def a\n#{keyword}\nend").should be_false
described_class.void_value_expression?("-> {\n#{keyword}\n}").should be_false
described_class.void_value_expression?("Proc.new {\n#{keyword}\n}").should be_false
described_class.void_value_expression?("#{keyword}_something").should be_false
described_class.void_value_expression?("'#{keyword}\n#{keyword}\n#{keyword}'").should be_false

unless options[:no_args]
described_class.void_value_expression?("#{keyword}(1)").should be_true
described_class.void_value_expression?("#{keyword} 1").should be_true
described_class.void_value_expression?("#{keyword} 1\n").should be_true
described_class.void_value_expression?("#{keyword} 1 if true").should be_true
described_class.void_value_expression?("#{keyword} 1 if false").should be_true
described_class.void_value_expression?("def a\n#{keyword} 1\nend").should be_false
described_class.void_value_expression?("-> {\n#{keyword} 1\n}").should be_false
described_class.void_value_expression?("Proc.new {\n#{keyword} 1\n}").should be_false
described_class.void_value_expression?("#{keyword} \\\n1").should be_true
end
end

it "knows when an if statement ends in `#{keyword}`" do
# if
described_class.void_value_expression?("if true\n#{keyword}\nend").should be_true
described_class.void_value_expression?("if true\n #{keyword}\nend").should be_true
described_class.void_value_expression?("if true\n 1+1\n #{keyword}\nend").should be_true
described_class.void_value_expression?("if true\n #{keyword}\n 1+1\n end").should be_false
described_class.void_value_expression?("123 && if true\n #{keyword}\nend").should be_false
described_class.void_value_expression?("def m\n if true\n #{keyword}\nend\n end").should be_false
described_class.void_value_expression?("if true; #{keyword}; end").should be_true
described_class.void_value_expression?("if true; 1; end").should be_false

# if .. elsif
described_class.void_value_expression?("if true\n #{keyword}\n elsif true\n 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n elsif true\n #{keyword}\n end").should be_true
described_class.void_value_expression?("if true\n #{keyword}\n 2\n elsif true\n 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n elsif true\n #{keyword}\n 2\n end").should be_false

# if .. else
described_class.void_value_expression?("if true\n #{keyword}\n else 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n else\n #{keyword}\n end").should be_true
described_class.void_value_expression?("if true\n #{keyword}\n 2\n else 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n else\n #{keyword}\n 2\n end").should be_false

# if .. elsif .. else .. end
described_class.void_value_expression?("if true\n #{keyword}\nelsif true\n 1 else 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n elsif true\n #{keyword}\n else\n 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n elsif true\n 1\n elsif true\n #{keyword}\n else\n 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n elsif true\n 1\n else\n #{keyword}\n end").should be_true
described_class.void_value_expression?("if true\n #{keyword}\n 2\nelsif true\n 1 else 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n elsif true\n #{keyword}\n 2\n else\n 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n elsif true\n 1\n elsif true\n #{keyword}\n 2\n else\n 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n elsif true\n 1\n else\n #{keyword}\n 2\n end").should be_false

unless options[:no_args]
# if
described_class.void_value_expression?("if true\n#{keyword} 1\nend").should be_true
described_class.void_value_expression?("if true\n #{keyword} 1\nend").should be_true
described_class.void_value_expression?("if true\n 1+1\n #{keyword} 1\nend").should be_true
described_class.void_value_expression?("if true\n #{keyword} 1\n 1+1\n end").should be_false
described_class.void_value_expression?("123 && if true\n #{keyword} 1\nend").should be_false
described_class.void_value_expression?("def m\n if true\n #{keyword} 1\nend\n end").should be_false
described_class.void_value_expression?("if true; #{keyword} 1; end").should be_true
described_class.void_value_expression?("if true; 1; end").should be_false

# if .. elsif
described_class.void_value_expression?("if true\n #{keyword} 1\n elsif true\n 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n elsif true\n #{keyword}\n end").should be_true
described_class.void_value_expression?("if true\n #{keyword} 1\n 2\n elsif true\n 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n elsif true\n #{keyword}\n 2\n end").should be_false

# if .. else
described_class.void_value_expression?("if true\n #{keyword} 1\n else 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n else\n #{keyword}\n end").should be_true
described_class.void_value_expression?("if true\n #{keyword} 1\n 2\n else 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n else\n #{keyword}\n 2\n end").should be_false

# if .. elsif .. else .. end
described_class.void_value_expression?("if true\n #{keyword} 1\nelsif true\n 1 else 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n elsif true\n #{keyword}\n else\n 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n elsif true\n 1\n elsif true\n #{keyword}\n else\n 1\n end").should be_true
described_class.void_value_expression?("if true\n 1\n elsif true\n 1\n else\n #{keyword}\n end").should be_true
described_class.void_value_expression?("if true\n #{keyword} 1\n 2\nelsif true\n 1 else 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n elsif true\n #{keyword}\n 2\n else\n 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n elsif true\n 1\n elsif true\n #{keyword}\n 2\n else\n 1\n end").should be_false
described_class.void_value_expression?("if true\n 1\n elsif true\n 1\n else\n #{keyword}\n 2\n end").should be_false
end
end

it "doesn't work because the return and next keyword evaluators are insufficient regexps" do
pending "doesn't pass yet (and prob never will >.<)" do
described_class.send(evalutor, "'#{keyword}\n#{keyword}\n#{keyword}'").should be_false
described_class.send(evalutor, "#{keyword} \\\n1").should be_true
it "knows when a begin statement ends in `#{keyword}`" do
described_class.void_value_expression?("begin\n #{keyword}\n end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword}\n end").should be_true
described_class.void_value_expression?("begin\n #{keyword}\n 1\n end").should be_false
described_class.void_value_expression?("begin\n 1\n #{keyword}\n 1\n end").should be_false

unless options[:no_args]
described_class.void_value_expression?("begin\n #{keyword} '123' \n end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword} 456\n end").should be_true
described_class.void_value_expression?("begin\n #{keyword} :'789'\n 1\n end").should be_false
described_class.void_value_expression?("begin\n 1\n #{keyword} /101112/\n 1\n end").should be_false
end

# I don't know that the rest of these hold across all versions of Ruby since they make no fucking sense
# so even though some of them can technically be non-vve,
# I'm still going to call any one of them a vve
#
# e.g. (tested on 2.0)
# this is allowed
# -> { a = begin; return
# rescue; return
# ensure; return
# end }
# this is not
# -> { a = begin; return
# end }

# with rescue...
described_class.void_value_expression?("begin\n #{keyword}\n rescue\n #{keyword} end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword}\n rescue RuntimeError => e\n end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword}\n rescue RuntimeError\n end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword}\n rescue\n end").should be_true
described_class.void_value_expression?("begin\n 1\n rescue\n end").should be_false
described_class.void_value_expression?("begin\n 1\n rescue\n #{keyword}\n end").should be_true
described_class.void_value_expression?("begin\n 1\n rescue\n #{keyword}\n 1\n end").should be_false

unless options[:no_args]
described_class.void_value_expression?("begin\n #{keyword}\n rescue\n #{keyword} 1 end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword} 1\n rescue RuntimeError => e\n end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword} 1\n rescue RuntimeError\n end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword} :abc\n rescue\n end").should be_true
described_class.void_value_expression?("begin\n 1\n rescue\n #{keyword} 'abc'\n end").should be_true
described_class.void_value_expression?("begin\n 1\n rescue\n #{keyword} :abc\n 1\n end").should be_false
end

# with ensure
described_class.void_value_expression?("begin\n #{keyword}\n ensure\n #{keyword} end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword}\n ensure\n end").should be_true
described_class.void_value_expression?("begin\n 1\n ensure\n end").should be_false
described_class.void_value_expression?("begin\n 1\n ensure\n #{keyword}\n end").should be_true
described_class.void_value_expression?("begin\n 1\n ensure\n #{keyword}\n 1\n end").should be_false

unless options[:no_args]
described_class.void_value_expression?("begin\n #{keyword}\n ensure\n #{keyword} 1 end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword} 1\n ensure\n end").should be_true
described_class.void_value_expression?("begin\n 1\n #{keyword} :abc\n ensure\n end").should be_true
described_class.void_value_expression?("begin\n 1\n ensure\n #{keyword} 'abc'\n end").should be_true
described_class.void_value_expression?("begin\n 1\n ensure\n #{keyword} :abc\n 1\n end").should be_false
end

# with ensure and rescue
described_class.void_value_expression?("begin\n 1\n rescue\n 2\n ensure\n 3\n end").should be_false
described_class.void_value_expression?("begin\n #{keyword}\n rescue\n 2\n ensure\n 3\n end").should be_true
described_class.void_value_expression?("begin\n 1\n rescue\n #{keyword}\n ensure\n 3\n end").should be_true
described_class.void_value_expression?("begin\n 1\n rescue\n 2\n ensure\n #{keyword}\n end").should be_true
end
end

it_should_behave_like 'single line void_value_expression?', 'return'
it_should_behave_like 'single line void_value_expression?', 'next'
it_should_behave_like 'single line void_value_expression?', 'redo'
it_should_behave_like 'single line void_value_expression?', 'retry'
it_should_behave_like 'single line void_value_expression?', 'break'

it 'knows when an if statement evaluates to a void value expression' do
described_class.void_value_expression?("if true\nreturn 1\nend").should be_true
described_class.void_value_expression?("if true\n return 1\nend").should be_true
described_class.void_value_expression?("if true\n 1+1\n return 1\nend").should be_true
described_class.void_value_expression?("if true\n return 1\n 1+1\n end").should be_false
described_class.void_value_expression?("123 && if true\n return 1\nend").should be_false
described_class.void_value_expression?("def m\n if true\n return 1\nend\n end").should be_false
described_class.void_value_expression?("if true; return 1; end").should be_true
described_class.void_value_expression?("if true; 1; end").should be_false
end
it_should_behave_like 'single line void_value_expression?', 'redo', no_args: true
it_should_behave_like 'single line void_value_expression?', 'retry', no_args: true

it 'knows when a line opens the data segment' do
described_class.begins_data_segment?('__END__').should be_true
Expand Down

1 comment on commit 259a6f9

@JoshCheek
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're using Parser, we probably don't really need this many tests, we can just trust it to parse correctly.

Please sign in to comment.