Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extend Contracts for raised and thrown objects #263

Open
md-work opened this issue Aug 23, 2017 · 2 comments
Open

extend Contracts for raised and thrown objects #263

md-work opened this issue Aug 23, 2017 · 2 comments

Comments

@md-work
Copy link

md-work commented Aug 23, 2017

Thanks for this really great gem!

It helps a lot checking the types of objects being send up and down the stack trough method calls and returns. Sadly there's still no way to check the type of objects being send trough the stack by the Ruby raise and throw commands.

I'm thinking about something similar to throws Exception in Java.
(sure this isn't Java and the checking can only be done dynamically, not statically)

public void example() throws Exception {
    throw new Exception();
}

What do you think about adding such an feature to Contracts?

  • Would it technically be possible?
  • What could be a nice and backward compatible syntax?

See also: #193

@egonSchiele
Copy link
Owner

Hi @md-work, this is a neat idea. Based on the comments in #193, would you like to add a contract that says "if an exception is raised, catch it and raise a contract exception"? Or would you want to annotate methods saying "this can throw an exception"?

@md-work
Copy link
Author

md-work commented Aug 28, 2017

Hi @egonSchiele
I'm talking about "this can throw an exception".

Examples:

Contract String => String raises Errno::ENOENT
def read_file(filename)
    # This raises Errno::ENOENT if the filename doesn't exist.
    # In this case everything's fine the Contract should let the
    # exception pass (or re-raise it without modification).

    # But in case the user doesn't has permissions to read the file,
    # this raises Errno::EACCES.
    # In this case the Contract should catch the exception and raise an
    # own exception instead (e.g. a RaiseContractError).

    return File.read(filename).to_s
end


Contract Integer, Integer => Integer raises Contracts::None
def add(num_a, num_b)
    # This should never raise an exception.
    return num_a + num_b
end


# This method usually never raises an exception. Just in one very rare
# situation, about which the programmer forgot when he wrote the
# contract.
# So if the method gets 23 it raises :illuminati and the Contract
# should catch that and raise an own exception instead.
Contract Integer => Integer raises Contracts::None
def some_fun(number)
    if number = 23
        raise :illuminati
    else
        return number
    end
end


# Same with a fixed contract.
Contract Integer => Integer raises :illuminati
def some_fun(number)
    if number = 23
        raise :illuminati
    else
        return number
    end
end


# Same for throw-catch like for raise-rescue.
Contract Integer => Integer throws :illuminati
def some_fun(number)
    if number = 23
        throw :illuminati
    else
        return number
    end
end


# And for both, raise-rescue and throw-catch.
Contract Integer => Integer throws :illuminati raises :answer
def some_fun(number)
    if number = 23
        raise :illuminati
    elsif number = 42
        throw :answer
    else
        return number
    end
end

Some notes

  • Remember: In Ruby you can't just raise or throw objects from exception classes, but also every other object.
  • In case a Contract is violated by a throw, it should raise an ThrowContractError. The ThrowContractError should be raised, because throw is for normal control flow and not for errors.
  • Unchecked exceptions: There should be a whitelist of raised objects/classes, which must not be checked and don't trigger a RaiseContractError, because those exceptions can just happen everywhere. E.g. objects of the classes ArgumentError, NoMemoryError and ZeroDivisionError.
    • There should be the possibility to modify the whitelist and add more objects/classes to it.
    • This is only for raise, not for throw.
    • Optionally there could be a whitelist for throw, but it should be empty by default.
    • Java has a similar whitelist for. In Java everything that inherits from RuntimeError is an unchecked exception and doesn't have to be declared at the method header. https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html
    • Objects of classes inheriting from whitelisted classes should be unchecked too.
    • Here's a list of Rubys standard exception classes: https://ruby-doc.org/core-2.2.0/Exception.html
    • Optionally there could be the possibility to set blacklists of objects/classes for raise and throw. By default those blacklists should be empty, but if they get set only blacklisted objects/classes should be checked.
  • Optionally there should be global raises_none and throws_none settings. If those get set, every method with a Contract but without a Contract ... throws ... should be considered to throw nothing. (except whitelisted objects/classes)
  • In case the raises/throws part of a Contract is violated, the RaiseContractError/ThrowContractError should reference the original raised/thrown object, so the developer can look up what originally happened.

@md-work md-work changed the title extend Contracts for raise and rescue objects extend Contracts for raised and thrown objects Aug 29, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants