-
-
Notifications
You must be signed in to change notification settings - Fork 253
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 cop to check for ActiveModel errors hash direct manipulation #491
Conversation
138cd0c
to
1399e20
Compare
config/default.yml
Outdated
@@ -49,6 +49,11 @@ Rails/ActionFilter: | |||
Include: | |||
- app/controllers/**/*.rb | |||
|
|||
Rails/ActiveModelErrorsDirectManipulation: | |||
Description: 'Avoid manipulating ActiveModel errors hash directly.' | |||
Enabled: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it disabled by default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was not sure if this will be helpful, or creating too much noise. But if you are okay then I do want to try making it enabled.
module RuboCop | ||
module Cop | ||
module Rails | ||
# This cop checks direct manipulation of ActiveModel#errors as hash. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it ActiveModel::Errors
or ActiveModel::Validators#errors
?
# This cop checks direct manipulation of ActiveModel#errors as hash. | |
# This cop checks direct manipulation of `ActiveModel::Errors` as hash. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method is defined in ActiveModel::Validations#errors
, but people mostly know it as model.errors
. It is also a class of ActiveModel::Errors
. So both are correct.
# # good | ||
# user.errors.delete(:name) | ||
# | ||
class ActiveModelErrorsDirectManipulation < Base |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about the name DeprecatedActiveModelErrorsMethods
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the cop is targeting Rails 6.1 or higher only, can you specify extend TargetRailsVersion
and minimum_target_rails_version 6.1
(and add tests)?
- https://github.com/rubocop/rubocop-rails/blob/v2.10.1/lib/rubocop/cop/rails/application_record.rb#L21-L23
- https://github.com/rubocop/rubocop-rails/blob/v2.10.1/spec/rubocop/cop/rails/application_record_spec.rb#L4
- https://github.com/rubocop/rubocop-rails/blob/v2.10.1/spec/rubocop/cop/rails/application_record_spec.rb#L52
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are many other methods which are deprecated. This is only the dynamic manipulation part. What do you think of DeprecatedActiveModelErrorsDynamicManipulation
?
If the cop is targeting Rails 6.1 or higher only
I recommend running this before upgrading to Rails 6.1, so developers can upgrade the usage, and be ready for Rails 7 in the future. In theory it should be runnable since Rails 3.0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about the name DeprecatedActiveModelErrorsMethods?
I originally aligned this with other cops, e.g.: ActiveRecordCallbacksOrder
and ActiveSupportAliases
, so I feel maybe we should keep this for consistency?
lib/rubocop/cop/rails/active_model_errors_direct_manipulation.rb
Outdated
Show resolved
Hide resolved
spec/rubocop/cop/rails/active_model_errors_direct_manipulation_spec.rb
Outdated
Show resolved
Hide resolved
RUBY | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be acceptable if receiver is not specified:
errors[:name] << 'msg'
Can you add the test case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I think about it, I feel it is important if the receiver is not specified. However it could leave to many false positives. I want to find a way so this receiver-less check can be done only to files under the models
directory. I wonder if that is possible.
# user.errors.delete(:name) | ||
# | ||
class ActiveModelErrorsDirectManipulation < Base | ||
MSG = 'Avoid manipulating ActiveModel errors as hash directly.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MSG = 'Avoid manipulating ActiveModel errors as hash directly.' | |
MSG = 'Avoid manipulating `ActiveModel::Errors` as hash directly.' |
9b91e7e
to
1fcef9d
Compare
@lulalala Is there any guidance on how to migrate away from direct Hash access when testing? Not sure if it fits better as an expect(model.errors).to have_key(:name)
expect(model.errors[:name]).to include("cannot be blank") |
@swanson This cop will only dealt with manipulation, not reading(accessing), but to answer your questions:
For this, we will be moving to:
My preferred way would be:
But if you have suggestions or other thoughts, let me know :). |
1fcef9d
to
9bd4fd2
Compare
Hi @koic I've made some changes and replied to your questions. Could you check please? Thanks! 🏓 |
9bd4fd2
to
ed3c3eb
Compare
34dc95b
to
a019d12
Compare
Hi @koic could you recheck please? Thanks! |
7900677
to
50c27fe
Compare
Hi @koic could you recheck please? I've rebased and everything is passing now. Thanks! |
1 similar comment
Hi @koic could you recheck please? I've rebased and everything is passing now. Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Useful cop, indeed. Thanks for the contribution.
A few mostly cosmetic notes below.
Before it's merged, would you run the cop against some large codebases to check how well it performs?
private | ||
|
||
def file_type | ||
filename = File.expand_path(processed_source.buffer.name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is expand_path
needed? What is usually in processed_source.buffer.name
, just user.rb
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some checking processed_source.buffer.name
just returns full path name. Sorry it was long ago so I can't remember why I added this 😛. I've remove it.
model_file: '{nil? send ivar lvar}' | ||
}.freeze | ||
|
||
PATTERN = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you agree to separate those patterns and define them statically? It's possible to combine them with { }
.
Like:
MANIPULATIVE_METHODS = Set[:<<, :append, ...].freeze
def_node_pattern :root_manipulation?, <<~PATTERN
(send
(send
(send %1 :errors) :[] ...)
MANIPULATIVE_METHODS
...
)
)
PATTERN
def_node_pattern :any_manipulation?, <<~PATTERN
{
root_manipulation?(%1)
root_assignment?(%1)
}
PATTERN
def_node_pattern :general?, <<~PATTERN
{send ivar lvar}
PATTERN
def on_send(node)
send_pattern =
'{nil? send ivar lvar}' # I might be mistaken, check docs https://docs.rubocop.org/rubocop-ast/node_pattern.html#composing-complex-expressions-with-multiple-matchers
else
:general? # or like this? https://github.com/rubocop/rubocop-rspec/blob/573f22bbbd54e5332231b9823b87843550ae21a4/lib/rubocop/cop/rspec/multiple_expectations.rb#L91
end
any_manipulation?(node, send_pattern) do |node|
add_offence(...)
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure if we can avoid all these string interpolation. However I actually know very little about how the rubocop-ast works. I tried to apply what you provided, but it would say
undefined method `def_node_pattern' for RuboCop::Cop::Rails::ActiveModelErrorsDirectManipulation:Class
Did you mean? def_node_matcher
When I tried using def_node_matcher
it would say:
RuboCop::AST::NodePattern::Invalid:
parse error on value :")" (")")
If you could pull some AST expertise on this it would be greatly appreciated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll just send the whole thing as I see a canonical implementation.
Leaving it up to you if you find it readable - it's up to you to support the cop (hopefully :) anyway.
class ActiveModelErrorsDirectManipulation < Base
MSG = 'Avoid manipulating ActiveModel errors as hash directly.'
MANIPULATIVE_METHODS = Set[%i[<< append clear collect! compact! concat
delete delete_at delete_if drop drop_while fill filter! keep_if
flatten! insert map! pop prepend push reject! replace reverse!
rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift]].freeze
def_node_matcher :general?, '{send ivar lvar}'
def_node_matcher :model?, '{nil? send ivar lvar}'
def_node_matcher :any_manipulation?, <<~PATTERN
{
#root_manipulation?
#root_assignment?
#messages_details_manipulation?
#messages_details_assignment?
}
PATTERN
def_node_matcher :root_manipulation?, <<~PATTERN
(send
(send
(send #file_type_match? :errors) :[] ...)
#MANIPULATIVE_METHODS
...
)
PATTERN
def_node_matcher :root_assignment?, <<~PATTERN
(send
(send #file_type_match? :errors)
:[]=
...)
PATTERN
def_node_matcher :messages_details_manipulation?, <<~PATTERN
(send
(send
(send
(send #file_type_match? :errors)
{:messages :details})
:[]
...)
#MANIPULATIVE_METHODS
...)
PATTERN
def_node_matcher :messages_details_assignment?, <<~PATTERN
(send
(send
(send #file_type_match? :errors)
{:messages :details})
:[]=
...)
PATTERN
def on_send(node)
any_manipulation?(node) do
add_offense(node)
end
end
private
def file_type_match?(node)
model_file? ? model?(node) : general?(node)
end
def model_file?
processed_source.buffer.name.include?('/models/')
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much! I made some renamings and it works. I've placed this as a separate commit with you being the author. Could you review? 🏓
50c27fe
to
7939fbb
Compare
Thanks @pirj I've pushed some changes up (except the last one because it's not working). This is extracted from gitlab, which is quite large already, but I still went ahead and tried "real-world-rails". Would you happen to know how to rubocop into git submodules' content? |
I guess the failure is due to
it should start with "new_". |
d63797d
to
62a13f8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect, thank you for the contribution, your patience and persistence! 👏
Merry Christmas and a Happy New year, @lulalala !
config/default.yml
Outdated
Rails/ActiveModelErrorsDirectManipulation: | ||
Description: 'Avoid manipulating ActiveModel errors hash directly.' | ||
Enabled: pending | ||
Safe: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just out of curiosity – is there a good reason to make this cop unsafe? There is a possibility that it detects non-AR statements, but I haven't seen such code in practice.
Going back to the question I forgot to answer:
Would you happen to know how to rubocop into git submodules' content?
You git checkout --include-submodules
, and then fd --hidden --no-ignore .rubocop | xargs rm
. I don't know a better way to skip submodule's RuboCop configuration.
But usually GitLab's source is a good enough sandbox.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pirj Yes, non-AR classes with errors
method is what I fear, would that qualify as "unsafe"? We don't have those in GitLab codebase, but I feel errors
is generic enough that it can happen in utility classes.
Thanks. I found that there are some false positives on non-manipulative methods:
redmine/app/controllers/wiki_controller.rb:74:10: C: Rails/ActiveModelErrorsDirectManipulation: Avoid manipulating ActiveModel errors as hash directly.
if @page.errors[:title].blank?
^^^^^^^^^^^^^^^^^^^^^^^^^^^
This happens after the refactoring. I might be able to take a look tomorrow (and adding more tests). So please hold off from merging for now. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pirj I've added specs and fixed the issue:
- The
MANIPULATIVE_METHODS
was a single element Set (the element being an array). I splat it now. - The constant seems to be references without
#
so I removed it.
Spec seems to pass so I guess all is good?
62a13f8
to
b82aad2
Compare
Looks good to me! |
I'm wondering about false positives for many of the methods defined in Maybe it seems better useful to add auto-correction with a cop called % cd path/to/github.com/rails/rails
% git checkout v6.1.4
% git grep 'ActiveModel::Errors message'
activemodel/lib/active_model/errors.rb:617: ActiveSupport::Deprecation.warn("Calling `delete` to an ActiveModel::Errors messages hash is deprecated. Please call `ActiveModel::Errors#delete` instead.")
activemodel/lib/active_model/errors.rb:646: ActiveSupport::Deprecation.warn("Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead.")
activemodel/lib/active_model/errors.rb:654: ActiveSupport::Deprecation.warn("Calling `clear` to an ActiveModel::Errors message array in order to delete all errors is deprecated. Please call `ActiveModel::Errors#delete` instead.") |
@koic Due to the false positives I don't think adding auto correct is a good idea. It's better for them to inspect each case and manually adjust. If you are afraid of false positives, how about make this off by default? The example you grepped in Rails was written by me (and there are a few others like |
Well, I will merge this cop after the next bug fix release. Before that, please add Apart from that, I may have a look at what to do with (unsafe) auto-correction later. Anyway, it will not covered in this PR :-) |
b82aad2
to
857df6f
Compare
@koic I've added You already mentioned that you will review this after the next bug release though (which was 2.12.3). I was hoping we can speed up the process if there is nothing blocking the way, please? |
# @safety | ||
# This cop is unsafe because it can report `errors` manipulation on non-ActiveModel, | ||
# which is obviously valid. | ||
# The cop has no way of knowing whether a variable is an ActiveModel or not, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the comma at the end of the sentence a typo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, my bad. I've corrected this.
857df6f
to
e7ae6bb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this cop useful. One change I would like you to make is to rename to Rails/DeprecatedActiveModelErrorsMethods
cop. I was stuck at the name Rails/ActiveModelErrorsDirectManipulation
cop, so I will merge this PR after renaming.
70a3473
to
be384aa
Compare
@koic I've renamed the cop. Thanks. |
This looks good to me. Can you squash your commits into one? |
These are deprecated in Rails 6.1 and will be removed in Rails 7. See rails/rails#32313 for details. The cop acts in two modes: For files under `/models` directory, any `errors` call, whether with receiver or not, will be checked. For general files, only `errors` calls with receivers will be checked. E.g. `errors[:bar] = []` is without receiver. It will record an offense if it is a model file. It will not record an offense if it is other general fie. This is to reduce false-positives, since other classes may also have a `errors` method.
be384aa
to
c145336
Compare
@koic OK I've squashed them. Thanks. |
@lulalala Thank you! |
`[]` by itself is not deprecated, but `[]<<` is. I think in rails 7 we
still want to prevent these kind of direct manipulation since they will not
have a lasting effect.
Bohdan Schepansky ***@***.***> 於 2022年11月25日 週五 20:48 寫道:
… Looks like this cop has been made obsolete by this commit
<rails/rails@ef40a92>,
and [] is no longer deprecated. What's interesting, though, is that this
has actually happened *before* this PR was even merged...
—
Reply to this email directly, view it on GitHub
<#491 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACZSPQKFOO5ICG4AY2I67LWKCYTRANCNFSM45G6GWOQ>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Indeed it is so! I realised my mistake just as I posted that comment, so deleted it right after. :) Sorry for the inconvenience caused. |
This cop checks direct manipulation of ActiveModel#errors as hash.
These operations are deprecated in Rails 6.1 and will not work in Rails 7.
This was recently added to GitLab codebase and doesn't seem to create too much noise, but any advice is welcomed.
It's my first time using the parser so if there is a more elegant way to write the checker please let me know ^^.
Before submitting the PR make sure the following are checked:
[Fix #issue-number]
(if the related issue exists).master
(if not - rebase it).and description in grammatically correct, complete sentences.
bundle exec rake default
. It executes all tests and RuboCop for itself, and generates the documentation.