-
-
Notifications
You must be signed in to change notification settings - Fork 282
Add new cop RSpec/LeakyLocalVariable
#2101
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
Conversation
Here's the cop report for real-world-rspec. I wrote a custom formatter which prints the URL to the file and line on the remote, so you can go through it and see offenses directly on GH. There are 866 offenses for all codebases in that repo. Report
|
next unless part_of_example_scope?(reference) | ||
next if permitted_argument?(reference) | ||
|
||
add_offense(assignment.node) |
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.
Initially I added the offense to the reference node, but that created offense noise (if an offending variable is referenced many times, there will be many offenses). An offense on the assignment node reduces that noise, but it may not be clear which reference triggered the offense. Let me know what you think is the better option.
Here's an example:
user = create(:user)
# ^^^^^^^^^^^^^^^^^^^^ assignment offense (only one)
before do
user.update(admin: true)
# ^^^^ reference offense
user.flag!
# ^^^^ reference offense (repeated for all references)
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.
I agree that the offence should be where the var is defined, as this is the place that needs correction, not usages of the var.
1c78177
to
c791895
Compare
The problem is not the variable, but a call to factory outside of the example, made outside of the example lifecycle. Variables come handy at times, because you can’t reasonably use constants (see LeakyConstant). Would you agree to re-target the cop to catch those out-of-scope factory calls? |
I agree that that's a problem, but I don't think it's the problem. A local variable that's not a call to a factory can still be equally problematic, e.g.: foo = 'string'
it 'does something' do
foo << 'modification'
expect(foo).to eq('stringmodification')
end
it 'does something else' do
expect(foo).to eq('string') # this example can fail depending on the example order
end I would agree that some examples in there are harmless, but a lot of them are unnecessarily made local variables. To give a few examples:
Most of the examples in the report follow this pattern. Also, despite being harmless, I think that |
I've reconsidered my past opinion, as the risk of make specs non-deterministic is not justifiable by making specs more concise or marginally more performant (e.g. reading some JSON config). Please let me know if you're still up to continue with this cop. As a side note, there are other known ways to introduce non-determinism similarly. |
Great, thank you! I will rebase the branch and also resolve offenses on the Rails monolith I'm working on (300+ offenses have been caught there for this cop) to see if there are any false positives or areas to improve the cop. I'll let you know once it's done so you can continue with the review. |
I ran this cop on my monolith (~1500 spec files) and found 9 offenses. None were false positives. One thing I noticed however, is that the cop recommends that I use a |
45a65e4
to
6eeea96
Compare
I rebased the branch and I think it's ready for review now. I went over the offenses in my monolith and found one false positive with factory bot, for which I added a check (
Hmm yeah, I think the offense message was too prescriptive, but the solution really depends on the context. I updated it to something more generic, let me know how it looks. |
cac4294
to
77a7e91
Compare
|
||
def check_references(variable) | ||
variable.assignments.each do |assignment| | ||
next if part_of_example_scope?(assignment.node) |
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.
Cosmetic, optional. Can those be computed once per scope
? Is this a worthwhile optimization? It feels like we’re iterating over vars defined in the same scope, right?
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.
Hmm I tried optimizing it like that, but some examples fail then, like this one:
describe SomeClass do
user = create(:user)
it 'updates the user' do
user = create(:user)
expect { user.update(admin: true) }.to change(user, :updated_at)
end
end
when the scope of describe
is checked. This one fails because part_of_example_scope?
returns false
for that scope (expected, since describe
block doesn't count as example scope) but then it iterates over every assignment inside of describe
block, including the user = create(:user)
one inside it
. Because the next if part_of_example_scope?
line was removed, the offense is then added for references on expect
line.
In summary, it cannot be computed once per scope because after_leaving_scope
may be a non-example scope, but it might iterate over assignments from example scopes if they're nested within it.
77a7e91
to
ddaddea
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.
Thank you!
Adds a new cop to register offenses for local variables assigned outside of examples, but referenced within them. In such cases, local variables are shared state, and they can make tests non-deterministic if they run in random order.
The cop will register offenses for:
The cop does not register offenses when local variable is:
I think this closes #1648. There are some other ideas in there, but I believe this cop solves the major pain point (which is shared state between examples through local variables).
Before submitting the PR make sure the following are checked:
config/default.yml
.Enabled: pending
inconfig/default.yml
.Enabled: true
in.rubocop.yml
.VersionAdded: "<<next>>"
indefault/config.yml
.