tapioca icon indicating copy to clipboard operation
tapioca copied to clipboard

Suggestion: Treat ActiveModel::Attribute as non-nilable if a PresenceValidator is present

Open lavoiesl opened this issue 9 months ago • 1 comments

class Foo
  include ActiveModel::Model

  attribute :name, :string
  validates :name, presence: true
end

Under normal circumstances, Foo#name should never be nil.

Yes, it's possible to get a nil like Foo.new.name, but I'd argue that returning a type error would make sense here.

I ended up implementing this tapioca extension in my codebase:

# sorbet/tapioca/compilers/active_model_presence_validator.rb

require "active_model/attributes"
require "tapioca/dsl/compilers/active_model_attributes"

module Tapioca
  module Compilers
    module ActiveModelPresenceValidator
      def attribute_methods_for_constant
        return super unless constant.is_a?(ActiveModel::Validations::ClassMethods)

        attribute_methods = super.to_h # Convert to hash for easier manipulation

        attribute_methods.each do |method, type|
          has_unconditional_validator = constant
            .validators_on(method)
            .grep(ActiveModel::Validations::PresenceValidator)
            # Skip if any options would mean that the attribute is not always non-nilable
            .any? { |v| v.options.slice(:if, :unless, :on, :allow_nil, :allow_blank).empty? }

          next unless has_unconditional_validator

          type = as_non_nilable_type(type)
          attribute_methods[method] = type
          attribute_methods["#{method}="] = type
        end

        attribute_methods.to_a
      end
    end

    Dsl::Compilers::ActiveModelAttributes.prepend(ActiveModelPresenceValidator)
  end
end

It's hacky, but it works 😅

lavoiesl avatar Apr 09 '25 18:04 lavoiesl

See also known-type signatures: https://github.com/Shopify/tapioca/issues/2250

lavoiesl avatar Apr 09 '25 18:04 lavoiesl