Resolving "code is unreachable" in sorbet - ruby

I maintain the pdf-reader ruby gem, and I'm experimenting with static type checking via sorbet.
I have a particular source file that's currently typed: false, and I'd like to change it to typed: true. When I do so, sorbet complains about this state machine shaped method:
def process_data(data)
parser = build_parser(data)
mode = :none
instructions = []
while token = parser.parse_token(CMAP_KEYWORDS)
if token == "beginbfchar"
mode = :char
elsif token == "endbfchar"
process_bfchar_instructions(instructions)
instructions = []
mode = :none
elsif token == "beginbfrange"
mode = :range
elsif token == "endbfrange"
process_bfrange_instructions(instructions)
instructions = []
mode = :none
elsif mode == :char || mode == :range
instructions << token
end
end
end
I get this error:
$ srb tc
./lib/pdf/reader/cmap.rb:74: This code is unreachable https://srb.help/7006
74 | elsif mode == :char || mode == :range
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
./lib/pdf/reader/cmap.rb:74: This condition was always falsy (T::Boolean)
74 | elsif mode == :char || mode == :range
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Got T::Boolean originating from:
./lib/pdf/reader/cmap.rb:74:
74 | elsif mode == :char || mode == :range
^^^^^^^^^^^^^
./lib/pdf/reader/cmap.rb:75: This code is unreachable https://srb.help/7006
75 | instructions << token
^^^^^^^^^^^^^^^^^^^^^
./lib/pdf/reader/cmap.rb:74: This condition was always falsy (T::Boolean)
74 | elsif mode == :char || mode == :range
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Got T::Boolean originating from:
./lib/pdf/reader/cmap.rb:74:
74 | elsif mode == :char || mode == :range
I'm fairly confident that the code that sorbet believes in unreachable to definitely executing in production. This class wouldn't work if that was the case.
Presumably there's something about this code structure or the type annotations that makes sorbet believe that mode can never equal :char or :range. So far I haven't been able to work it out though. What am I missing?

Looks like it is related to this bug. I would mark mode as unsafe for now.
mode = T.unsafe(:none)
You can play with the code on sorbet.run

Related

Cyclomatic complexity for some_method is too high

I have following method in controller. When i run lint i get the error
Cyclomatic complexity for some_method is too high
I checked online about it and seems like it is the way i wrote the method. How could i rewrite this method so i dont get the lint error?
def customer_order
if params[:customer_id].present? && !params[:order_id].present?
render_error :not_found, 'No info found for given customer id' \
unless #info.customer_id == params[:customer_id]
elsif params[:order_id].present? && !params[:customer_id].present?
render_error :not_found, 'No info found for given order id' \
unless #info.order_id == params[:order_id]
elsif params[:customer_id].present? && params[:order_id].present?
render_error :not_found, 'No info found for given customer id and order id’ \
unless #info.customer_id == params[:customer_id] &&
#info.order_id == params[:order_id]
end
end
Yikes! It is too high!
This linting message essentially means you have so many if statements that it's hard for a developer to keep straight.
I would suggest refactoring the contents of each if statement into it's own method.
def customer_order
if params[:customer_id].present? && !params[:order_id].present?
no_order_error
elsif params[:order_id].present? && !params[:customer_id].present?
no_info_error
...
end
def no_order_error
return if #info.customer_id == params[:customer_id]
render_error :not_found, 'No info found for given customer id'
end
def no_info_error
...
end
One approach would be to make each of the conditionals their own method. As an example:
def customer_without_order(params)
params[:customer_id].present? && !params[:order_id].present?
end
You may find that the conditionals that determine when you render an error warrant this treatment as well.
You could reduce some of the redundancy like so.
def customer_order
msg = case [params[:customer_id].present?, params[:order_id].present?]
when [true, false]
#info.customer_id == params[:customer_id] ?
nil : 'No info found for given customer id'
when [false, true]
#info.order_id == params[:order_id] ?
nil : 'No info found for given order id'
when [true, true]
#info.customer_id == params[:customer_id] && #info.order_id == params[:order_id] ?
nil : 'No info found for given customer id and order id’
else
nil
end
render_error(:not_found, msg) if msg
end

Debugging a stack level too deep error - Ruby

I am building a Tic Tac Toe game in which the user can play a computer, or computers can play each other. While building the AI I am running into the below error. How can I debug this? I understand it is to do with a loop somewhere but I can't find it.
ttt-with-ai-project-cb-000/lib/players/computer.rb:13:in `each': stack level too deep (SystemStackError)
from ttt-with-ai-project-cb-000/lib/players/computer.rb:13:in `detect'
from ttt-with-ai-project-cb-000/lib/players/computer.rb:13:in `check_move'
from ttt-with-ai-project-cb-000/lib/players/computer.rb:8:in `move'
from ttt-with-ai-project-cb-000/lib/game.rb:61:in `wargame_turn'
from ttt-with-ai-project-cb-000/lib/game.rb:64:in `wargame_turn'
from ttt-with-ai-project-cb-000/lib/game.rb:64:in `wargame_turn'
from ttt-with-ai-project-cb-000/lib/game.rb:64:in `wargame_turn'
from ttt-with-ai-project-cb-000/lib/game.rb:64:in `wargame_turn'
... 11900 levels...
from bin/tictactoe:37:in `block in run_game'
from bin/tictactoe:35:in `times'
from bin/tictactoe:35:in `run_game'
from bin/tictactoe:79:in `<main>'
The Offending Methods
lib/players/computer.rb
def move(board)
check_move(board)
end
def check_move(board)
win_combo = Game::WIN_COMBINATIONS.detect do |indices|
board.cells[indices[0]] == token && board.cells[indices[1]] == token || board.cells[indices[1]] == token && board.cells[indices[2]] == token || board.cells[indices[0]] == token && board.cells[indices[2]] == token
end
win_combo.detect {|index| board.cells[index] == " "}.join if win_combo
end
lib/game.rb
def wargame_turn
input = current_player.move(board)
if !board.valid_move?(input)
wargame_turn
else
board.update(input, current_player)
end
end
bin/tictactoe calls the method in the following block within #run_game:
100.times do
game = Game.new(Players::Computer.new("X"), player_2 = Players::Computer.new("O"))
game.wargames
if game.winner == "X"
x_wins += 1
elsif game.winner == "O"
x_wins += 1
elsif game.draw?
draws += 1
end
end
You can use the ruby Tracer class:
ruby -r tracer your_main_script.rb
Also, you can take a look at your code and see where the loop may happen:
def wargame_turn
input = current_player.move(board)
if !board.valid_move?(input)
wargame_turn #### HERE'S A POTENTIAL INFINITE LOOP
So the heart of the problem seems at:
if !board.valid_move?(input)
It could be a good start.

Code not actually asserting in RSpec?

I'm new to Ruby and in various open source software I've noticed a number of "statements" in some RSpec descriptions that appear not to accomplish what they intended, like they wanted to make an assertion, but didn't. Are these coding errors or is there some RSpec or Ruby magic I'm missing? (Likelihood of weirdly overloaded operators?)
The examples, with #??? added to the suspect lines:
(rubinius/spec/ruby/core/array/permutation_spec.rb)
it "returns no permutations when the given length has no permutations" do
#numbers.permutation(9).entries.size == 0 #???
#numbers.permutation(9) { |n| #yielded << n }
#yielded.should == []
end
(discourse/spec/models/topic_link_spec.rb)
it 'works' do
# ensure other_topic has a post
post
url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}"
topic.posts.create(user: user, raw: 'initial post')
linked_post = topic.posts.create(user: user, raw: "Link to another topic: #{url}")
TopicLink.extract_from(linked_post)
link = topic.topic_links.first
expect(link).to be_present
expect(link).to be_internal
expect(link.url).to eq(url)
expect(link.domain).to eq(test_uri.host)
link.link_topic_id == other_topic.id #???
expect(link).not_to be_reflection
...
(chef/spec/unit/chef_fs/parallelizer.rb)
context "With :ordered => false (unordered output)" do
it "An empty input produces an empty output" do
parallelize([], :ordered => false) do
sleep 10
end.to_a == [] #???
expect(elapsed_time).to be < 0.1
end
(bosh/spec/external/aws_bootstrap_spec.rb)
it "configures ELBs" do
load_balancer = elb.load_balancers.detect { |lb| lb.name == "cfrouter" }
expect(load_balancer).not_to be_nil
expect(load_balancer.subnets.sort {|s1, s2| s1.id <=> s2.id }).to eq([cf_elb1_subnet, cf_elb2_subnet].sort {|s1, s2| s1.id <=> s2.id })
expect(load_balancer.security_groups.map(&:name)).to eq(["web"])
config = Bosh::AwsCliPlugin::AwsConfig.new(aws_configuration_template)
hosted_zone = route53.hosted_zones.detect { |zone| zone.name == "#{config.vpc_generated_domain}." }
record_set = hosted_zone.resource_record_sets["\\052.#{config.vpc_generated_domain}.", 'CNAME'] # E.g. "*.midway.cf-app.com."
expect(record_set).not_to be_nil
record_set.resource_records.first[:value] == load_balancer.dns_name #???
expect(record_set.ttl).to eq(60)
end
I don't think there is any special behavior. I think you've found errors in the test code.
This doesn't work because there's no assertion, only a comparison:
#numbers.permutation(9).entries.size == 0
It would need to be written as:
#numbers.permutation(9).entries.size.should == 0
Or using the newer RSpec syntax:
expect(#numbers.permutation(9).entries.size).to eq(0)

Strange behavior using CSV class [duplicate]

I'm working on the EventReporter project to help learn Ruby.
Here's what I've got so far:
require 'CSV'
puts 'Welcome to Event Reporter!'
print 'Enter command: '
command = gets.chomp
def clean(attribute, type)
if (type == 'regdate')
elsif (type == 'first_name')
elsif (type == 'last_name')
elsif (type == 'email_address')
elsif (type == 'homephone')
homephone = attribute
homephone = homephone.to_s.gsub(/\D/, '')
if (homephone.length < 10)
homephone = '0000000000'
elsif (homephone.length == 11)
if (homephone[0] == '1')
homephone[0] = ''
else
homephone = '0000000000'
end
elsif (homephone.length > 11)
homephone = '0000000000'
end
return homephone
elsif (type == 'street')
elsif (type == 'city')
elsif (type == 'state')
elsif (type == 'zipcode')
zipcode = attribute.to_s.rjust(5, "0")[0..4]
return zipcode
end
return attribute
end
queue = []
while (command != 'q') do
command = command.split
if (command[0] == 'load')
command[1] ? filename = command[1] : filename = 'event_attendees.csv'
attendees = CSV.open filename, headers: true, header_converters: :symbol
puts "Loaded #{filename}"
elsif (command[0] == 'find')
attribute = command[1]
criteria = command[2]
# REACHES HERE SECOND TIME AROUND
puts "#{command[0]} #{command[1]} #{command[2]}"
attendees.each do |attendee|
# ISNT REACHING HERE SECOND TIME AROUND
puts 'TEST'
# get cleaned attendee attribute
attendee_attribute = clean(attendee[attribute.to_sym], attribute)
# see if it matches the criteria input
if criteria.to_s.downcase.strip == attendee_attribute.to_s.downcase.strip
# if it does, add the attendee to the queue
puts 'Match!'
queue << attendee
end
end
end
print 'Enter command: '
command = gets.chomp
end
It seems that the attendees.each isn't being executed the second time through the while loop. Why is this?
~/practice/event_manager >> ruby 'lib/event_reporter.rb'
Welcome to Event Reporter!
Enter command: load
Loaded event_attendees.csv
Enter command: find zipcode 11111
find zipcode 11111
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
Enter command: find zipcode 11111
find zipcode 11111
Enter command: q
~/practice/event_manager >>
According to the docs, the CSV object behaves basically like a regular IO object. They keep track of their current position in the file which is advanced by reading through it, generally line by line. So on your first attendees.each, you read through the entire file. Subsequent calls to .each will try to read the next line, but there is not any since we are already at the end of the file hence your loop does not execute anymore.
You can fix this by rewinding the underlying IO instance to the beginning of the file, using #rewind. In your specific case, put it after iterating through the attendees.
attendees.each do |attendee|
# ...
end
attendees.rewind

Ruby CSV.each in while loop not executing second time through

I'm working on the EventReporter project to help learn Ruby.
Here's what I've got so far:
require 'CSV'
puts 'Welcome to Event Reporter!'
print 'Enter command: '
command = gets.chomp
def clean(attribute, type)
if (type == 'regdate')
elsif (type == 'first_name')
elsif (type == 'last_name')
elsif (type == 'email_address')
elsif (type == 'homephone')
homephone = attribute
homephone = homephone.to_s.gsub(/\D/, '')
if (homephone.length < 10)
homephone = '0000000000'
elsif (homephone.length == 11)
if (homephone[0] == '1')
homephone[0] = ''
else
homephone = '0000000000'
end
elsif (homephone.length > 11)
homephone = '0000000000'
end
return homephone
elsif (type == 'street')
elsif (type == 'city')
elsif (type == 'state')
elsif (type == 'zipcode')
zipcode = attribute.to_s.rjust(5, "0")[0..4]
return zipcode
end
return attribute
end
queue = []
while (command != 'q') do
command = command.split
if (command[0] == 'load')
command[1] ? filename = command[1] : filename = 'event_attendees.csv'
attendees = CSV.open filename, headers: true, header_converters: :symbol
puts "Loaded #{filename}"
elsif (command[0] == 'find')
attribute = command[1]
criteria = command[2]
# REACHES HERE SECOND TIME AROUND
puts "#{command[0]} #{command[1]} #{command[2]}"
attendees.each do |attendee|
# ISNT REACHING HERE SECOND TIME AROUND
puts 'TEST'
# get cleaned attendee attribute
attendee_attribute = clean(attendee[attribute.to_sym], attribute)
# see if it matches the criteria input
if criteria.to_s.downcase.strip == attendee_attribute.to_s.downcase.strip
# if it does, add the attendee to the queue
puts 'Match!'
queue << attendee
end
end
end
print 'Enter command: '
command = gets.chomp
end
It seems that the attendees.each isn't being executed the second time through the while loop. Why is this?
~/practice/event_manager >> ruby 'lib/event_reporter.rb'
Welcome to Event Reporter!
Enter command: load
Loaded event_attendees.csv
Enter command: find zipcode 11111
find zipcode 11111
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
Enter command: find zipcode 11111
find zipcode 11111
Enter command: q
~/practice/event_manager >>
According to the docs, the CSV object behaves basically like a regular IO object. They keep track of their current position in the file which is advanced by reading through it, generally line by line. So on your first attendees.each, you read through the entire file. Subsequent calls to .each will try to read the next line, but there is not any since we are already at the end of the file hence your loop does not execute anymore.
You can fix this by rewinding the underlying IO instance to the beginning of the file, using #rewind. In your specific case, put it after iterating through the attendees.
attendees.each do |attendee|
# ...
end
attendees.rewind

Resources