mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
Move required import parsing out of lint rule (#12536)
## Summary Instead, make it part of the serialization and deserialization itself. This makes it _much_ easier to reuse when solving https://github.com/astral-sh/ruff/issues/12458.
This commit is contained in:
parent
7ad4df9e9f
commit
d930052de8
18 changed files with 330 additions and 276 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<ModModule>,
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
pub required_imports: BTreeSet<NameImport>,
|
||||
pub combine_as_imports: bool,
|
||||
pub force_single_line: bool,
|
||||
pub force_sort_within_sections: bool,
|
||||
|
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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"]
|
||||
|
|
235
crates/ruff_python_semantic/src/imports.rs
Normal file
235
crates/ruff_python_semantic/src/imports.rs
Normal file
|
@ -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<NameImport>);
|
||||
|
||||
/// 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<String>,
|
||||
pub name: Alias,
|
||||
pub level: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, CacheKey)]
|
||||
pub struct Alias {
|
||||
pub name: String,
|
||||
pub as_name: Option<String>,
|
||||
}
|
||||
|
||||
impl NameImports {
|
||||
pub fn into_imports(self) -> Vec<NameImport> {
|
||||
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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for NameImport {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
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<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
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<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
))
|
||||
|
@ -2035,7 +2035,7 @@ pub struct IsortOptions {
|
|||
required-imports = ["from __future__ import annotations"]
|
||||
"#
|
||||
)]
|
||||
pub required_imports: Option<Vec<String>>,
|
||||
pub required_imports: Option<Vec<NameImports>>,
|
||||
|
||||
/// An override list of tokens to always recognize as a Class for
|
||||
/// [`order-by-type`](#lint_isort_order-by-type) regardless of casing.
|
||||
|
@ -2435,7 +2435,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,
|
||||
|
|
5
ruff.schema.json
generated
5
ruff.schema.json
generated
|
@ -1676,7 +1676,7 @@
|
|||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/NameImports"
|
||||
}
|
||||
},
|
||||
"section-order": {
|
||||
|
@ -2293,6 +2293,9 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"NameImports": {
|
||||
"type": "string"
|
||||
},
|
||||
"OutputFormat": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue