This commit is contained in:
Dan Parizher 2025-12-16 16:39:34 -05:00 committed by GitHub
commit 3b97ce9526
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 186 additions and 1 deletions

View File

@ -0,0 +1,21 @@
# Module-level mutable "constant"
MY_SET = {"ABC", "DEF"} # plain set
MY_LIST = [1, 2, 3] # plain list
MY_DICT = {"key": "value"} # plain dict
# NOT triggering B006 (correct - PEP 8 specifies that UPPER_CASE variables are considered constants)
def func_A(s: set[str] = MY_SET):
return s
# Triggering B006 (correct)
def func_B(s: set[str] = {"ABC", "DEF"}):
return s
# NOT triggering B006 (correct - PEP 8 specifies that UPPER_CASE variables are considered constants)
def func_C(items: list[int] = MY_LIST):
return items
# NOT triggering B006 (correct - PEP 8 specifies that UPPER_CASE variables are considered constants)
def func_D(data: dict[str, str] = MY_DICT):
return data

View File

@ -0,0 +1,39 @@
# Test nested function reading value from outer scope
def outer_function():
my_list = [1, 2, 3] # Assignment in outer function
# Should trigger B006 - nested function using mutable from outer scope
def inner_function(items=my_list):
return items
return inner_function
# Test that imports don't trigger B006 (assignment restriction)
from some_module import IMPORTED_LIST
# Should NOT trigger B006 - imported names are not assignments
def func_with_import(items=IMPORTED_LIST):
return items
# Test that function parameters don't trigger B006 (assignment restriction)
def func_with_param(param_list):
# Should NOT trigger B006 - function parameters are not assignments
def nested_func(items=param_list):
return items
return nested_func
# Test module-level assignment that should trigger B006
module_list = [1, 2, 3] # Module-level assignment
# Should trigger B006 - module-level assignment used as default
def func_with_module_var(items=module_list):
return items
# Test that non-assignment bindings don't trigger B006
# (This would require a more complex setup to actually test, so we'll focus on the key cases above)

View File

@ -48,6 +48,8 @@ mod tests {
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_7.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_8.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_9.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_10.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_11.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.pyi"))]
#[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))]
@ -93,6 +95,8 @@ mod tests {
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_7.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_8.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_9.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_10.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_11.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.pyi"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@ -7,7 +7,10 @@ use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, ParameterWithDefault};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::function_type::is_stub;
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
use ruff_python_semantic::analyze::typing::{
find_binding_value, is_immutable_annotation, is_mutable_expr,
};
use ruff_python_stdlib::str;
use ruff_python_trivia::{indentation_at_offset, textwrap};
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
@ -142,6 +145,30 @@ fn is_guaranteed_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
elts.iter().any(|e| is_guaranteed_mutable_expr(e, semantic))
}
Expr::Named(ast::ExprNamed { value, .. }) => is_guaranteed_mutable_expr(value, semantic),
Expr::Name(name) => {
// Exclude UPPER_CASE constants (PEP 8 convention - meant to be read-only)
if str::is_cased_uppercase(&name.id) {
return false;
}
// Resolve the name in the current scope (module-level, function-level, etc.)
// This can resolve constants, non-constants, and any name in the current scope
let Some(binding_id) = semantic.only_binding(name) else {
return false;
};
let binding = semantic.binding(binding_id);
// Only check assignments (not imports, function parameters, etc.)
// This restriction ensures we only flag cases where a mutable object is explicitly
// assigned to a name, which is then used as a default argument.
if !binding.kind.is_assignment() {
return false;
}
// Get the assigned value and check if it's mutable
if let Some(value) = find_binding_value(binding, semantic) {
is_guaranteed_mutable_expr(value, semantic)
} else {
false
}
}
_ => is_mutable_expr(expr, semantic),
}
}

View File

@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006 [*] Do not use mutable data structures for argument defaults
--> B006_10.py:11:26
|
10 | # Triggering B006 (correct)
11 | def func_B(s: set[str] = {"ABC", "DEF"}):
| ^^^^^^^^^^^^^^
12 | return s
|
help: Replace with `None`; initialize within function
8 | return s
9 |
10 | # Triggering B006 (correct)
- def func_B(s: set[str] = {"ABC", "DEF"}):
11 + def func_B(s: set[str] = None):
12 + if s is None:
13 + s = {"ABC", "DEF"}
14 | return s
15 |
16 | # NOT triggering B006 (correct - PEP 8 specifies that UPPER_CASE variables are considered constants)
note: This is an unsafe fix and may change runtime behavior

View File

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---

View File

@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006 [*] Do not use mutable data structures for argument defaults
--> B006_10.py:11:26
|
10 | # Triggering B006 (correct)
11 | def func_B(s: set[str] = {"ABC", "DEF"}):
| ^^^^^^^^^^^^^^
12 | return s
|
help: Replace with `None`; initialize within function
8 | return s
9 |
10 | # Triggering B006 (correct)
- def func_B(s: set[str] = {"ABC", "DEF"}):
11 + def func_B(s: set[str] = None):
12 + if s is None:
13 + s = {"ABC", "DEF"}
14 | return s
15 |
16 | # NOT triggering B006 (correct - PEP 8 specifies that UPPER_CASE variables are considered constants)
note: This is an unsafe fix and may change runtime behavior

View File

@ -0,0 +1,44 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B006 [*] Do not use mutable data structures for argument defaults
--> B006_11.py:6:30
|
5 | # Should trigger B006 - nested function using mutable from outer scope
6 | def inner_function(items=my_list):
| ^^^^^^^
7 | return items
|
help: Replace with `None`; initialize within function
3 | my_list = [1, 2, 3] # Assignment in outer function
4 |
5 | # Should trigger B006 - nested function using mutable from outer scope
- def inner_function(items=my_list):
6 + def inner_function(items=None):
7 + if items is None:
8 + items = my_list
9 | return items
10 |
11 | return inner_function
note: This is an unsafe fix and may change runtime behavior
B006 [*] Do not use mutable data structures for argument defaults
--> B006_11.py:33:32
|
32 | # Should trigger B006 - module-level assignment used as default
33 | def func_with_module_var(items=module_list):
| ^^^^^^^^^^^
34 | return items
|
help: Replace with `None`; initialize within function
30 | module_list = [1, 2, 3] # Module-level assignment
31 |
32 | # Should trigger B006 - module-level assignment used as default
- def func_with_module_var(items=module_list):
33 + def func_with_module_var(items=None):
34 + if items is None:
35 + items = module_list
36 | return items
37 |
38 |
note: This is an unsafe fix and may change runtime behavior