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

use named arguments in spec methods #14915

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

kamil-gwozdz
Copy link
Contributor

closes #14759

@@ -45,7 +45,7 @@ module Spec::Methods
# It is usually used inside a `#describe` or `#context` section.
#
# If `focus` is `true`, only this test, and others marked with `focus: true`, will run.
def it(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, focus : Bool = false, tags : String | Enumerable(String) | Nil = nil, &block)
def it(description = "assert", file : String = __FILE__, line : Int32 = __LINE__, end_line : Int32 = __END_LINE__, focus : Bool = false, tags : String | Enumerable(String) | Nil = nil, &block)
Copy link
Member

@Blacksmoke16 Blacksmoke16 Aug 17, 2024

Choose a reason for hiding this comment

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

So this isn't really forcing these parameters to be named arguments. It's only adding a type restriction to them. You'd really need to do something like this, ditto for the other src/spec/methods.cr files:

Suggested change
def it(description = "assert", file : String = __FILE__, line : Int32 = __LINE__, end_line : Int32 = __END_LINE__, focus : Bool = false, tags : String | Enumerable(String) | Nil = nil, &block)
def it(description = "assert", *, file : String = __FILE__, line : Int32 = __LINE__, end_line : Int32 = __END_LINE__, focus : Bool = false, tags : String | Enumerable(String) | Nil = nil, &block)

As called out in https://crystal-lang.org/reference/1.13/syntax_and_semantics/default_values_named_arguments_splats_tuples_and_overloading.html#how-call-arguments-are-matched-to-method-parameters.

I'd be open keeping the more explicit type restrictions if we can ensure they already produce compile time errors if you pass the wrong type as this change would just make it more clear where the problem is without changing the underlying behavior.

EDIT: Might be worth adding new named only overloads and deprecate the current positional ones versus changing things directly to avoid a breaking change as well.

Copy link
Contributor Author

@kamil-gwozdz kamil-gwozdz Aug 17, 2024

Choose a reason for hiding this comment

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

Might be worth adding new named only overloads

Defining it like this isn't possible because external names have to be unique:

# this throws `Error: duplicated def parameter external name: file`:
def it(description = "assert", file : String = __FILE__, *, file file_named = __FILE__) 

do you think it would be okay to rename the positional arguments in this case?

my plan would be to do something like:

def it(description = "assert", file_deprecated : String | Nil = nil, *, file = __FILE__)
  file = file_deprecated || file 
  raise_deprecation_warning unless file_deprecated.nil?

WDYT? Would it be ok to execute the deprecation warning check at runtime? Or does it have to happen at compilation time? I found this code that handles deprecation logic but it doesn't seem to handle the kind of the change that I want to make.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nvm, i just figured out that it's possible to overload the whole method

Copy link
Member

@Blacksmoke16 Blacksmoke16 Aug 17, 2024

Choose a reason for hiding this comment

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

They would need to be separate overloads, versus just messing with the existing method. We definitely want the warnings to be at compile time.

Something along the lines of:

def it(description : String = "assert", *, file : String = __FILE__, line : Int32 = __LINE__, end_line : Int32 = __END_LINE__, focus : Bool = false, tags : String | Enumerable(String) | Nil = nil, &block)
  Spec.cli.root_context.it(description.to_s, file, line, end_line, focus, tags, &block)
end

# :ditto:
@[Deprecated("Arguments other than the first should be provided as named arguments.")]
def it(description = "assert", _file file = __FILE__, _line line = __LINE__, _end_line end_line = __END_LINE__, _focus focus : Bool = false, _tags tags : String | Enumerable(String) | Nil = nil, &block)
  Spec.cli.root_context.it(description.to_s, file, line, end_line, focus, tags, &block)
end

However it seems like compiler doesn't really like this as:

require "spec"

it "foo", line: 123 do
  1.should eq 1
end

it "bar" do
  2.should eq 2
end

Results in an unexpected:

In test.cr:7:1

 7 | it "bar" do
     ^-
Warning: Deprecated ::it. Arguments other than the first should be provided as named arguments.

A total of 1 warnings were found.

It does work as expect with the -Dpreview_overload_order flag, but idt we can rely upon that until Crystal 2.0. So not sure what our options are at this point...

Copy link
Member

@Blacksmoke16 Blacksmoke16 Aug 18, 2024

Choose a reason for hiding this comment

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

I figured out a way for this to work I think. It's a bit less than ideal, but does allow using the @[Deprecated] annotation, with both versions showing up in the API docs.

The problem with it picking the correct overload stems from the fact the methods are defined in a module then included into the top level namespace. What we can do to work around that is add an empty bodied overload with the proper type restrictions and :ditto: doc comment. This will allow both overloads to show up in the API docs, with the old one flagged as deprecated.

Then we can put the real implementations of the overloads in a macro included so that they are also added to the top level, but after the ones from the module itself. From my toy example I don't get any deprecation warnings so it seems to be working at least.

  # ... 
  #
  # <Docs for the current method>
  @[Deprecated("Arguments other than the first should be provided as named arguments.")]
  def it(_description description = "assert", _file file = __FILE__, _line line = __LINE__, _end_line end_line = __END_LINE__, _focus focus : Bool = false, _tags tags : String | Enumerable(String) | Nil = nil, &block)
    Spec.cli.root_context.it(description.to_s, file, line, end_line, focus, tags, &block)
  end

  # :ditto:
  def it(description : _ = "assert", *, file : String = __FILE__, line : Int32 = __LINE__, end_line : Int32 = __END_LINE__, focus : Bool = false, tags : String | Enumerable(String) | Nil = nil, &block)
  end

  # ...

  macro included
    def it(description : _ = "assert", *, file : String = __FILE__, line : Int32 = __LINE__, end_line : Int32 = __END_LINE__, focus : Bool = false, tags : String | Enumerable(String) | Nil = nil, &block)
      Spec.cli.root_context.it(description.to_s, file, line, end_line, focus, tags, &block)
    end

    # ...
  end

WDYT?

EDIT: I think it's impt to not add an explicit String restriction on description. As that would prevent the very common use case of doing things like describe MyClass. Maybe for now we at least make it explicit via a _ type restriction?

@straight-shoota
Copy link
Member

suggestion: We could use this opportunity to introduce type restrictions for all parameters (as previsously mentioned in
#14915 (comment)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Force named arguments for spec methods
3 participants