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

F401 - Recommend adding unused import bindings to __all__ #11314

Merged
merged 49 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
fe7016e
Revert "patch to remove the unfinished dunder_all mutation"
plredmond May 3, 2024
41584e1
charlie patch w/proto dunder_all finder
plredmond May 3, 2024
61cca6c
find_dunder_all_expr implemented
plredmond May 3, 2024
453c905
pass ExprList to fix_by_reexporting
plredmond May 3, 2024
628053e
slight cleanup of branch arms readability
plredmond May 3, 2024
59d96ae
rename F401_25__all fixture test to F401_25__all_nonempty
plredmond May 4, 2024
5b1f3e5
remove unnecessary pattern match
plredmond May 6, 2024
1d16c8b
new field in ImportBinding which is the name of the binding
plredmond May 6, 2024
ff5f82f
change fix_bind_reexporting call add_to_dunder_all with the list of b…
plredmond May 6, 2024
93f0bc3
uncomment and review test cases for F401_25 fixture test
plredmond May 6, 2024
07bd559
add edit to insert binding names into __all__
plredmond May 6, 2024
4c38950
fixture tests to more thoroughly explore the cases of the "add to __a…
plredmond May 6, 2024
f5370cb
cargo clippy edits -- map..or to map_or and single-pattern match to i…
plredmond May 6, 2024
db7cbb5
include the binding name in UnusedImport; when it is not the suffix o…
plredmond May 6, 2024
1ad23f8
accept fixture test changes that show the binding when it differs fro…
plredmond May 6, 2024
2a72d34
accept stylist argument to place correct quotes around binders added …
plredmond May 7, 2024
494e56d
use Expr instead of ExprList to convey dunder_all value to the edit-p…
plredmond May 7, 2024
a54ea9d
clippy again
plredmond May 7, 2024
99fd1f6
change make_redundant_alias to take an iter of Cow to simplify caller
plredmond May 7, 2024
c8dc342
rewrite add_to_dunder_all and its tests
plredmond May 8, 2024
188a232
shakes fist at cloud and yells, "Clippy!!!"
plredmond May 8, 2024
877800e
change message for redundant-aliases
plredmond May 8, 2024
d5978f9
use binding method instead of accessing the field
plredmond May 8, 2024
86f30e7
use filter_map followed by last to obtain the first __all__ binding
plredmond May 8, 2024
7f092bf
update fixture test to reflect that we add to the first binding of __…
plredmond May 8, 2024
1d0f3ca
fixture test to demonstrate how we treat __all__ followed by a condit…
plredmond May 8, 2024
3e284b6
do not offer a fix to add to __all__ when there is more than one __al…
plredmond May 8, 2024
7f3335d
delete F401_29 fixture test
plredmond May 9, 2024
097ce55
rename F401_30 fixture test to F401_29
plredmond May 9, 2024
5beb13f
add unused-import case to F401_29 fixture test
plredmond May 9, 2024
92a7a43
change `find_dunder_all_expr` to `find_dunder_all_exprs` and track th…
plredmond May 9, 2024
190d191
change find_dunder_all_exprs to return /all/ such exprs regardless of…
plredmond May 9, 2024
6035e1d
change fix-title for redundant alias case to format itself and return…
plredmond May 9, 2024
c81506a
use if-let instead of match with empty branch per alex
plredmond May 13, 2024
bb545a2
Update crates/ruff_linter/src/fix/edits.rs
plredmond May 13, 2024
1cbf320
wording improvement per alex
plredmond May 13, 2024
64c17e2
replace into::Into with Cow::from to make conversion explicit
plredmond May 13, 2024
a747897
improve the wording of "add unused import to __all__" message per alex
plredmond May 13, 2024
edc88f3
reformat a function -- noop w.r.t. the tests
plredmond May 13, 2024
117a1f0
remove a now unnecessary branch, which also makes the overall PR diff…
plredmond May 13, 2024
1b167e6
call .end() directly instead of going through .range or .range(); rem…
plredmond May 14, 2024
26d4714
readd erroneously removed newlines to fixture snaps
plredmond May 14, 2024
5dbdcfe
ruff format the fixture __init__.py files
plredmond May 14, 2024
64fa9af
cargo insta review -- accept whitespace changes (see `git diff -w`)
plredmond May 14, 2024
527d395
clippy fix -- replace closure with static reference to method
plredmond May 14, 2024
548180b
collect only 2 `__all__` definitions and then stop pulling the iterator
plredmond May 14, 2024
01e974f
Revert "collect only 2 `__all__` definitions and then stop pulling th…
plredmond May 14, 2024
0696df5
remove the qualified-name field from UnusedImport in favor of the bin…
plredmond May 14, 2024
dde9756
Revert "remove the qualified-name field from UnusedImport in favor of…
plredmond May 14, 2024
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
@@ -1,4 +1,4 @@
"""__init__.py with __all__
"""__init__.py with nonempty __all__

Unused stdlib and third party imports are unsafe removals

Expand Down Expand Up @@ -33,10 +33,10 @@
from . import exported # Ok: is exported in __all__


# from . import unused # F401: add to __all__
from . import unused # F401: add to __all__


# from . import renamed as bees # F401: add to __all__
from . import renamed as bees # F401: add to __all__


__all__ = ["argparse", "exported"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""__init__.py with empty __all__
"""


from . import unused # F401: add to __all__


from . import renamed as bees # F401: add to __all__


__all__ = []
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""__init__.py with mis-typed __all__
"""


from . import unused # F401: recommend add to all w/o fix


from . import renamed as bees # F401: recommend add to all w/o fix


__all__ = None
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""__init__.py with multiple imports added to all in one edit
"""


from . import unused, renamed as bees # F401: add to __all__


__all__ = [];
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""__init__.py with __all__ populated by conditional plus-eq

multiple __all__ so cannot offer a fix to add to them
"""

import sys

from . import unused, exported, renamed as bees

if sys.version_info > (3, 9):
from . import also_exported

__all__ = ["exported"]

if sys.version_info >= (3, 9):
__all__ += ["also_exported"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# empty module imported by __init__.py for test fixture
116 changes: 107 additions & 9 deletions crates/ruff_linter/src/fix/edits.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
//! Interface for generating fix edits from higher-level actions (e.g., "remove an argument").

use std::borrow::Cow;

use anyhow::{Context, Result};

use ruff_diagnostics::Edit;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, ExprList, Stmt};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
Expand Down Expand Up @@ -124,7 +126,7 @@ pub(crate) fn remove_unused_imports<'a>(

/// Edits to make the specified imports explicit, e.g. change `import x` to `import x as x`.
pub(crate) fn make_redundant_alias<'a>(
member_names: impl Iterator<Item = &'a str>,
member_names: impl Iterator<Item = Cow<'a, str>>,
stmt: &Stmt,
) -> Vec<Edit> {
let aliases = match stmt {
Expand All @@ -144,6 +146,53 @@ pub(crate) fn make_redundant_alias<'a>(
.collect()
}

/// Fix to add the specified imports to the `__all__` export list.
pub(crate) fn add_to_dunder_all<'a>(
names: impl Iterator<Item = &'a str>,
expr: &Expr,
stylist: &Stylist,
) -> Vec<Edit> {
let (insertion_point, export_prefix_length) = match expr {
Expr::List(ExprList { elts, range, .. }) => (
elts.last()
.map_or(range.end() - "]".text_len(), Ranged::end),
elts.len(),
),
Expr::Tuple(tup) if tup.parenthesized => (
tup.elts
.last()
.map_or(tup.end() - ")".text_len(), Ranged::end),
tup.elts.len(),
),
Expr::Tuple(tup) if !tup.parenthesized => (
tup.elts
.last()
.expect("unparenthesized empty tuple is not possible")
.range()
.end(),
tup.elts.len(),
),
_ => {
// we don't know how to insert into this expression
return vec![];
}
};
let quote = stylist.quote();
let mut edits: Vec<_> = names
.enumerate()
.map(|(offset, name)| match export_prefix_length + offset {
0 => Edit::insertion(format!("{quote}{name}{quote}"), insertion_point),
_ => Edit::insertion(format!(", {quote}{name}{quote}"), insertion_point),
})
.collect();
if let Expr::Tuple(tup) = expr {
if tup.parenthesized && export_prefix_length + edits.len() == 1 {
edits.push(Edit::insertion(",".to_string(), insertion_point));
}
}
edits
}

#[derive(Debug, Copy, Clone)]
pub(crate) enum Parentheses {
/// Remove parentheses, if the removed argument is the only argument left.
Expand Down Expand Up @@ -477,14 +526,20 @@ fn all_lines_fit(

#[cfg(test)]
mod tests {
use anyhow::Result;
use anyhow::{anyhow, Result};
use std::borrow::Cow;
use test_case::test_case;

use ruff_diagnostics::Edit;
use ruff_python_parser::parse_suite;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_codegen::Stylist;
use ruff_python_parser::{lexer, parse_expression, parse_suite, Mode};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};

use crate::fix::edits::{make_redundant_alias, next_stmt_break, trailing_semicolon};
use crate::fix::apply_fixes;
use crate::fix::edits::{
add_to_dunder_all, make_redundant_alias, next_stmt_break, trailing_semicolon,
};

#[test]
fn find_semicolon() -> Result<()> {
Expand Down Expand Up @@ -562,28 +617,71 @@ x = 1 \
let program = parse_suite(contents).unwrap();
let stmt = program.first().unwrap();
assert_eq!(
make_redundant_alias(["x"].into_iter(), stmt),
make_redundant_alias(["x"].into_iter().map(Cow::from), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
)],
"make just one item redundant"
);
assert_eq!(
make_redundant_alias(vec!["x", "y"].into_iter(), stmt),
make_redundant_alias(vec!["x", "y"].into_iter().map(Cow::from), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
)],
"the second item is already a redundant alias"
);
assert_eq!(
make_redundant_alias(vec!["x", "z"].into_iter(), stmt),
make_redundant_alias(vec!["x", "z"].into_iter().map(Cow::from), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
)],
"the third item is already aliased to something else"
);
}

#[test_case("()", &["x", "y"], r#"("x", "y")"# ; "2 into empty tuple")]
#[test_case("()", &["x"], r#"("x",)"# ; "1 into empty tuple adding a trailing comma")]
#[test_case("[]", &["x", "y"], r#"["x", "y"]"# ; "2 into empty list")]
#[test_case("[]", &["x"], r#"["x"]"# ; "1 into empty list")]
#[test_case(r#""a", "b""#, &["x", "y"], r#""a", "b", "x", "y""# ; "2 into unparenthesized tuple")]
#[test_case(r#""a", "b""#, &["x"], r#""a", "b", "x""# ; "1 into unparenthesized tuple")]
#[test_case(r#""a", "b","#, &["x", "y"], r#""a", "b", "x", "y","# ; "2 into unparenthesized tuple w/trailing comma")]
#[test_case(r#""a", "b","#, &["x"], r#""a", "b", "x","# ; "1 into unparenthesized tuple w/trailing comma")]
#[test_case(r#"("a", "b")"#, &["x", "y"], r#"("a", "b", "x", "y")"# ; "2 into nonempty tuple")]
#[test_case(r#"("a", "b")"#, &["x"], r#"("a", "b", "x")"# ; "1 into nonempty tuple")]
#[test_case(r#"("a", "b",)"#, &["x", "y"], r#"("a", "b", "x", "y",)"# ; "2 into nonempty tuple w/trailing comma")]
#[test_case(r#"("a", "b",)"#, &["x"], r#"("a", "b", "x",)"# ; "1 into nonempty tuple w/trailing comma")]
#[test_case(r#"["a", "b",]"#, &["x", "y"], r#"["a", "b", "x", "y",]"# ; "2 into nonempty list w/trailing comma")]
#[test_case(r#"["a", "b",]"#, &["x"], r#"["a", "b", "x",]"# ; "1 into nonempty list w/trailing comma")]
#[test_case(r#"["a", "b"]"#, &["x", "y"], r#"["a", "b", "x", "y"]"# ; "2 into nonempty list")]
#[test_case(r#"["a", "b"]"#, &["x"], r#"["a", "b", "x"]"# ; "1 into nonempty list")]
fn add_to_dunder_all_test(raw: &str, names: &[&str], expect: &str) -> Result<()> {
let locator = Locator::new(raw);
let edits = {
let expr = parse_expression(raw)?;
let stylist = Stylist::from_tokens(
&lexer::lex(raw, Mode::Expression).collect::<Vec<_>>(),
&locator,
);
// SUT
Copy link
Member

Choose a reason for hiding this comment

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

What does SUT mean?

Copy link
Contributor Author

@plredmond plredmond May 14, 2024

Choose a reason for hiding this comment

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

system under test -- the test is long so i'm marking the bit that's unit tested

Copy link
Member

Choose a reason for hiding this comment

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

This acronym was also unfamiliar to me FWIW

add_to_dunder_all(names.iter().copied(), &expr, &stylist)
};
let diag = {
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
let mut iter = edits.into_iter();
Diagnostic::new(
MissingNewlineAtEndOfFile, // The choice of rule here is arbitrary.
TextRange::default(),
)
.with_fix(Fix::safe_edits(
iter.next().ok_or(anyhow!("expected edits nonempty"))?,
iter,
))
};
assert_eq!(apply_fixes([diag].iter(), &locator).code, expect);
Ok(())
}
}
6 changes: 5 additions & 1 deletion crates/ruff_linter/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ mod tests {
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
#[test_case(Rule::UnusedImport, Path::new("__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_25__all/__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_25__all_nonempty/__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_26__all_empty/__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_27__all_mistyped/__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_28__all_multiple/__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_29__all_conditional/__init__.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
Expand Down
Loading
Loading