Giter Club home page Giter Club logo

rubocop-sorbet's Introduction

Rubocop-Sorbet

A collection of Rubocop rules for Sorbet.

Installation

Just install the rubocop-sorbet gem

gem install rubocop-sorbet

or, if you use Bundler, add this line your application's Gemfile:

gem 'rubocop-sorbet', require: false

Usage

You need to tell RuboCop to load the Sorbet extension. There are three ways to do this:

RuboCop configuration file

Put this into your .rubocop.yml:

require: rubocop-sorbet

Alternatively, use the following array notation when specifying multiple extensions:

require:
  - rubocop-other-extension
  - rubocop-sorbet

Now you can run rubocop and it will automatically load the RuboCop Sorbet cops together with the standard cops.

Command line

rubocop --require rubocop-sorbet

Rake task

RuboCop::RakeTask.new do |task|
  task.requires << 'rubocop-sorbet'
end

Rubocop rules for RBI files

To enable the cops related to RBI files under the sorbet/rbi/ directory, put this in sorbet/rbi/.rubocop.yml:

inherit_gem:
  rubocop-sorbet: config/rbi.yml

This will turn off default cops for **/*.rbi files and enable the RBI specific cops.

You'll also need to add an entry to the main .rubocop.yml so that RBI files are included, e.g.:

AllCops:
  Include:
    - "sorbet/rbi/shims/**/*.rbi"

The Cops

All cops are located under lib/rubocop/cop/sorbet, and contain examples/documentation.

In your .rubocop.yml, you may treat the Sorbet cops just like any other cop. For example:

Sorbet/FalseSigil:
  Exclude:
    - lib/example.rb

Documentation

You can read about each cop supplied by RuboCop Sorbet in the manual.

Compatibility

Sorbet cops support the following versions:

  • Sorbet >= 0.5
  • Ruby >= 2.5

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/rubocop-sorbet. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

To contribute a new cop, please use the supplied generator like this:

bundle exec rake "new_cop[Sorbet/NewCopName]"

which will create a skeleton cop, a skeleton spec, an entry in the default config file and will require the new cop so that it is properly exported from the gem.

Don't forget to update the documentation with:

bundle exec rake generate_cops_documentation

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Rubocop::Sorbet project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

rubocop-sorbet's People

Contributors

alexevanczuk avatar ameketa avatar amomchilov avatar andyw8 avatar bitwise-aiden avatar byroot avatar casperisfine avatar cursedcoder avatar dduugg avatar dependabot[bot] avatar egiurleo avatar ethanjgoldberg avatar george-ma avatar ghiculescu avatar jakebrady5 avatar jeffcarbs avatar jez avatar kaanozkan avatar kddnewton avatar morriar avatar paracycle avatar petergoldstein avatar peterzhu2118 avatar rafaelfranca avatar ryanbrushett avatar sambostock avatar shopify-admins avatar shopify-services avatar st0012 avatar vinistock avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rubocop-sorbet's Issues

Sigil cops ignore double commented sigils

It seems that our cops are not catching the usage of

# # typed: true

and end up adding another sigil above that one - even though Sorbet does seem to understand the double commented sigil. We should make our cops more robust to this and also remove the extra leading comments since only one is necessary.

Add a rule to discourage use of `T::Struct#serialize`

As the Sorbet docs themselves admit, T::Struct#serialize has some surprising behavior, such that it is often better to implement serialization by hand.

To avoid these issues, it would be nice to have a rule that disallows T::Struct#serialize. Ideally it would include these additional features:

  • It should link to the Sorbet docs that explain why we're make this recommendation
  • It should link to an example of the preferred pattern
  • It should respect an allowlist of exceptions
  • It should not discourage use of T::Enum#serialize (which is safer)

I'm happy to contribute an implementation for review, but I'd love to hear any feedback about this approach in the meantime!

Option for extra newline on autocorrection of `ValidSigil`

When autocorrecting the Sorbet/ValidSigil cop on files without any magic string comments at the top, it adds the sigil without an extra newline like so:

# typed: false
class Banana
...

This generally trips the Layout/EmptyLineAfterMagicComment cop and a subsequent auto correct needs to be run. This is okay locally, but can be annoying in CI when we're using a tool like Pronto to make suggestions; we need to run the job again to pick up this difference.

It would be great to introduce an option to this cop to insert an extra blank line when it adds the magic string up top. I can imagine some teams would not like that behavior, but an option seems reasonable.

Would love some thoughts on this, or if I'm missing an easy way around this issue. I'll work on a PR soon to implement the option.

Inline visibility modifiers cause false positives in Sorbet/EnforceSignatures

To repro, create a.rb with:

# typed: strict

class A
  sig { void }
  private def one
    1
  end
end

Expected result: no Sorbet/EnforceSignatures violation
Actual result:

$ rubocop --only Sorbet/EnforceSignatures a.rb
Inspecting 1 file
C

Offenses:

a.rb:5:11: C: [Correctable] Sorbet/EnforceSignatures: Each method is required to have a signature.
  private def one ...
          ^^^^^^^

1 file inspected, 1 offense detected, 1 offense auto-correctable

The autocorrect here is also a bit busted:

$ rubocop --only Sorbet/EnforceSignatures -a a.rb
Inspecting 1 file
C

Offenses:

a.rb:5:11: C: [Corrected] Sorbet/EnforceSignatures: Each method is required to have a signature.
  private def one ...
          ^^^^^^^
a.rb:6:11: C: [Corrected] Sorbet/EnforceSignatures: Each method is required to have a signature.
          def one ...
          ^^^^^^^

1 file inspected, 2 offenses detected, 2 offenses corrected
$ cat a.rb
# typed: strict

class A
  sig { void }
  private sig { returns(T.untyped) }
          sig { returns(T.untyped) }
          def one
    1
  end
end

Best way to apply to all `rbi` files in the project

This is not strictly an issue with rubocop-sorbet, but I've been unable to find a general solution to this problem, and it keeps us from being able to use rubocop-sorbet.

The issue being that once **/*.rbi is added to the Include list for rubocop, it causes all of the other configured cops to run on rbi files, making a lot of complaints (and changes). And of course rbi generation going forward will continue to create them as-is, so in my mind it's not desirable to enforce the same rubocop rules within rbi files.

My preference would be to only include rbi files for the Sorbet cops, and exclude them for all other cops, but there's no elegant way to do this.

Can anyone provide some guidance on this? I can open a PR to add to the README if we can come up with a solution.

disabling signature cops on typed: false files

We use Sorbet/EnforceSignatures along with Pronto to ensure that newly added methods are having signatures. However, it doesn't make sense to add them on typed: false files as they won't be validated anyway. Can an option be added to not do so? e.g.

Sorbet/EnforceSignatures:
  Enabled: true
  EnforcedStyle: ignore_typed_false

Rubocop auto-correcting sigil and frozen_string_literal

Hi folks!

When creating new files in Rails applications and running rubocop -a to let it auto-correct the files, rubocop autocorrects by adding the sigil correctly, but then it can't autocorrect the missing frozen string literal comment.

I've tried this in two different repos, one on v0.4.0 and one on v0.4.1 and both behave the same way.

Here's some output:
I created the following class app/models/this_test.rb:

class ThisTest
  THIS_TEST = true
end

Ran rubocop -a:

 9:02PM $ rubocop -a
Inspecting 50 files
......................C...........................

Offenses:

app/models/this_test.rb:1:1: C: [Corrected] Sorbet/FalseSigil: No Sorbet sigil found in file. Try a typed: false to start (you can also use rubocop -a to automatically add this).
class ThisTest
^^^^^
app/models/this_test.rb:1:1: C: Style/FrozenStringLiteralComment: Missing frozen string literal comment.
# typed: false
^

50 files inspected, 2 offenses detected, 1 offense corrected

Checked the file, which now had a sigil but no # frozen_string_literal: true:

# typed: false
class ThisTest
  THIS_TEST = true
end

Running rubocop, it outputs the expected:

 9:03PM $ rubocop
Inspecting 50 files
......................C...........................

Offenses:

app/models/this_test.rb:1:1: C: Style/FrozenStringLiteralComment: Missing frozen string literal comment.
# typed: false
^

50 files inspected, 1 offense detected

Running rubocop -a a second time to see if it gets less confused now that the sigil has already been fixed, but it still can't fix it:

 9:03PM $ rubocop -a
Inspecting 50 files
......................C...........................

Offenses:

app/models/this_test.rb:1:1: C: Style/FrozenStringLiteralComment: Missing frozen string literal comment.
# typed: false
^

50 files inspected, 1 offense detected

cc @Shopify/sorbet

Discourage misuses of `T::Enum`

While T::Enum provides a low-effort, type-safe way to construct an exhaustive set of values, it is also less performant than implementing the same functionality using data structures from Ruby (e.g. list of constants).

rubocop-sorbet should discourage using T::Enum in ways that exacerbates performance issues or doesn't use T::Enum for its intended purpose.

Potential cops include:

  • Convert any T::Enum with one value into a constant
  • Disallow including the Comparable module in T::Enum (comparing T::Enum values has particularly poor performance)
  • Disallow case statements with more than a certain number of T::Enum values (motivation)

EDIT (2024-04-22): I decided not to create a cop forbidding case statements above a certain size.

After running some benchmarks, it seems that, at runtime, case statements have exponentially decreasing performance whether they're being used with T::Enum values or Ruby constants, and when statically type checking, time spent increases linearly. There isn't an obvious point at which performance drops off in either of these cases, making any cut off we assign totally arbitrary. We can revisit this issue if it comes up in the future.

Screenshot 2024-04-22 at 4 25 43 PM Screenshot 2024-04-22 at 4 28 00 PM

EDIT (2024-04-23): I've just verified that these results are the same with or without YJIT on (thanks @amomchilov for prompting me to do that).

Screenshot 2024-04-23 at 9 08 45 AM

Infinite loop using autocorrection

I've attempted to introduce rubocop-sorbet in a couple projects, and consistently encounter infinite loops with autocorrection. A fairly minimal repro is:

bundle gem foo
cd foo

# make gemspec valid:
sed -ie 's/TODO: //g' foo.gemspec
sed -i '' '/uri/d' foo.gemspec
sed -i '' '/homepage/d' foo.gemspec

bundle add rubocop rubocop-sorbet
cat << EOF >> .rubocop.yml
---
require:
  - rubocop-sorbet
EOF
bundle exec rubocop -a

the last fails, with the relevant part of the output looking like this:

Infinite loop detected in /Users/dug/git/foo/Gemfile.
/Users/dug/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rubocop-0.80.1/lib/rubocop/runner.rb:273:in `block in iterate_until_no_changes'
/Users/dug/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rubocop-0.80.1/lib/rubocop/runner.rb:269:in `loop'
/Users/dug/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rubocop-0.80.1/lib/rubocop/runner.rb:269:in `iterate_until_no_changes'
/Users/dug/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rubocop-0.80.1/lib/rubocop/runner.rb:240:in `do_inspection_loop'
/Users/dug/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rubocop-0.80.1/lib/rubocop/runner.rb:119:in `block in file_offenses'

Gemfile ends up with 100 # frozen_string_literal: true lines, and also each typed sigil, e.g.:

# frozen_string_literal: true
# typed: strong
# typed: strict
# typed: true
# typed: false
# typed: ignore
# typed: false
# frozen_string_literal: true
# frozen_string_literal: true
# frozen_string_literal: true
# (97 more of the previous line)

I typically configure rubocop.yml differently, e.g. disabling strong & strict sigils, but still see infinite loops.
Also, this isn't specific to Gemfile – even if that file is excluded, it repros on subsequent files.

Feature Request: Style/MutableConstant applied to T.let argument

The Style/MutableConstant cop ensures that literal constants (or all constants in strict style) are frozen.

However, in a typed: strict file, constants must have T.let annotations. Sorbet does not recognize T.let calls that have.freeze appended to them. This largely invalidates the MutableConstant cop (since literal constants are wrapped by T.let calls, and the latter cannot be frozen).

I would like a sorbet-compatible version of the Style/MutableConstant cop. It should require literal first arguments to T.let to be frozen with EnforcedStyle set to literals (the default), or all first arguments when set to strict.

Would the maintainers be amenable to such a cop?

Explain EnforceSigilOrder better in documentation

The first thing we often get when installing rubocop-sorbet is a mass amount of requests to change the sigil ordering in files. That's a big commit, and it might cause people to ask what the significance of the ordering means. But the documentation does not provide any of this.

`EnforceSignatures` cop doesn't detect `sig(:final)`

It appears that the EnforceSignatures cop has trouble accepting sig(:final) as a valid signature. This might also affect other signature-based cops.

Here's a failing code example.

# typed: true

class Boundary
  sig(:final) { returns(Elem) }
  def call
    trace { filter }
  end
end

Here's the cop configuration:

Sorbet/EnforceSignatures:
  Enabled: true
  AutoCorrect: false
  Severity: warning
  Exclude:
    - 'config/**/*'
   ...

imagen

Expected behaviour:

The method doesn't trigger the cop's warning, since it has a valid signature.

BindingConstantWithoutTypeAlias erroring

As of sorbet/sorbet#1929, T.type_alias takes a block and evaluates lazily. See also https://sorbet.org/docs/type-aliases

rubocop-sorbet seems to not like this. FooTwo and FooThree both error with "Sorbet/BindingConstantWithoutTypeAlias: It looks like you're trying to bind a type to a constant. To do this, you must alias the type using T.type_alias."

# typed: strict

class Bar
  extend T::Sig

  FooOne = T.type_alias(T.any(String, Integer))
  FooTwo = T.type_alias { T.any(String, Integer) }
  FooThree = T.type_alias do
    T.any(String, Integer)
  end
end

rubocop-sorbet :: undefined method `source' for nil:NilClass

Actual behavior

undefined method `source' for nil:NilClass
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-sorbet-0.6.2/lib/rubocop/cop/sorbet/callback_conditionals_binding.rb:128:in `on_send'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:100:in `public_send'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:100:in `block (2 levels) in trigger_responding_cops'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:160:in `with_cop_error_handling'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:99:in `block in trigger_responding_cops'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:98:in `each'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:98:in `trigger_responding_cops'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:69:in `on_send'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-ast-1.11.0/lib/rubocop/ast/traversal.rb:137:in `block in on_dstr'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-ast-1.11.0/lib/rubocop/ast/traversal.rb:137:in `each'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-ast-1.11.0/lib/rubocop/ast/traversal.rb:137:in `on_dstr'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:71:in `on_begin'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-ast-1.11.0/lib/rubocop/ast/traversal.rb:154:in `on_class'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:71:in `on_class'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-ast-1.11.0/lib/rubocop/ast/traversal.rb:154:in `on_class'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:71:in `on_class'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-ast-1.11.0/lib/rubocop/ast/traversal.rb:20:in `walk'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/commissioner.rb:86:in `investigate'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/team.rb:155:in `investigate_partial'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cop/team.rb:83:in `investigate'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:309:in `inspect_file'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:253:in `block in do_inspection_loop'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:287:in `block in iterate_until_no_changes'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:280:in `loop'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:280:in `iterate_until_no_changes'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:249:in `do_inspection_loop'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:130:in `block in file_offenses'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:155:in `file_offense_cache'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:129:in `file_offenses'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:120:in `process_file'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:101:in `block in each_inspected_file'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:100:in `each'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:100:in `reduce'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:100:in `each_inspected_file'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:86:in `inspect_files'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/runner.rb:47:in `run'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli/command/execute_runner.rb:26:in `block in execute_runner'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli/command/execute_runner.rb:52:in `with_redirect'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli/command/execute_runner.rb:25:in `execute_runner'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli/command/execute_runner.rb:17:in `run'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli/command.rb:11:in `run'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli/environment.rb:18:in `run'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli.rb:71:in `run_command'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli.rb:78:in `execute_runners'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/lib/rubocop/cli.rb:47:in `run'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/exe/rubocop:12:in `block in <top (required)>'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/2.7.0/benchmark.rb:308:in `realtime'
/Users/robynelliott/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/rubocop-1.21.0/exe/rubocop:12:in `<top (required)>'
/Users/robynelliott/.rbenv/versions/2.7.3/bin/rubocop:23:in `load'
/Users/robynelliott/.rbenv/versions/2.7.3/bin/rubocop:23:in `<main>'

Steps to reproduce the problem

rubocop --require rubocop-sorbet --format offenses

RuboCop version

1.21.0 (using Parser 3.0.2.0, rubocop-ast 1.11.0, running on ruby 2.7.3 x86_64-darwin20)

ValidSigil parsing behaves differently than sorbet sigil parsing

Bug Description

The ValidSigil cop considers any valid sigil value followed by any non-word non-whitespace character to be valid. Sorbet does not. This conflict causes the ValidSigil cop to be overly permissive of functionally invalid sigil values.

Practical Example

A sample file began with this sigil:

# typed: false # rubocop:todo Sorbet/StrictSigil

A similar regex bug in spoom caused spoom to erroneously bump the sigil to:

# typed: true# rubocop:todo Sorbet/StrictSigil

This new true# sigil is functionally ignored by Sorbet, but does not cause a rubocop error despite being an invalid sigil value.

Suggested cop: prefer `first`/`drop`/`last` over `[]` with particular kinds of ranges

There's a bunch of overloads of #slice/#[], many of which are technically nilable, but don't return nil in practice.

We can't fix that in the general case, but of a lot uses with ranges start from 0 (or ending in -1 can be replaced by more dedicated methods like first/drop/last, which aren't nilable.

A challenge here is that the APIs aren't consistent between Array and String (and other types), and there's no way to only make the change for particular types.

These examples assume:

a = Array(0..9)
s = Array("a".."z").join

Replace with first

Applicable for Array, but requires a patch from ActiveSupport for String.

-T.must(s[0..5])
-T.must(s[ ..5])
+s.first(6)
-T.must(s[0...5])
-T.must(s[ ...5])
-T.must(s[0, 5])
+s.first(5)

Replace with last

Applicable for Array, but not to String.

-T.must(a[5..-1])
-T.must(a[5..  ])
-T.must(a[5... ])
+a.drop(5)

Replace with dup

Really, these likely just be s, but we can't be sure that the callers weren't relying on the result being a new copy of the string, so we'd conservatively need to dup it.

-T.must(s[0..])
-T.must(s[0...])
-T.must(s[..-1])
s.dup

Publish version 1.0

According to Semantic Versioning

  1. Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.
  2. Version 1.0.0 defines the public API. The way in which the version number is incremented after this release is dependent on this public API and how it changes.

We appear to be past initial development, so we should publish 1.0 and provide consumers with a better stability contract.

There are a couple changes that would make sense to include in this release:

Sorbet/AllowIncompatibleOverride inspection error

When parsing a signature resembling sig { override(allow_incompatible: true).void } the inspection errors as follows:

NoMethodError:
  undefined method `each_node' for nil:NilClass
# ./lib/rubocop/cop/sorbet/signatures/allow_incompatible_override.rb:39:in `sig?'
# ./lib/rubocop/cop/sorbet/signatures/allow_incompatible_override.rb:32:in `allow_incompatible_override?'
# ./lib/rubocop/cop/sorbet/signatures/allow_incompatible_override.rb:41:in `on_send'

Test case added on this branch reproduces the error.

rspec ./spec/rubocop/cop/sorbet/signatures/allow_incompatible_override_spec.rb:17

Seems potentially related to a recent refactor?

Improve autocorrection for `Sorbet/ForbidIncludeConstLiteral` cop

Changing the autocorrect of:

class Foo
  include Bar.module
end

from:

class Foo
  BarInclude = Bar.module
  include BarInclude
end

to:

class Foo
  T.unsafe(self).include Bar.module
end

is better since that allows Sorbet to totally ignore the include call but the runtime behaviour is exactly the same and we don't end up creating new constant names.

If users still need the methods from that include, they can add a shim RBI file to signal the inclusion of the proper module kind.

Sorbet/EnforceSignatures has some false positives

When using an argument or using the WithoutRuntime sig, the rubocop will warn that it's lacking a signature and the autocorrect will insert a new one.

# frozen_string_literal: true

# typed: strict

require 'sorbet'

class Mine
  extend T::Sig

  sig(:final) { void }
  def test_final
  end

  T::Sig::WithoutRuntime.sig { void }
  def test_without_runtime
  end
end

Rubocop output:

Offenses:

test_sig.rb:11:3: C: [Correctable] Sorbet/EnforceSignatures: Each method is required to have a signature.
  def test_final ...
  ^^^^^^^^^^^^^^
test_sig.rb:15:3: C: [Correctable] Sorbet/EnforceSignatures: Each method is required to have a signature.
  def test_without_runtime ...
  ^^^^^^^^^^^^^^^^^^^^^^^^

1 file inspected, 2 offenses detected, 2 offenses auto-correctable

Corrected:

# frozen_string_literal: true

# typed: strict

require 'sorbet'

class Mine
  extend T::Sig

  sig(:final) { void }
  sig { returns(T.untyped) }
  def test_final
  end

  T::Sig::WithoutRuntime.sig { void }
  sig { returns(T.untyped) }
  def test_without_runtime
  end
end

My expectation is that both these forms should pass the lint rule.

Integration with the Sorbet LSP

This is something I've thought about a few times, and I was reminded of it by #130:

If RuboCop was able to use the Sorbet LSP to know about types, it could provide more accurate linting and safer auto-correction.

This would likely be a big change, and perhaps doesn't even belong in rubocop-sorbet itself, but I'd love to experiment to see what could be done.

Suggested cop: time/date for const/prop defaults

We were recently affected by a bug where had stale times/dates for struct defaults --- which is not surprising, just easy to overlook. We ended up writing the cop below which could be generally useful.

I'd be happy to work at creating a PR, but I thought it would be great to get general feedback first. My concerns:

  • this cop currently isn't smart enough to know whether the prop or const declaration is within a T::Struct definition, so it is likely prone to false positives; it works in our code base fine but I imagine it might need to be a bit more contextual for general usage
  • this is an easy cop for autocorrect, but I'm still learning rubocop 😅
  class StructDefaultTime < Cop
        MSG = "Struct property defaults are evaluated only once. Use a factory for time and date defaults"
        DATE_OR_TIME_METHODS = %i[since from_now after ago until before yesterday tomorrow now today].to_set.freeze
        STRUCT_DECLARATIONS = %i[const prop].to_set.freeze

        def on_send(node)
          struct_default(node) do |_, default_node|
            nested_date_or_time(default_node) do
              add_offense(node)
            end
          end
        end

        private

        def_node_matcher :struct_default, <<~PATTERN
          (send nil? $STRUCT_DECLARATIONS
            (sym _)
            (const nil? _)
            (hash
              (pair
                (sym :default) $(...))))
        PATTERN

        def_node_matcher :date_or_time, <<~PATTERN
          (send _ $DATE_OR_TIME_METHODS)
        PATTERN

        def nested_date_or_time(node, &callback)
          return if node.nil? || node.block_type?

          node.each_child_node do |child|
            nested_date_or_time(child, &callback)
          end

          date_or_time(node, &callback)
        end
      end

The rough test suite looks something like:

 class StructDefaultTimeTest < ActiveSupport::TestCase
        include RubocopTestHelper

        def setup
          @cop = RuboCop::Cop::Bourgeois::StructDefaultTime.new
        end

        test "ok for non date/time props and constants with or without defaults" do
          assert_no_offenses("const :foo, Integer, default: 3")
          assert_no_offenses("prop :bar, String, default: 'foo'")
          assert_no_offenses("const :baz, Time")
        end

        test "ok for factory date and time props and constants" do
          assert_no_offenses("const :baz, Time, factory: -> { Time.zone.now }")
        end

        test "offense when using current or relative date/time methods" do
          assert_offense("const :foo, Date, default: Time.zone.today")
          assert_offense("const :foo, Time, default: Time.zone.now")
          assert_offense("const :foo, Date, default: 3.days.from_now")
          assert_offense("const :foo, Date, default: Time.zone.now.yesterday")
          assert_offense("const :foo, Time, default: 10.minutes.ago")
          assert_offense("const :foo, Date, default: 5.days.before(Date.today)")

          assert_offense("prop :foo, Date, default: Time.zone.today")
          assert_offense("prop :foo, Time, default: Time.zone.now")
          assert_offense("prop :foo, Date, default: 3.days.from_now")
          assert_offense("prop :foo, Date, default: Time.zone.now.yesterday")
          assert_offense("prop :foo, Time, default: 10.minutes.ago")
          assert_offense("prop :foo, Date, default: 5.days.before(Date.today)")
        end
      end

Sorbet/ForbidTStruct - autocorrect does not handle optional params without default or factory

Hi, was experimenting with the new ForbidTStruct cop and noticed the autocorrect does not correctly generate the initializer for nilable properties without an explicitly defined default/factory value. The initializer should have them as optional arguments

Example

class Foo < T::Struct
  const :name, T.nilable(String)
end

Foo.new.name # <--- nil

Expected after autocorrect

class Foo
  extend T::Sig

  sig { returns(T.nilable(String)) }
  attr_reader :name

  sig { params(name: T.nilable(String)).void }
  def initialize(name: nil)
    @name = name
  end
end

Foo.new.name # <--- nil

Current output of autocorrect

class Foo
  extend T::Sig

  sig { returns(T.nilable(String)) }
  attr_reader :name

  sig { params(name: T.nilable(String)).void }
  def initialize(name:)
    @name = name
  end
end

Foo.new.name # <--- ArgumentError missing keyword: :name

The result is that name becomes a required param

`BindingConstantWithoutTypeAlias` cop triggering on false positive cases.

Given the code:

class Foo
  ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
end

it is obvious that there is no constant binding there thus no need for a type_alias call. However, the BindingConstantWithoutTypeAlias cop treats this as a violation and tries to autocorrect this to:

class Foo
  ConstantType = T.type_alias { type_member { { fixed: T.class_of(::ActiveRecord::Base) } } }
end

which is wrong.

This is causing problems with the new type_member syntax that is expecting blocks like above.

`BindingConstantWithoutTypeAlias` false positive

TModel = type_member(fixed: T.untyped)

^ that code gets this error:

Sorbet/BindingConstantWithoutTypeAlias: It looks like you're trying to bind a type to a constant. To do this, you must alias the type using T.type_alias.
    TModel = type_member(fixed: T.untyped)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

And autocorrects to this:

TModel = T.type_alias { type_member(fixed: T.untyped) }

Which is incorrect:

app/graphql/types/platform/basic_view.rb:4: Type variable TModel needs to be declared as `= type_member(SOMETHING)` https://srb.help/5018
     4 |    TModel = T.type_alias { type_member(fixed: T.untyped) }
            ^^^^^^

app/graphql/types/platform/basic_view.rb:4: Malformed type declaration. Unknown type syntax. Expected a ClassName or T.<func> https://srb.help/5004
     4 |    TModel = T.type_alias { type_member(fixed: T.untyped) }
                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Error In Sorbet/ParametersOrderingInSignature when parameter name is missing

If you forget to include the parameter name in a signature, Sorbet/ParametersOrderingInSignature fails rather than reporting an error.

Repro:

# typed: true

class Test
  extend T::Sig

  sig { params(Integer).void }
  def test(abc); end
end

Actual Behavior:

flexport$ rubocop -a test.rb
Inspecting 1 file
An error occurred while Sorbet/ParametersOrderingInSignature cop was inspecting /Users/rklingler/flexport/test.rb:7:2.
To see the complete backtrace run rubocop -d.
.

1 file inspected, no offenses detected

1 error occurred:
An error occurred while Sorbet/ParametersOrderingInSignature cop was inspecting /Users/rklingler/flexport/test.rb:7:2.

Expected Behavior:

I would expect the cop to not crash. Since the problem isn't exactly about parameter ordering, it may not be this cop's responsibility to report the error, but it shouldn't crash.

Version Info

Rubocop: 0.80.1
Rubocop-sorbet: 0.3.7

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.