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

Add new Lint/NonDeterministicRequireOrder cop #7528

Merged

Conversation

Mangara
Copy link
Contributor

@Mangara Mangara commented Nov 27, 2019

What is this PR trying to do?

Dir[...] and Dir.glob(...) make no guarantees about the order files are returned in:

Case sensitivity depends on your system [...], as does the order in which the results are returned.

This becomes a problem when they are used for applications in which the order can matter, such as requiring files. At worst, this can lead to bugs that only happen intermittently in production and can't be reproduced in development.

This cop suggests adding a .sort:

# bad
Dir["./lib/middleware/*.rb"].each do |file|
  require file
end

# good
Dir["./lib/middleware/*.rb"].sort.each do |file|
  require file
end

Questions for reviewers

  • Should it test for more patterns?

The current implementation very specifically looks for these three patterns: Dir[_].each do |_|, Dir.glob(_).each do |_|, and Dir.glob(_) do |_|. Are there other common patterns that have the same effect? Should we allow for intermediate operations between the Dir[...] and .each?

  • Should it apply regardless of whether the file is required?

There are other use cases where the order matters, although this cannot in general be detected statically, and you may want to avoid the performance penalty associated with sorting. We could add a configuration option: Always, OnlyForRequires?

  • Is there a better way to match the pattern?

Before submitting the PR make sure the following are checked:

  • Wrote good commit messages.
  • Commit message starts with [Fix #issue-number] (if the related issue exists).
  • Feature branch is up-to-date with master (if not - rebase it).
  • Squashed related commits together.
  • Added tests.
  • Added an entry to the Changelog if the new code introduces user-observable changes. See changelog entry format.
  • The PR relates to only one subject with a clear title and description in grammatically correct, complete sentences.
  • Run bundle exec rake default. It executes all tests and RuboCop for itself, and generates the documentation.

@Mangara Mangara force-pushed the add-non-deterministic-require-order-cop branch 2 times, most recently from f2fdf25 to 02586f9 Compare November 27, 2019 20:29
@Mangara Mangara force-pushed the add-non-deterministic-require-order-cop branch from 02586f9 to 607379a Compare November 27, 2019 20:40
@Mangara Mangara force-pushed the add-non-deterministic-require-order-cop branch from 607379a to f0b033c Compare November 27, 2019 21:35
@Mangara
Copy link
Contributor Author

Mangara commented Nov 29, 2019

@buehmann What needs to happen before we can merge this?

Copy link
Contributor

@buehmann buehmann left a comment

Choose a reason for hiding this comment

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

@Mangara I just did a full review and left some comments. In the end we need someone from the core team to visit this.

CHANGELOG.md Outdated Show resolved Hide resolved
lib/rubocop/cop/lint/non_deterministic_require_order.rb Outdated Show resolved Hide resolved
lib/rubocop/cop/lint/non_deterministic_require_order.rb Outdated Show resolved Hide resolved
context 'with unsorted glob' do
it 'registers an offsense' do
expect_offense(<<~RUBY)
Dir.glob(Rails.root.join(__dir__, 'test', '*.rb'), File::FNM_DOTMATCH).each do |file|
Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding your question

Should it test for more patterns?

I think it might make sense to support this case as well from the beginning:

Dir.glob(Rails.root.join(__dir__, 'test', '*.rb'), File::FNM_DOTMATCH) do |file|
  require file
end

(that is glob taking a block without each)

What do you think? (Adding this should be rather easy with a {(send …) (send …)} alternative in unsorted_dir_loop?.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes! Good catch. We should definitely support that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this the best way to write the new pattern?

{ (send (const nil? :Dir) :glob ...) (send (send (const nil? :Dir) {:[] :glob} ...) :each) }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Only the .glob variant supports the direct block syntax)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ended up splitting it into two patterns to help with autocorrection.

@Mangara Mangara force-pushed the add-non-deterministic-require-order-cop branch from f0b033c to 6a73177 Compare December 2, 2019 21:00
Comment on lines 45 to 49
var_name = loop_variable(node.arguments)

return unless var_name && var_is_required?(node.body, var_name)

add_offense(node.send_node)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The generated methods also take a block, which is yielded to in the case of a match. 🙂

loop_variable(node.arguments) do |var_name|
  return unless var_is_required?(node.body, var_name)

  add_offense(node.send_node)
end

Looks like you could also potentially combine the patterns into a single matcher.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, nice! I like the block syntax.

As for the single matcher, I like that all of the building blocks are fairly simple patterns. Merging them into one would make it harder to understand, I think.

Copy link
Collaborator

@Drenmi Drenmi left a comment

Choose a reason for hiding this comment

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

Great job! 🎉

Some minor nits, but overall looks good to me.

@Mangara Mangara force-pushed the add-non-deterministic-require-order-cop branch from 6a73177 to 0635f79 Compare December 3, 2019 15:14
@bbatsov
Copy link
Collaborator

bbatsov commented Dec 9, 2019

Can you rebase this on top of master?

@Mangara Mangara force-pushed the add-non-deterministic-require-order-cop branch from 0635f79 to 161781d Compare December 9, 2019 13:54
@Mangara
Copy link
Contributor Author

Mangara commented Dec 10, 2019

@bbatsov @koic Is this ready to merge?

@koic
Copy link
Member

koic commented Dec 11, 2019

The following commit is due to the impact of this cop addition.
161781d

Can you squash your commits into one?

@koic
Copy link
Member

koic commented Dec 11, 2019

And I think @bbatsov will decide whether the next release will be new feature release or bug fix release. Please wait until the next release is a release that includes new features.

Dir[...] and Dir.glob(...) make no guarantees about the order files
are returned in:

> Case sensitivity depends on your system [...], as does the order
> in which the results are returned.

This becomes a problem when they are used for applications in which
the order can matter, such as requiring files. At worst, this can
lead to bugs that only happen intermittently in production and can't
be reproduced in development.

This cop suggests adding a .sort when requiring the files:

> # bad
> Dir["./lib/middleware/*.rb"].each do |file|
>   require file
> end

> # good
> Dir["./lib/middleware/*.rb"].sort.each do |file|
>   require file
> end
@Mangara Mangara force-pushed the add-non-deterministic-require-order-cop branch from 161781d to 4e236c4 Compare December 11, 2019 13:59
@bbatsov bbatsov merged commit cc7e221 into rubocop:master Dec 11, 2019
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.

5 participants