Skip to content

Commit

Permalink
Fixed #3706: rescue in initialize can lead to uninitialized instance …
Browse files Browse the repository at this point in the history
…variables
Ary Borenszweig committed Dec 15, 2016

Verified

This commit was signed with the committer’s verified signature. The key has expired.
nomadium Miguel Landaeta
1 parent 366aec3 commit 02d4db3
Showing 3 changed files with 42 additions and 47 deletions.
76 changes: 30 additions & 46 deletions spec/compiler/semantic/exception_spec.cr
Original file line number Diff line number Diff line change
@@ -355,52 +355,6 @@ describe "Semantic: exception" do
)) { int32 }
end

it "doesn't type instance variable if initialized inside begin and rescue raises" do
assert_type(%(
require "prelude"
class Foo
def initialize
begin
@bar = 1
rescue
raise "OH NO"
end
end
def bar
@bar
end
end
foo = Foo.new
foo.bar
)) { int32 }
end

it "doesn't type instance variable if initialized inside begin and in rescue" do
assert_type(%(
require "prelude"
class Foo
def initialize
begin
@bar = 1
rescue
@bar = 2
end
end
def bar
@bar
end
end
foo = Foo.new
foo.bar
)) { int32 }
end

it "correctly types #1988" do
assert_type(%(
begin
@@ -445,4 +399,34 @@ describe "Semantic: exception" do
var
)) { union_of(int32, string) }
end

it "marks instance variable as nilable if assigned inside rescue inside initialize" do
assert_error %(
require "prelude"
def some_method
if rand < 0.999999999
raise "OH NO"
else
2
end
end
class Coco < Exception
def initialize(@x : Foo)
end
end
class Foo
def initialize
@x = 1

This comment has been minimized.

Copy link
@luislavena

luislavena Dec 15, 2016

Contributor

Question, in the issue #3706 @x is being assigned to some_method but in this case that method is ignored.

Perhaps some_method can be removed as it doesn't affect initialize?

This comment has been minimized.

Copy link
@asterite

asterite Dec 15, 2016

Member

Ah, you are right. I think I tried it with simpler code and then forgot to change it.

The rule will apply regardless of whether there's a method call inside the begin-rescue body, because it's safer and the compiler has to do less work. I'll fix the spec. Thank you!

rescue
raise Coco.new(self)
end
end
Foo.new
),
"instance variable '@x' of Foo must be Int32, not Nil"
end
end
2 changes: 2 additions & 0 deletions src/compiler/crystal/semantic/exception.cr
Original file line number Diff line number Diff line change
@@ -232,6 +232,8 @@ module Crystal
io << colorize("instance variable '#{nil_reason.name}' was used before it was initialized in one of the 'initialize' methods, rendering it nilable").bold
when :used_self_before_initialized
io << colorize("'self' was used before initializing instance variable '#{nil_reason.name}', rendering it nilable").bold
when :initialized_in_rescue
io << colorize("instance variable '#{nil_reason.name}' is initialized inside a begin-rescue, so it can potentially be left uninitialized if an exception is raised and rescued").bold
end
end

11 changes: 10 additions & 1 deletion src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
@@ -2490,6 +2490,15 @@ module Crystal
# was raised before assigning any of the vars.
exception_handler_vars.each do |name, var|
unless before_body_vars[name]?
# Instance variables inside the body must be marked as nil
if name.starts_with?('@')
ivar = scope.lookup_instance_var(name)
unless ivar.type.includes_type?(@program.nil_var)
ivar.nil_reason = NilReason.new(name, :initialized_in_rescue)
ivar.bind_to @program.nil_var
end
end

var.nil_if_read = true
end
end
@@ -2549,7 +2558,7 @@ module Crystal
end

# However, those previous variables can't be nil afterwards:
# if an exception was raised then we won't running the code
# if an exception was raised then we won't be running the code
# after the ensure clause, so variables don't matter. But if
# an exception was not raised then all variables were declared
# successfully.

0 comments on commit 02d4db3

Please sign in to comment.