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

Annotate Active Support string extensions #185

Merged
merged 4 commits into from
Jan 9, 2024

Conversation

bdewater
Copy link
Contributor

@bdewater bdewater commented Nov 4, 2023

Completes String extensions for #137

Type of Change

  • Add RBI for a new gem
  • Modify RBI for an existing gem
  • Other:

Changes

@bdewater bdewater requested a review from a team as a code owner November 4, 2023 14:00
@bdewater bdewater marked this pull request as draft November 4, 2023 14:03
@bdewater bdewater marked this pull request as ready for review November 4, 2023 15:55
sig { params(separator: T.nilable(String), preserve_case: T.untyped, locale: T.nilable(Symbol)).returns(T.self_type) }
def parameterize(separator: "-", preserve_case: false, locale: nil); end

sig { params(count: T.nilable(Integer), locale: T.nilable(Symbol)).returns(T.self_type) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doh! Thanks, will fix

Copy link
Contributor

@KaanOzkan KaanOzkan left a comment

Choose a reason for hiding this comment

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

Thank you so much! Just a few comments.

def html_safe; end

sig { params(capitalize: T.untyped, keep_id_suffix: T.untyped).returns(T.self_type) }
def humanize(capitalize: true, keep_id_suffix: false); end
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Always a tough balance to strike when adding types to untyped code :)

I opted to err on the side of flexibility here since any truthy value will work here but happy to change to boolean if you want a more strict type.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not familiar with its usages so if you think being flexible is good for cases where an object is supplied I'm okay with it.

Copy link
Contributor

Choose a reason for hiding this comment

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

My $0.02: We should enforce booleans, as it improves the clarity of the API.

It's easy enough for callers to toss in a !!x if they need.

def first(limit = 1); end

sig { params(separate_class_name_and_id_with_underscore: T.untyped).returns(T.self_type) }
def foreign_key(separate_class_name_and_id_with_underscore = true); end

This comment was marked as resolved.

Copy link
Member

@paracycle paracycle left a comment

Choose a reason for hiding this comment

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

I gave this a really quick view, will look in more detail later, but here are my preliminary comments

rbi/annotations/activesupport.rbi Outdated Show resolved Hide resolved
rbi/annotations/activesupport.rbi Outdated Show resolved Hide resolved
rbi/annotations/activesupport.rbi Outdated Show resolved Hide resolved
@bdewater
Copy link
Contributor Author

Thanks @paracycle! Addressed your comments so far :)

@amomchilov
Copy link
Contributor

Rebased and resolved conflicts

Copy link
Contributor

@amomchilov amomchilov left a comment

Choose a reason for hiding this comment

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

Hey @bdewater, thanks for putting these together!

I'm just rebasing and making some small tweaks, and I'll merge this shortly.

Comment on lines 295 to 296
sig { returns(Module) }
def constantize; end
Copy link
Contributor

Choose a reason for hiding this comment

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

constantize can return anything, because anything can be assigned to constants:

ABC = 123
"ABC".constantize # => 123
Suggested change
sig { returns(Module) }
def constantize; end
sig { returns(T.anything) }
def constantize; end

Copy link
Member

Choose a reason for hiding this comment

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

Oh, nice catch.

Comment on lines 350 to 351
sig { params(separator: T.nilable(String), preserve_case: T.untyped, locale: T.nilable(Symbol)).returns(String) }
def parameterize(separator: "-", preserve_case: false, locale: nil); end
Copy link
Contributor

Choose a reason for hiding this comment

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

The separator can't be nil:

irb(main):014> "abc def ghi".parameterize
=> "abc-def-ghi"
irb(main):015> "abc def ghi".parameterize(separator: nil)
/Users/alex/.gem/ruby/3.3.0/gems/activesupport-7.0.8/lib/active_support/inflector/transliterate.rb:126:in `gsub!': no implicit conversion of nil into String (TypeError)

      parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
Suggested change
sig { params(separator: T.nilable(String), preserve_case: T.untyped, locale: T.nilable(Symbol)).returns(String) }
def parameterize(separator: "-", preserve_case: false, locale: nil); end
sig { params(separator: String, preserve_case: T.untyped, locale: T.nilable(Symbol)).returns(String) }
def parameterize(separator: "-", preserve_case: false, locale: nil); end

Comment on lines +353 to +354
sig { params(count: T.nilable(T.any(Integer, Symbol)), locale: T.nilable(Symbol)).returns(String) }
def pluralize(count = nil, locale = :en); end
Copy link
Contributor

Choose a reason for hiding this comment

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

Woah this is a weird sig (the implementation itself is strange, because the count can be the locale if you only pass 1 arg).

Comment on lines 365 to 366
sig { params(locale: T.nilable(Symbol)).returns(String) }
def singularize(locale = :en); end
Copy link
Contributor

Choose a reason for hiding this comment

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

The locale can't be nil:

irb(main):023> "cars".singularize
=> "car"
irb(main):024> "cars".singularize(nil)
/Users/alex/.gem/ruby/3.3.0/gems/i18n-1.14.1/lib/i18n/locale/fallbacks.rb:61:in `[]': nil is not a valid locale (I18n::InvalidLocale)

              old_raise.call(*args, **kwargs)
                             ^^^^^^^^^^^^^^^
Suggested change
sig { params(locale: T.nilable(Symbol)).returns(String) }
def singularize(locale = :en); end
sig { params(locale: T.nilable(Symbol)).returns(String) }
def singularize(locale = :en); end

Comment on lines 356 to 357
sig { params(patterns: T.nilable(T.any(Regexp, String))).returns(String) }
def remove(*patterns); end
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 be nil:

"abc".remove(nil)
/Users/alex/.gem/ruby/3.3.0/gems/activesupport-7.0.8/lib/active_support/core_ext/string/filters.rb:42:in `gsub!': wrong argument type nil (expected Regexp) (TypeError)

      gsub! pattern, ""
            ^^^^^^^^^^^
Suggested change
sig { params(patterns: T.nilable(T.any(Regexp, String))).returns(String) }
def remove(*patterns); end
sig { params(patterns: T.any(Regexp, String)).returns(String) }
def remove(*patterns); end

Likewise for #remove!

Comment on lines 393 to 394
sig { returns(T.nilable(Date)) }
def to_date; end
Copy link
Contributor

Choose a reason for hiding this comment

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

This method raises instead of returning nil:

irb(main):027> "abc".to_date
/Users/alex/.gem/ruby/3.3.0/gems/activesupport-7.0.8/lib/active_support/core_ext/string/conversions.rb:48:in `parse': invalid date (Date::Error)

    ::Date.parse(self, false) unless blank?
                 ^^^^^^^^^^^
Suggested change
sig { returns(T.nilable(Date)) }
def to_date; end
sig { returns(Date) }
def to_date; end

Comment on lines 396 to 397
sig { returns(T.nilable(DateTime)) }
def to_datetime; end
Copy link
Contributor

Choose a reason for hiding this comment

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

This method raises instead of returning nil:

irb(main):028> "abc".to_datetime
/Users/alex/.gem/ruby/3.3.0/gems/activesupport-7.0.8/lib/active_support/core_ext/string/conversions.rb:58:in `parse': invalid date (Date::Error)

    ::DateTime.parse(self, false) unless blank?
                     ^^^^^^^^^^^
Suggested change
sig { returns(T.nilable(DateTime)) }
def to_datetime; end
sig { returns(Date) }
def to_datetime; end

Comment on lines +399 to +400
sig { params(form: T.nilable(Symbol)).returns(T.nilable(Time)) }
def to_time(form = :local); end
Copy link
Contributor

Choose a reason for hiding this comment

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

Interestingly, this one does return nil on error, unlike #to_date and #to_datetime.

"abc".to_time # => nil

def html_safe; end

sig { params(capitalize: T.untyped, keep_id_suffix: T.untyped).returns(T.self_type) }
def humanize(capitalize: true, keep_id_suffix: false); end
Copy link
Contributor

Choose a reason for hiding this comment

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

My $0.02: We should enforce booleans, as it improves the clarity of the API.

It's easy enough for callers to toss in a !!x if they need.

Copy link
Contributor

@KaanOzkan KaanOzkan left a comment

Choose a reason for hiding this comment

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

I'd selfishly like it if we returned a union e.g.T.any(Date, Date::Error) to signify the exception possibility but I can imagine dealing with this would be unpopular among Ruby devs.

@andyw8
Copy link
Contributor

andyw8 commented Jan 9, 2024

@KaanOzkan do you mean you'd (in theory) like Rails to return an error rather than raise? Or is this actually the valid Sorbet way to write that a method may raise?

@KaanOzkan
Copy link
Contributor

Yes the method will have the return instead of raise to make that work which makes what I said even less feasible. Example: https://blog.jez.io/union-types-checked-exceptions/#:~:text=Here%E2%80%99s%20how%20we%E2%80%99d%20write%20that%20in%20Sorbet%3A

@amomchilov amomchilov merged commit 5ed51f0 into Shopify:main Jan 9, 2024
3 checks passed
@bdewater
Copy link
Contributor Author

bdewater commented Jan 9, 2024

Thanks for getting it over the finish line @amomchilov !

@bdewater bdewater deleted the as-string branch January 9, 2024 20:35
@Morriar Morriar added the rbi Change related to RBI annotations label Mar 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rbi Change related to RBI annotations
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants