Skip to content

Add Object#not_nil! assertion#7

Merged
amomchilov merged 2 commits intomainfrom
Alex/not_nil
Feb 23, 2026
Merged

Add Object#not_nil! assertion#7
amomchilov merged 2 commits intomainfrom
Alex/not_nil

Conversation

@amomchilov
Copy link
Contributor

@amomchilov amomchilov commented Feb 20, 2026

When debugging a nil-related error, it can be difficult to trace back where the nil actually originated from. It could have come in from a parameter, whose argument was read from an instance variable, on an object loaded from a cache, populated by some totally different request.

If a value can't be nil, it's best for that to be clearly asserted as close to where that nilable value was first generated. That way, a rogue nil isn't allowed to propagate arbitrary far away in downstream code.

This PR:

  1. Adds an Object#not_nil! method (inspired by https://bugs.ruby-lang.org/issues/17326 and https://github.com/ruby/ruby/pull/11772/files)
    • I've confirmed that nothing else uses this method name, across the entire Shopify monorepo.
  2. Adds a Cop to ensure that people don't rely on asserting this error in tests

@amomchilov
Copy link
Contributor Author

amomchilov commented Feb 20, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@amomchilov amomchilov changed the title Add not_nil! assertion method Add Object#not_nil! assertion Feb 20, 2026
@amomchilov amomchilov marked this pull request as ready for review February 20, 2026 00:47
Copy link

@dersam dersam left a comment

Choose a reason for hiding this comment

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

We copied this over to Shopify/roast a while ago. I'll swap over to this gem once it's open sourced.

cc: @juniper-shopify

@amomchilov amomchilov changed the base branch from Alex/gem-infra to graphite-base/7 February 20, 2026 16:33
end

class NilClass
# @override

Choose a reason for hiding this comment

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

Why does this need override?

Copy link
Contributor Author

@amomchilov amomchilov Feb 20, 2026

Choose a reason for hiding this comment

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

It's not strictly necessary. @override isn't unless the parent is explicitly marked @overrideable (whether you use RBS or sig {} syntax).

But this is indeed an override, since NilClass < Kernel, so I opted to document that fact

@amomchilov amomchilov changed the base branch from graphite-base/7 to Alex/gem-infra February 23, 2026 23:30
Base automatically changed from Alex/gem-infra to main February 23, 2026 23:47
@amomchilov amomchilov merged commit 4b7e80e into main Feb 23, 2026
1 check passed
@amomchilov amomchilov deleted the Alex/not_nil branch February 23, 2026 23:52
module TypeToolkit
# This cop detects attempts to raise, rescue, or otherwise use the `UnexpectedNilError` class.
class DontExpectUnexpectedNil < Base
RESTRICT_ON_SEND = [:assert_raises, :raise].freeze
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need raise here? Look like we commented the branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The branch shouldn't be commented. And the tests should have caught that. 🤦‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was initially thinking following in the footsteps of Active Support, https://guides.rubyonrails.org/active_support_core_extensions.html#loading-all-active-support , but decided against and changed this in #13

# so that bare rescues clauses (like `rescue => e`) don't rescue it.
class UnexpectedNilError < Exception # rubocop:disable Lint/InheritException
def initialize(message = "Called `not_nil!` on nil.")
super(message)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't is just be super?

Suggested change
super(message)
super

# `UnexpectedNilError` should never occur in well-formed code, so it should never be rescued.
# This is why it inherits from `Exception` instead of `StandardError`,
# so that bare rescues clauses (like `rescue => e`) don't rescue it.
class UnexpectedNilError < Exception # rubocop:disable Lint/InheritException
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want this to be namespaced or are we happy having it at the root?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm leaning towards namespaced.

My dream is for this gem to be common/standard enough that it's worthy of owning top-level constants (to look "clean" and "native" 😎), but I won't get too big for my britches, so I'll make it namespaced.

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

Successfully merging this pull request may close these issues.

4 participants