Skip to content

Add Object#not_nil! assertion - #7

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

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

Conversation

@amomchilov

@amomchilov amomchilov commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

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

amomchilov commented Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

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

@dersam dersam left a comment

Copy link
Copy Markdown

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

Copy link
Copy Markdown

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?

@amomchilov amomchilov Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

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
Comment thread lib/rubocop/cop/type_toolkit/dont_expect_unexpected_nil.rb
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
Copy Markdown
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
Copy Markdown
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. 🤦‍♂️

Comment thread lib/type_toolkit/all.rb

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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