Skip to content

Commit

Permalink
Allow rescuing exceptions that include a module (crystal-lang#14553)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blacksmoke16 authored May 10, 2024
1 parent 62a6944 commit d83e42f
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 4 deletions.
160 changes: 160 additions & 0 deletions spec/compiler/codegen/exception_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1319,4 +1319,164 @@ describe "Code gen: exception" do
end
))
end

it "handles rescuing module type" do
run(%(
require "prelude"
module Foo; end
class Ex1 < Exception
include Foo
end
x = 0
begin
raise Ex1.new
rescue Foo
x = 1
end
x
)).to_i.should eq(1)
end

it "handles rescuing union between module type and class type" do
run(%(
require "prelude"
module Foo; end
abstract class BaseError < Exception; end
class Ex2 < BaseError; end
class Ex1 < BaseError
include Foo
end
x = 0
begin
raise Ex1.new
rescue Foo | BaseError
x = 1
end
x
)).to_i.should eq(1)
end

it "handles rescuing union between module types" do
run(%(
require "prelude"
module Foo; end
module Bar; end
class Ex1 < Exception
include Foo
end
class Ex2 < Exception
include Bar
end
x = 0
begin
raise Ex1.new
rescue Foo | Bar
x = 1
end
x
)).to_i.should eq(1)
end

it "does not rescue just any module" do
run(%(
require "prelude"
module Foo; end
module Bar; end
class Ex < Exception
include Foo
end
x = 0
begin
begin
raise Ex.new("oh no")
rescue Bar
x = 1
end
rescue ex
x = 2
end
x
)).to_i.should eq(2)
end

it "rescues a valid union" do
run(%(
require "prelude"
module Foo; end
module Bar; end
class Ex < Exception
include Foo
end
x = 0
begin
raise Ex.new("oh no")
rescue Union(Foo, Bar)
x = 1
end
x
)).to_i.should eq(1)
end

it "rescues a valid nested union" do
run(%(
require "prelude"
module Foo; end
module Bar; end
module Baz; end
class Ex < Exception
include Foo
end
x = 0
begin
raise Ex.new("oh no")
rescue Union(Baz, Union(Foo, Bar))
x = 1
end
x
)).to_i.should eq(1)
end

it "does not rescue just any union" do
run(%(
require "prelude"
module Foo; end
module Bar; end
module Baz; end
class Ex < Exception
include Foo
end
x = 0
begin
raise Ex.new("oh no")
rescue Union(Bar, Baz)
x = 1
rescue
x = 2
end
x
)).to_i.should eq(2)
end
end
12 changes: 10 additions & 2 deletions spec/compiler/semantic/exception_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,19 @@ describe "Semantic: exception" do
end

it "errors if caught exception is not a subclass of Exception" do
assert_error "begin; rescue ex : Int32; end", "Int32 is not a subclass of Exception"
assert_error "begin; rescue ex : Int32; end", "Int32 cannot be used for `rescue`. Only subclasses of `Exception` and modules, or unions thereof, are allowed."
end

it "errors if caught exception is a union but not all types are valid" do
assert_error "begin; rescue ex : Union(Exception, String); end", "(Exception | String) cannot be used for `rescue`. Only subclasses of `Exception` and modules, or unions thereof, are allowed."
end

it "errors if caught exception is a nested union but not all types are valid" do
assert_error "begin; rescue ex : Union(Exception, Union(Exception, String)); end", "(Exception | String) cannot be used for `rescue`. Only subclasses of `Exception` and modules, or unions thereof, are allowed."
end

it "errors if caught exception is not a subclass of Exception without var" do
assert_error "begin; rescue Int32; end", "Int32 is not a subclass of Exception"
assert_error "begin; rescue Int32; end", "Int32 cannot be used for `rescue`. Only subclasses of `Exception` and modules, or unions thereof, are allowed."
end

assert_syntax_error "begin; rescue ex; rescue ex : Foo; end; ex",
Expand Down
16 changes: 14 additions & 2 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2770,14 +2770,26 @@ module Crystal
false
end

private def allowed_type_in_rescue?(type : UnionType) : Bool
type.union_types.all? do |subtype|
allowed_type_in_rescue? subtype
end
end

private def allowed_type_in_rescue?(type : Crystal::Type) : Bool
type.implements?(@program.exception) || type.module?
end

def visit(node : Rescue)
if node_types = node.types
types = node_types.map do |type|
type.accept self
instance_type = type.type.instance_type
unless instance_type.implements?(@program.exception)
type.raise "#{instance_type} is not a subclass of Exception"

unless self.allowed_type_in_rescue? instance_type
type.raise "#{instance_type} cannot be used for `rescue`. Only subclasses of `Exception` and modules, or unions thereof, are allowed."
end

instance_type
end
end
Expand Down

0 comments on commit d83e42f

Please sign in to comment.