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

[pylint] Implement redefined-argument-from-local (R1704) #8159

Merged
merged 37 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
58c3c2e
adding rule
danbi2990 Oct 10, 2023
d58e38d
Merge branch 'main' of https://github.com/astral-sh/ruff into redefin…
danbi2990 Oct 13, 2023
969b3fa
Merge branch 'main' of https://github.com/astral-sh/ruff into redefin…
danbi2990 Oct 13, 2023
d6054c5
add code
danbi2990 Oct 24, 2023
0f75bb7
check with stmt
danbi2990 Oct 24, 2023
67dcefd
check for stmt
danbi2990 Oct 24, 2023
c660f25
check try stmt
danbi2990 Oct 24, 2023
f3d15cb
add python test
danbi2990 Oct 24, 2023
c28b437
impl rule
danbi2990 Oct 24, 2023
a6dd737
Merge branch 'main' of https://github.com/astral-sh/ruff into redefin…
danbi2990 Oct 24, 2023
6dc8afc
add rule
danbi2990 Oct 24, 2023
630bfaa
add test
danbi2990 Oct 24, 2023
e5dc044
add no error cases
danbi2990 Oct 24, 2023
8dd88a3
update schema
danbi2990 Oct 24, 2023
9c321e0
add snapshot
danbi2990 Oct 24, 2023
d676b9a
fix typo
danbi2990 Oct 24, 2023
a85a6c5
apply format
danbi2990 Oct 24, 2023
85166ca
check equality by if
danbi2990 Oct 24, 2023
0e6f48a
early continue
danbi2990 Oct 24, 2023
ab14d52
add WithItemVar
danbi2990 Nov 9, 2023
99bff35
add test cases
danbi2990 Nov 9, 2023
efb7ce5
remove legacy impl
danbi2990 Nov 9, 2023
b6dd3ad
add binding with stmt
danbi2990 Nov 10, 2023
a290569
add rule
danbi2990 Nov 10, 2023
ebf0aaa
remove WithItemVar
danbi2990 Nov 10, 2023
818980b
Revert "add binding with stmt"
danbi2990 Nov 10, 2023
6426c0e
Revert "add WithItemVar"
danbi2990 Nov 10, 2023
724d882
update snapshot
danbi2990 Nov 10, 2023
ab95194
format code
danbi2990 Nov 10, 2023
b29547b
Merge branch 'main' of https://github.com/astral-sh/ruff into redefin…
danbi2990 Nov 10, 2023
9d9c502
Merge branch 'main' of https://github.com/astral-sh/ruff into redefin…
danbi2990 Nov 10, 2023
d5bb19d
edit test cases
danbi2990 Nov 10, 2023
0698990
check WithItemVar
danbi2990 Nov 10, 2023
c7eca3e
check binding.is_used
danbi2990 Nov 10, 2023
8c8b0ac
update snapshot
danbi2990 Nov 10, 2023
82a7563
format code
danbi2990 Nov 10, 2023
c5faa27
Check even if unused
charliermarsh Nov 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

# No Errors

def foo(a):
for b in range(1):
...

def foo(a):
try:
...
except Exception as e:
...

def foo(a):
with open('', ) as f:
...

if True:
def foo(a):
...
else:
for a in range(1):
...

# Errors

def foo(i):
for i in range(10):
...

def foo(e):
try:
...
except Exception as e:
...

def foo(f):
with open('', ) as f:
...

def foo(a, b):
with context() as (a, b, c):
...

def foo(a, b):
with context() as [a, b, c]:
...

def foo(a):
def bar(b):
for a in range(1):
...

def foo(a):
def bar(b):
for b in range(1):
...

def foo(a=1):
def bar(b=2):
for a in range(1):
...
for b in range(1):
...
25 changes: 25 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if !checker.any_enabled(&[
Rule::GlobalVariableNotAssigned,
Rule::ImportShadowedByLoopVar,
Rule::RedefinedArgumentFromLocal,
Rule::RedefinedWhileUnused,
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
Expand Down Expand Up @@ -89,6 +90,30 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
}

if checker.enabled(Rule::RedefinedArgumentFromLocal) {
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
let binding = &checker.semantic.bindings[shadow.binding_id()];
if !matches!(
binding.kind,
BindingKind::LoopVar | BindingKind::BoundException
) {
continue;
}
let shadowed = &checker.semantic.bindings[shadow.shadowed_id()];
if !shadowed.kind.is_argument() {
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this supposed to check if the argument is unused? Or does it fire in either case?

Copy link
Contributor Author

@danbi2990 danbi2990 Nov 10, 2023

Choose a reason for hiding this comment

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

It's not checking the usage.

It looks sensible to check binding.is_used() as below

if !shadowed.kind.is_argument() || !binding.is_used() {
    continue;
}

In this case, results are following

def foo(i):
    for i in range(10):  # emit error
        print(i)
        ...

def foo(i):
    for i in range(10):  # no error because it's unused
        ...

Is it good to go?

Copy link
Member

Choose a reason for hiding this comment

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

I was referring to the inverse -- should this depend on the argument being unused? I'll check Pylint.

Copy link
Member

Choose a reason for hiding this comment

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

I erred on the side of removing the !binding.is_used() piece here. I think it still makes sense to raise, even if the new binding is unused, since it will cause the same problems?

checker.diagnostics.push(Diagnostic::new(
pylint::rules::RedefinedArgumentFromLocal {
name: name.to_string(),
},
binding.range(),
));
}
}
}

if checker.enabled(Rule::ImportShadowedByLoopVar) {
for (name, binding_id) in scope.bindings() {
for shadow in checker.semantic.shadowed_bindings(scope_id, binding_id) {
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
(Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1706") => (RuleGroup::Preview, rules::pylint::rules::AndOrTernary),
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/rules/pylint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ mod tests {
)]
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"))]
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"))]
#[test_case(
Rule::RedefinedArgumentFromLocal,
Path::new("redefined_argument_from_local.py")
)]
#[test_case(Rule::RedefinedLoopName, Path::new("redefined_loop_name.py"))]
#[test_case(Rule::ReturnInInit, Path::new("return_in_init.py"))]
#[test_case(Rule::TooManyArguments, Path::new("too_many_arguments.py"))]
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pylint/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub(crate) use non_ascii_module_import::*;
pub(crate) use non_ascii_name::*;
pub(crate) use nonlocal_without_binding::*;
pub(crate) use property_with_parameters::*;
pub(crate) use redefined_argument_from_local::*;
pub(crate) use redefined_loop_name::*;
pub(crate) use repeated_equality_comparison::*;
pub(crate) use repeated_isinstance_calls::*;
Expand Down Expand Up @@ -111,6 +112,7 @@ mod non_ascii_module_import;
mod non_ascii_name;
mod nonlocal_without_binding;
mod property_with_parameters;
mod redefined_argument_from_local;
mod redefined_loop_name;
mod repeated_equality_comparison;
mod repeated_isinstance_calls;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};

/// ## What it does
/// Checks for variables defined in `for`, `try`, `with` statements
/// that redefine function parameters.
///
/// ## Why is this bad?
/// Redefined variable can cause unexpected behavior because of overridden function parameter.
/// If nested functions are declared, inner function's body can override outer function's parameter.
///
/// ## Example
/// ```python
/// def show(host_id=10.11):
/// for host_id, host in [[12.13, "Venus"], [14.15, "Mars"]]:
/// print(host_id, host)
/// ```
///
/// Use instead:
/// ```python
/// def show(host_id=10.11):
/// for inner_host_id, host in [[12.13, "Venus"], [14.15, "Mars"]]:
/// print(host_id, inner_host_id, host)
/// ```
/// ## References
/// - [Pylint documentation](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/redefined-argument-from-local.html)

#[violation]
pub struct RedefinedArgumentFromLocal {
pub(crate) name: String,
}

impl Violation for RedefinedArgumentFromLocal {
#[derive_message_formats]
fn message(&self) -> String {
let RedefinedArgumentFromLocal { name } = self;
format!("Redefining argument with the local name `{name}`")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
redefined_argument_from_local.py:28:9: PLR1704 Redefining argument with the local name `i`
|
27 | def foo(i):
28 | for i in range(10):
| ^ PLR1704
29 | ...
|

redefined_argument_from_local.py:34:25: PLR1704 Redefining argument with the local name `e`
|
32 | try:
33 | ...
34 | except Exception as e:
| ^ PLR1704
35 | ...
|

redefined_argument_from_local.py:51:13: PLR1704 Redefining argument with the local name `a`
|
49 | def foo(a):
50 | def bar(b):
51 | for a in range(1):
| ^ PLR1704
52 | ...
|

redefined_argument_from_local.py:56:13: PLR1704 Redefining argument with the local name `b`
|
54 | def foo(a):
55 | def bar(b):
56 | for b in range(1):
| ^ PLR1704
57 | ...
|

redefined_argument_from_local.py:61:13: PLR1704 Redefining argument with the local name `a`
|
59 | def foo(a=1):
60 | def bar(b=2):
61 | for a in range(1):
| ^ PLR1704
62 | ...
63 | for b in range(1):
|

redefined_argument_from_local.py:63:13: PLR1704 Redefining argument with the local name `b`
|
61 | for a in range(1):
62 | ...
63 | for b in range(1):
| ^ PLR1704
64 | ...
|


1 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading