mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:45:24 +00:00

## Summary This PR adds support for unpacking tuple expression in an assignment statement where the target expression can be a tuple or a list (the allowed sequence targets). The implementation introduces a new `infer_assignment_target` which can then be used for other targets like the ones in for loops as well. This delegates it to the `infer_definition`. The final implementation uses a recursive function that visits the target expression in source order and compares the variable node that corresponds to the definition. At the same time, it keeps track of where it is on the assignment value type. The logic also accounts for the number of elements on both sides such that it matches even if there's a gap in between. For example, if there's a starred expression like `(a, *b, c) = (1, 2, 3)`, then the type of `a` will be `Literal[1]` and the type of `b` will be `Literal[2]`. There are a couple of follow-ups that can be done: * Use this logic for other target positions like `for` loop * Add diagnostics for mis-match length between LHS and RHS ## Test Plan Add various test cases using the new markdown test framework. Validate that existing test cases pass. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
146 lines
4.6 KiB
Rust
146 lines
4.6 KiB
Rust
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
use red_knot_python_semantic::{HasTy, SemanticModel};
|
|
use red_knot_workspace::db::RootDatabase;
|
|
use red_knot_workspace::workspace::WorkspaceMetadata;
|
|
use ruff_db::files::{system_path_to_file, File};
|
|
use ruff_db::parsed::parsed_module;
|
|
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
|
use ruff_python_ast::visitor::source_order;
|
|
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
|
|
use ruff_python_ast::{self as ast, Alias, Expr, Parameter, ParameterWithDefault, Stmt};
|
|
|
|
fn setup_db(workspace_root: &SystemPath) -> anyhow::Result<RootDatabase> {
|
|
let system = OsSystem::new(workspace_root);
|
|
let workspace = WorkspaceMetadata::from_path(workspace_root, &system, None)?;
|
|
RootDatabase::new(workspace, system)
|
|
}
|
|
|
|
/// Test that all snippets in testcorpus can be checked without panic
|
|
#[test]
|
|
#[allow(clippy::print_stdout)]
|
|
fn corpus_no_panic() -> anyhow::Result<()> {
|
|
let root = SystemPathBuf::from_path_buf(tempfile::TempDir::new()?.into_path()).unwrap();
|
|
let db = setup_db(&root)?;
|
|
|
|
let corpus = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/corpus");
|
|
|
|
for path in fs::read_dir(&corpus)? {
|
|
let source = path?.path();
|
|
println!("checking {source:?}");
|
|
let source_fn = source.file_name().unwrap().to_str().unwrap();
|
|
let py_dest = root.join(source_fn);
|
|
fs::copy(&source, py_dest.as_std_path())?;
|
|
// this test is only asserting that we can pull every expression type without a panic
|
|
// (and some non-expressions that clearly define a single type)
|
|
let file = system_path_to_file(&db, py_dest).unwrap();
|
|
pull_types(&db, file);
|
|
|
|
// try the file as a stub also
|
|
println!("re-checking as .pyi");
|
|
let pyi_dest = root.join(format!("{source_fn}i"));
|
|
std::fs::copy(source, pyi_dest.as_std_path())?;
|
|
let file = system_path_to_file(&db, pyi_dest).unwrap();
|
|
pull_types(&db, file);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn pull_types(db: &RootDatabase, file: File) {
|
|
let mut visitor = PullTypesVisitor::new(db, file);
|
|
|
|
let ast = parsed_module(db, file);
|
|
|
|
visitor.visit_body(ast.suite());
|
|
}
|
|
|
|
struct PullTypesVisitor<'db> {
|
|
model: SemanticModel<'db>,
|
|
}
|
|
|
|
impl<'db> PullTypesVisitor<'db> {
|
|
fn new(db: &'db RootDatabase, file: File) -> Self {
|
|
Self {
|
|
model: SemanticModel::new(db, file),
|
|
}
|
|
}
|
|
|
|
fn visit_assign_target(&mut self, target: &Expr) {
|
|
match target {
|
|
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
|
for element in elts {
|
|
self.visit_assign_target(element);
|
|
}
|
|
}
|
|
_ => self.visit_expr(target),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
|
fn visit_stmt(&mut self, stmt: &Stmt) {
|
|
match stmt {
|
|
Stmt::FunctionDef(function) => {
|
|
let _ty = function.ty(&self.model);
|
|
}
|
|
Stmt::ClassDef(class) => {
|
|
let _ty = class.ty(&self.model);
|
|
}
|
|
Stmt::Assign(assign) => {
|
|
for target in &assign.targets {
|
|
self.visit_assign_target(target);
|
|
}
|
|
return;
|
|
}
|
|
Stmt::AnnAssign(_)
|
|
| Stmt::Return(_)
|
|
| Stmt::Delete(_)
|
|
| Stmt::AugAssign(_)
|
|
| Stmt::TypeAlias(_)
|
|
| Stmt::For(_)
|
|
| Stmt::While(_)
|
|
| Stmt::If(_)
|
|
| Stmt::With(_)
|
|
| Stmt::Match(_)
|
|
| Stmt::Raise(_)
|
|
| Stmt::Try(_)
|
|
| Stmt::Assert(_)
|
|
| Stmt::Import(_)
|
|
| Stmt::ImportFrom(_)
|
|
| Stmt::Global(_)
|
|
| Stmt::Nonlocal(_)
|
|
| Stmt::Expr(_)
|
|
| Stmt::Pass(_)
|
|
| Stmt::Break(_)
|
|
| Stmt::Continue(_)
|
|
| Stmt::IpyEscapeCommand(_) => {}
|
|
}
|
|
|
|
source_order::walk_stmt(self, stmt);
|
|
}
|
|
|
|
fn visit_expr(&mut self, expr: &Expr) {
|
|
let _ty = expr.ty(&self.model);
|
|
|
|
source_order::walk_expr(self, expr);
|
|
}
|
|
|
|
fn visit_parameter(&mut self, parameter: &Parameter) {
|
|
let _ty = parameter.ty(&self.model);
|
|
|
|
source_order::walk_parameter(self, parameter);
|
|
}
|
|
|
|
fn visit_parameter_with_default(&mut self, parameter_with_default: &ParameterWithDefault) {
|
|
let _ty = parameter_with_default.ty(&self.model);
|
|
|
|
source_order::walk_parameter_with_default(self, parameter_with_default);
|
|
}
|
|
|
|
fn visit_alias(&mut self, alias: &Alias) {
|
|
let _ty = alias.ty(&self.model);
|
|
|
|
source_order::walk_alias(self, alias);
|
|
}
|
|
}
|