From 6276b054735fbb1552fb2322ddb9b06bfca47374 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 26 Jul 2024 12:16:19 -0400 Subject: [PATCH] Move --- Cargo.lock | 5 + crates/ruff_linter/src/importer/mod.rs | 21 +- .../rules/future_required_type_annotation.rs | 7 +- crates/ruff_linter/src/rules/isort/mod.rs | 60 ++--- .../rules/isort/rules/add_required_imports.rs | 90 ++----- .../ruff_linter/src/rules/isort/settings.rs | 6 +- ...ombined_required_imports_docstring.py.snap | 16 -- ...mbined_required_imports_docstring.pyi.snap | 4 - ...ed_required_imports_docstring_only.py.snap | 4 - ...s__combined_required_imports_empty.py.snap | 4 - crates/ruff_python_ast/src/imports.rs | 114 --------- crates/ruff_python_ast/src/lib.rs | 1 - crates/ruff_python_semantic/Cargo.toml | 8 + crates/ruff_python_semantic/src/imports.rs | 235 ++++++++++++++++++ crates/ruff_python_semantic/src/lib.rs | 2 + crates/ruff_workspace/Cargo.toml | 3 +- crates/ruff_workspace/src/options.rs | 21 +- ruff.schema.json | 5 +- 18 files changed, 330 insertions(+), 276 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring.py.snap delete mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring.pyi.snap delete mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring_only.py.snap delete mode 100644 crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_empty.py.snap delete mode 100644 crates/ruff_python_ast/src/imports.rs create mode 100644 crates/ruff_python_semantic/src/imports.rs diff --git a/Cargo.lock b/Cargo.lock index fc87ccc9b87e5..6cb2de709a825 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2400,13 +2400,17 @@ version = "0.0.0" dependencies = [ "bitflags 2.6.0", "is-macro", + "ruff_cache", "ruff_index", + "ruff_macros", "ruff_python_ast", "ruff_python_parser", "ruff_python_stdlib", "ruff_source_file", "ruff_text_size", "rustc-hash 2.0.0", + "schemars", + "serde", ] [[package]] @@ -2539,6 +2543,7 @@ dependencies = [ "ruff_macros", "ruff_python_ast", "ruff_python_formatter", + "ruff_python_semantic", "ruff_source_file", "rustc-hash 2.0.0", "schemars", diff --git a/crates/ruff_linter/src/importer/mod.rs b/crates/ruff_linter/src/importer/mod.rs index b4bb20a5dbf16..bba1b155bdd28 100644 --- a/crates/ruff_linter/src/importer/mod.rs +++ b/crates/ruff_linter/src/importer/mod.rs @@ -9,11 +9,12 @@ use anyhow::Result; use libcst_native::{ImportAlias, Name as cstName, NameOrAttribute}; use ruff_diagnostics::Edit; -use ruff_python_ast::imports::{AnyImport, Import, ImportFrom}; use ruff_python_ast::{self as ast, ModModule, Stmt}; use ruff_python_codegen::Stylist; use ruff_python_parser::{Parsed, Tokens}; -use ruff_python_semantic::{ImportedName, SemanticModel}; +use ruff_python_semantic::{ + ImportedName, MemberNameImport, ModuleNameImport, NameImport, SemanticModel, +}; use ruff_python_trivia::textwrap::indent; use ruff_source_file::Locator; use ruff_text_size::{Ranged, TextSize}; @@ -71,7 +72,7 @@ impl<'a> Importer<'a> { /// If there are no existing imports, the new import will be added at the top /// of the file. Otherwise, it will be added after the most recent top-level /// import statement. - pub(crate) fn add_import(&self, import: &AnyImport, at: TextSize) -> Edit { + pub(crate) fn add_import(&self, import: &NameImport, at: TextSize) -> Edit { let required_import = import.to_string(); if let Some(stmt) = self.preceding_import(at) { // Insert after the last top-level import. @@ -359,8 +360,12 @@ impl<'a> Importer<'a> { // Case 2a: No `functools` import is in scope; thus, we add `import functools`, // and return `"functools.cache"` as the bound name. if semantic.is_available(symbol.module) { - let import_edit = - self.add_import(&AnyImport::Import(Import::module(symbol.module)), at); + let import_edit = self.add_import( + &NameImport::Import(ModuleNameImport::module( + symbol.module.to_string(), + )), + at, + ); Ok(( import_edit, format!( @@ -378,9 +383,9 @@ impl<'a> Importer<'a> { // `from functools import cache`, and return `"cache"` as the bound name. if semantic.is_available(symbol.member) { let import_edit = self.add_import( - &AnyImport::ImportFrom(ImportFrom::member( - symbol.module, - symbol.member, + &NameImport::ImportFrom(MemberNameImport::member( + symbol.module.to_string(), + symbol.member.to_string(), )), at, ); diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs index 11643f5a9fe59..8895b374a0f37 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/rules/future_required_type_annotation.rs @@ -2,8 +2,8 @@ use std::fmt; use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::imports::{AnyImport, ImportFrom}; use ruff_python_ast::Expr; +use ruff_python_semantic::{MemberNameImport, NameImport}; use ruff_text_size::{Ranged, TextSize}; use crate::checkers::ast::Checker; @@ -86,7 +86,10 @@ impl AlwaysFixableViolation for FutureRequiredTypeAnnotation { /// FA102 pub(crate) fn future_required_type_annotation(checker: &mut Checker, expr: &Expr, reason: Reason) { let mut diagnostic = Diagnostic::new(FutureRequiredTypeAnnotation { reason }, expr.range()); - let required_import = AnyImport::ImportFrom(ImportFrom::member("__future__", "annotations")); + let required_import = NameImport::ImportFrom(MemberNameImport::member( + "__future__".to_string(), + "annotations".to_string(), + )); diagnostic.set_fix(Fix::unsafe_edit( checker .importer() diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 71be8f1b7703b..e10913435f763 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -282,11 +282,11 @@ mod tests { use std::path::Path; use anyhow::Result; + use ruff_python_semantic::{MemberNameImport, ModuleNameImport, NameImport}; + use ruff_text_size::Ranged; use rustc_hash::{FxHashMap, FxHashSet}; use test_case::test_case; - use ruff_text_size::Ranged; - use crate::assert_messages; use crate::registry::Rule; use crate::rules::isort::categorize::{ImportSection, KnownModules}; @@ -804,9 +804,12 @@ mod tests { &LinterSettings { src: vec![test_resource_path("fixtures/isort")], isort: super::settings::Settings { - required_imports: BTreeSet::from_iter([ - "from __future__ import annotations".to_string() - ]), + required_imports: BTreeSet::from_iter([NameImport::ImportFrom( + MemberNameImport::member( + "__future__".to_string(), + "annotations".to_string(), + ), + )]), ..super::settings::Settings::default() }, ..LinterSettings::for_rule(Rule::MissingRequiredImport) @@ -834,9 +837,13 @@ mod tests { &LinterSettings { src: vec![test_resource_path("fixtures/isort")], isort: super::settings::Settings { - required_imports: BTreeSet::from_iter([ - "from __future__ import annotations as _annotations".to_string(), - ]), + required_imports: BTreeSet::from_iter([NameImport::ImportFrom( + MemberNameImport::alias( + "__future__".to_string(), + "annotations".to_string(), + "_annotations".to_string(), + ), + )]), ..super::settings::Settings::default() }, ..LinterSettings::for_rule(Rule::MissingRequiredImport) @@ -858,8 +865,14 @@ mod tests { src: vec![test_resource_path("fixtures/isort")], isort: super::settings::Settings { required_imports: BTreeSet::from_iter([ - "from __future__ import annotations".to_string(), - "from __future__ import generator_stop".to_string(), + NameImport::ImportFrom(MemberNameImport::member( + "__future__".to_string(), + "annotations".to_string(), + )), + NameImport::ImportFrom(MemberNameImport::member( + "__future__".to_string(), + "generator_stop".to_string(), + )), ]), ..super::settings::Settings::default() }, @@ -870,29 +883,6 @@ mod tests { Ok(()) } - #[test_case(Path::new("docstring.py"))] - #[test_case(Path::new("docstring.pyi"))] - #[test_case(Path::new("docstring_only.py"))] - #[test_case(Path::new("empty.py"))] - fn combined_required_imports(path: &Path) -> Result<()> { - let snapshot = format!("combined_required_imports_{}", path.to_string_lossy()); - let diagnostics = test_path( - Path::new("isort/required_imports").join(path).as_path(), - &LinterSettings { - src: vec![test_resource_path("fixtures/isort")], - isort: super::settings::Settings { - required_imports: BTreeSet::from_iter(["from __future__ import annotations, \ - generator_stop" - .to_string()]), - ..super::settings::Settings::default() - }, - ..LinterSettings::for_rule(Rule::MissingRequiredImport) - }, - )?; - assert_messages!(snapshot, diagnostics); - Ok(()) - } - #[test_case(Path::new("docstring.py"))] #[test_case(Path::new("docstring.pyi"))] #[test_case(Path::new("docstring_only.py"))] @@ -904,7 +894,9 @@ mod tests { &LinterSettings { src: vec![test_resource_path("fixtures/isort")], isort: super::settings::Settings { - required_imports: BTreeSet::from_iter(["import os".to_string()]), + required_imports: BTreeSet::from_iter([NameImport::Import( + ModuleNameImport::module("os".to_string()), + )]), ..super::settings::Settings::default() }, ..LinterSettings::for_rule(Rule::MissingRequiredImport) diff --git a/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs b/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs index 87265c9cd28d1..83b40b72d87f4 100644 --- a/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff_linter/src/rules/isort/rules/add_required_imports.rs @@ -1,12 +1,10 @@ -use log::error; - use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_docstring_stmt; -use ruff_python_ast::imports::{Alias, AnyImport, FutureImport, Import, ImportFrom}; use ruff_python_ast::{self as ast, ModModule, PySourceType, Stmt}; use ruff_python_codegen::Stylist; -use ruff_python_parser::{parse_module, Parsed}; +use ruff_python_parser::Parsed; +use ruff_python_semantic::{FutureImport, NameImport}; use ruff_source_file::Locator; use ruff_text_size::{TextRange, TextSize}; @@ -53,18 +51,19 @@ impl AlwaysFixableViolation for MissingRequiredImport { } } -/// Return `true` if the [`Stmt`] includes the given [`AnyImport`]. -fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { +/// Return `true` if the [`Stmt`] includes the given [`AnyImportRef`]. +fn includes_import(stmt: &Stmt, target: &NameImport) -> bool { match target { - AnyImport::Import(target) => { + NameImport::Import(target) => { let Stmt::Import(ast::StmtImport { names, range: _ }) = &stmt else { return false; }; names.iter().any(|alias| { - &alias.name == target.name.name && alias.asname.as_deref() == target.name.as_name + alias.name == target.name.name + && alias.asname.as_deref() == target.name.as_name.as_deref() }) } - AnyImport::ImportFrom(target) => { + NameImport::ImportFrom(target) => { let Stmt::ImportFrom(ast::StmtImportFrom { module, names, @@ -74,11 +73,11 @@ fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { else { return false; }; - module.as_deref() == target.module + module.as_deref() == target.module.as_deref() && *level == target.level && names.iter().any(|alias| { - &alias.name == target.name.name - && alias.asname.as_deref() == target.name.as_name + alias.name == target.name.name + && alias.asname.as_deref() == target.name.as_name.as_deref() }) } } @@ -86,7 +85,7 @@ fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { #[allow(clippy::too_many_arguments)] fn add_required_import( - required_import: &AnyImport, + required_import: &NameImport, parsed: &Parsed, locator: &Locator, stylist: &Stylist, @@ -134,69 +133,8 @@ pub(crate) fn add_required_imports( .isort .required_imports .iter() - .flat_map(|required_import| { - let Ok(body) = parse_module(required_import).map(Parsed::into_suite) else { - error!("Failed to parse required import: `{}`", required_import); - return vec![]; - }; - if body.is_empty() || body.len() > 1 { - error!( - "Expected require import to contain a single statement: `{}`", - required_import - ); - return vec![]; - } - let stmt = &body[0]; - match stmt { - Stmt::ImportFrom(ast::StmtImportFrom { - module, - names, - level, - range: _, - }) => names - .iter() - .filter_map(|name| { - add_required_import( - &AnyImport::ImportFrom(ImportFrom { - module: module.as_deref(), - name: Alias { - name: name.name.as_str(), - as_name: name.asname.as_deref(), - }, - level: *level, - }), - parsed, - locator, - stylist, - source_type, - ) - }) - .collect(), - Stmt::Import(ast::StmtImport { names, range: _ }) => names - .iter() - .filter_map(|name| { - add_required_import( - &AnyImport::Import(Import { - name: Alias { - name: name.name.as_str(), - as_name: name.asname.as_deref(), - }, - }), - parsed, - locator, - stylist, - source_type, - ) - }) - .collect(), - _ => { - error!( - "Expected required import to be in import-from style: `{}`", - required_import - ); - vec![] - } - } + .filter_map(|required_import| { + add_required_import(required_import, parsed, locator, stylist, source_type) }) .collect() } diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index 7307b6664a08d..7324be1f8da66 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -9,11 +9,11 @@ use rustc_hash::FxHashSet; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use ruff_macros::CacheKey; - use crate::display_settings; use crate::rules::isort::categorize::KnownModules; use crate::rules::isort::ImportType; +use ruff_macros::CacheKey; +use ruff_python_semantic::NameImport; use super::categorize::ImportSection; @@ -47,7 +47,7 @@ impl Display for RelativeImportsOrder { #[derive(Debug, Clone, CacheKey)] #[allow(clippy::struct_excessive_bools)] pub struct Settings { - pub required_imports: BTreeSet, + pub required_imports: BTreeSet, pub combine_as_imports: bool, pub force_single_line: bool, pub force_sort_within_sections: bool, diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring.py.snap deleted file mode 100644 index d65d89b7038d5..0000000000000 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring.py.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/isort/mod.rs ---- -docstring.py:1:1: I002 [*] Missing required import: `from __future__ import annotations` -ℹ Safe fix -1 1 | """Hello, world!""" - 2 |+from __future__ import annotations -2 3 | -3 4 | x = 1 - -docstring.py:1:1: I002 [*] Missing required import: `from __future__ import generator_stop` -ℹ Safe fix -1 1 | """Hello, world!""" - 2 |+from __future__ import generator_stop -2 3 | -3 4 | x = 1 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring.pyi.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring.pyi.snap deleted file mode 100644 index ed369f0fd61f0..0000000000000 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring.pyi.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/isort/mod.rs ---- - diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring_only.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring_only.py.snap deleted file mode 100644 index ed369f0fd61f0..0000000000000 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_docstring_only.py.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/isort/mod.rs ---- - diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_empty.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_empty.py.snap deleted file mode 100644 index ed369f0fd61f0..0000000000000 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__combined_required_imports_empty.py.snap +++ /dev/null @@ -1,4 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/isort/mod.rs ---- - diff --git a/crates/ruff_python_ast/src/imports.rs b/crates/ruff_python_ast/src/imports.rs deleted file mode 100644 index 838819d357e4e..0000000000000 --- a/crates/ruff_python_ast/src/imports.rs +++ /dev/null @@ -1,114 +0,0 @@ -/// A representation of an individual name imported via any import statement. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AnyImport<'a> { - Import(Import<'a>), - ImportFrom(ImportFrom<'a>), -} - -/// A representation of an individual name imported via an `import` statement. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Import<'a> { - pub name: Alias<'a>, -} - -/// A representation of an individual name imported via a `from ... import` statement. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ImportFrom<'a> { - pub module: Option<&'a str>, - pub name: Alias<'a>, - pub level: u32, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Alias<'a> { - pub name: &'a str, - pub as_name: Option<&'a str>, -} - -impl<'a> Import<'a> { - /// Creates a new `Import` to import the specified module. - pub fn module(name: &'a str) -> Self { - Self { - name: Alias { - name, - as_name: None, - }, - } - } -} - -impl<'a> ImportFrom<'a> { - /// Creates a new `ImportFrom` to import a member from the specified module. - pub fn member(module: &'a str, name: &'a str) -> Self { - Self { - module: Some(module), - name: Alias { - name, - as_name: None, - }, - level: 0, - } - } -} - -impl std::fmt::Display for AnyImport<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - AnyImport::Import(import) => write!(f, "{import}"), - AnyImport::ImportFrom(import_from) => write!(f, "{import_from}"), - } - } -} - -impl std::fmt::Display for Import<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "import {}", self.name.name)?; - if let Some(as_name) = self.name.as_name { - write!(f, " as {as_name}")?; - } - Ok(()) - } -} - -impl std::fmt::Display for ImportFrom<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "from ")?; - if self.level > 0 { - write!(f, "{}", ".".repeat(self.level as usize))?; - } - if let Some(module) = self.module { - write!(f, "{module}")?; - } - write!(f, " import {}", self.name.name)?; - if let Some(as_name) = self.name.as_name { - write!(f, " as {as_name}")?; - } - Ok(()) - } -} - -pub trait FutureImport { - /// Returns `true` if this import is from the `__future__` module. - fn is_future_import(&self) -> bool; -} - -impl FutureImport for Import<'_> { - fn is_future_import(&self) -> bool { - self.name.name == "__future__" - } -} - -impl FutureImport for ImportFrom<'_> { - fn is_future_import(&self) -> bool { - self.module == Some("__future__") - } -} - -impl FutureImport for AnyImport<'_> { - fn is_future_import(&self) -> bool { - match self { - AnyImport::Import(import) => import.is_future_import(), - AnyImport::ImportFrom(import_from) => import_from.is_future_import(), - } - } -} diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index 205c7b98c7754..48a9afeb5730f 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -12,7 +12,6 @@ mod expression; pub mod hashable; pub mod helpers; pub mod identifier; -pub mod imports; mod int; pub mod name; mod node; diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index fb087c02aeab5..86de14b27141b 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -11,8 +11,11 @@ repository = { workspace = true } license = { workspace = true } [dependencies] +ruff_cache = { workspace = true } ruff_index = { workspace = true } +ruff_macros = { workspace = true } ruff_python_ast = { workspace = true } +ruff_python_parser = { workspace = true } ruff_python_stdlib = { workspace = true } ruff_source_file = { workspace = true } ruff_text_size = { workspace = true } @@ -20,6 +23,8 @@ ruff_text_size = { workspace = true } bitflags = { workspace = true } is-macro = { workspace = true } rustc-hash = { workspace = true } +schemars = { workspace = true, optional = true } +serde = { workspace = true, optional = true } [dev-dependencies] ruff_python_parser = { workspace = true } @@ -27,3 +32,6 @@ ruff_python_parser = { workspace = true } [lints] workspace = true +[package.metadata.cargo-shear] +# Used via `CacheKey` macro expansion. +ignored = ["ruff_cache"] diff --git a/crates/ruff_python_semantic/src/imports.rs b/crates/ruff_python_semantic/src/imports.rs new file mode 100644 index 0000000000000..b7811910f1354 --- /dev/null +++ b/crates/ruff_python_semantic/src/imports.rs @@ -0,0 +1,235 @@ +use ruff_macros::CacheKey; + +/// A list of names imported via any import statement. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, CacheKey)] +pub struct NameImports(Vec); + +/// A representation of an individual name imported via any import statement. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, CacheKey)] +pub enum NameImport { + Import(ModuleNameImport), + ImportFrom(MemberNameImport), +} + +/// A representation of an individual name imported via an `import` statement. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, CacheKey)] +pub struct ModuleNameImport { + pub name: Alias, +} + +/// A representation of an individual name imported via a `from ... import` statement. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, CacheKey)] +pub struct MemberNameImport { + pub module: Option, + pub name: Alias, + pub level: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, CacheKey)] +pub struct Alias { + pub name: String, + pub as_name: Option, +} + +impl NameImports { + pub fn into_imports(self) -> Vec { + self.0 + } +} + +impl ModuleNameImport { + /// Creates a new `Import` to import the specified module. + pub fn module(name: String) -> Self { + Self { + name: Alias { + name, + as_name: None, + }, + } + } +} + +impl MemberNameImport { + /// Creates a new `ImportFrom` to import a member from the specified module. + pub fn member(module: String, name: String) -> Self { + Self { + module: Some(module), + name: Alias { + name, + as_name: None, + }, + level: 0, + } + } + + pub fn alias(module: String, name: String, as_name: String) -> Self { + Self { + module: Some(module), + name: Alias { + name, + as_name: Some(as_name), + }, + level: 0, + } + } +} + +impl std::fmt::Display for NameImport { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + NameImport::Import(import) => write!(f, "{import}"), + NameImport::ImportFrom(import_from) => write!(f, "{import_from}"), + } + } +} + +impl std::fmt::Display for ModuleNameImport { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "import {}", self.name.name)?; + if let Some(as_name) = self.name.as_name.as_ref() { + write!(f, " as {as_name}")?; + } + Ok(()) + } +} + +impl std::fmt::Display for MemberNameImport { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "from ")?; + if self.level > 0 { + write!(f, "{}", ".".repeat(self.level as usize))?; + } + if let Some(module) = self.module.as_ref() { + write!(f, "{module}")?; + } + write!(f, " import {}", self.name.name)?; + if let Some(as_name) = self.name.as_name.as_ref() { + write!(f, " as {as_name}")?; + } + Ok(()) + } +} + +pub trait FutureImport { + /// Returns `true` if this import is from the `__future__` module. + fn is_future_import(&self) -> bool; +} + +impl FutureImport for ModuleNameImport { + fn is_future_import(&self) -> bool { + self.name.name == "__future__" + } +} + +impl FutureImport for MemberNameImport { + fn is_future_import(&self) -> bool { + self.module.as_deref() == Some("__future__") + } +} + +impl FutureImport for NameImport { + fn is_future_import(&self) -> bool { + match self { + NameImport::Import(import) => import.is_future_import(), + NameImport::ImportFrom(import_from) => import_from.is_future_import(), + } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for NameImports { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for NameImport { + fn serialize(&self, serializer: S) -> Result { + match self { + NameImport::Import(import) => serializer.collect_str(import), + NameImport::ImportFrom(import_from) => serializer.collect_str(import_from), + } + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::de::Deserialize<'de> for NameImports { + fn deserialize>(deserializer: D) -> Result { + use ruff_python_ast::{self as ast, Stmt}; + use ruff_python_parser::Parsed; + + struct AnyNameImportsVisitor; + + impl<'de> serde::de::Visitor<'de> for AnyNameImportsVisitor { + type Value = NameImports; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("an import statement") + } + + fn visit_str(self, value: &str) -> Result { + let body = ruff_python_parser::parse_module(value) + .map(Parsed::into_suite) + .map_err(E::custom)?; + let [stmt] = body.as_slice() else { + return Err(E::custom("Expected a single statement")); + }; + + let imports = match stmt { + Stmt::ImportFrom(ast::StmtImportFrom { + module, + names, + level, + range: _, + }) => names + .iter() + .map(|name| { + NameImport::ImportFrom(MemberNameImport { + module: module.as_deref().map(ToString::to_string), + name: Alias { + name: name.name.to_string(), + as_name: name.asname.as_deref().map(ToString::to_string), + }, + level: *level, + }) + }) + .collect(), + Stmt::Import(ast::StmtImport { names, range: _ }) => names + .iter() + .map(|name| { + NameImport::Import(ModuleNameImport { + name: Alias { + name: name.name.to_string(), + as_name: name.asname.as_deref().map(ToString::to_string), + }, + }) + }) + .collect(), + _ => { + return Err(E::custom("Expected an import statement")); + } + }; + + Ok(NameImports(imports)) + } + } + + deserializer.deserialize_str(AnyNameImportsVisitor) + } +} + +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for NameImports { + fn schema_name() -> String { + "NameImports".to_string() + } + + fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::String.into()), + ..Default::default() + } + .into() + } +} diff --git a/crates/ruff_python_semantic/src/lib.rs b/crates/ruff_python_semantic/src/lib.rs index ce45050239e47..30128e23a5894 100644 --- a/crates/ruff_python_semantic/src/lib.rs +++ b/crates/ruff_python_semantic/src/lib.rs @@ -4,6 +4,7 @@ mod branches; mod context; mod definition; mod globals; +mod imports; mod model; mod nodes; mod reference; @@ -15,6 +16,7 @@ pub use branches::*; pub use context::*; pub use definition::*; pub use globals::*; +pub use imports::*; pub use model::*; pub use nodes::*; pub use reference::*; diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index 20a81205c55dd..c2b79a8bdde23 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -17,6 +17,7 @@ ruff_linter = { workspace = true } ruff_formatter = { workspace = true } ruff_python_formatter = { workspace = true, features = ["serde"] } ruff_python_ast = { workspace = true } +ruff_python_semantic = { workspace = true, features = ["serde"] } ruff_source_file = { workspace = true } ruff_cache = { workspace = true } ruff_macros = { workspace = true } @@ -55,7 +56,7 @@ ignored = ["colored"] [features] default = [] -schemars = ["dep:schemars", "ruff_formatter/schemars", "ruff_python_formatter/schemars"] +schemars = ["dep:schemars", "ruff_formatter/schemars", "ruff_python_formatter/schemars", "ruff_python_semantic/schemars"] [lints] workspace = true diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index c8eb6ad248c9a..ac66f09986018 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -5,6 +5,8 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; +use crate::options_base::{OptionsMetadata, Visit}; +use crate::settings::LineEnding; use ruff_formatter::IndentStyle; use ruff_linter::line_width::{IndentWidth, LineLength}; use ruff_linter::rules::flake8_import_conventions::settings::BannedAliases; @@ -30,9 +32,7 @@ use ruff_linter::{warn_user_once, RuleSelector}; use ruff_macros::{CombineOptions, OptionsMetadata}; use ruff_python_ast::name::Name; use ruff_python_formatter::{DocstringCodeLineWidth, QuoteStyle}; - -use crate::options_base::{OptionsMetadata, Visit}; -use crate::settings::LineEnding; +use ruff_python_semantic::NameImports; #[derive(Clone, Debug, PartialEq, Eq, Default, OptionsMetadata, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] @@ -481,12 +481,12 @@ impl OptionsMetadata for DeprecatedTopLevelLintOptions { #[cfg(feature = "schemars")] impl schemars::JsonSchema for DeprecatedTopLevelLintOptions { - fn schema_name() -> std::string::String { + fn schema_name() -> String { "DeprecatedTopLevelLintOptions".to_owned() } fn schema_id() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed(std::concat!( - std::module_path!(), + std::borrow::Cow::Borrowed(concat!( + module_path!(), "::", "DeprecatedTopLevelLintOptions" )) @@ -2034,7 +2034,7 @@ pub struct IsortOptions { required-imports = ["from __future__ import annotations"] "# )] - pub required_imports: Option>, + pub required_imports: Option>, /// An override list of tokens to always recognize as a Class for /// [`order-by-type`](#lint_isort_order-by-type) regardless of casing. @@ -2434,7 +2434,12 @@ impl IsortOptions { } Ok(isort::settings::Settings { - required_imports: BTreeSet::from_iter(self.required_imports.unwrap_or_default()), + required_imports: self + .required_imports + .unwrap_or_default() + .into_iter() + .flat_map(NameImports::into_imports) + .collect(), combine_as_imports: self.combine_as_imports.unwrap_or(false), force_single_line: self.force_single_line.unwrap_or(false), force_sort_within_sections, diff --git a/ruff.schema.json b/ruff.schema.json index 71bf642a01f68..f98aace0282e9 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1676,7 +1676,7 @@ "null" ], "items": { - "type": "string" + "$ref": "#/definitions/NameImports" } }, "section-order": { @@ -2293,6 +2293,9 @@ }, "additionalProperties": false }, + "NameImports": { + "type": "string" + }, "OutputFormat": { "oneOf": [ {