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:
Charlie Marsh 2024-07-26 13:35:45 -04:00 committed by GitHub
parent 7ad4df9e9f
commit d930052de8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 330 additions and 276 deletions

5
Cargo.lock generated
View file

@ -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",

View file

@ -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,
);

View file

@ -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()

View file

@ -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)

View file

@ -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()
}

View file

@ -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,

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -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(),
}
}
}

View file

@ -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;

View file

@ -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"]

View 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()
}
}

View file

@ -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::*;

View file

@ -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

View file

@ -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
View file

@ -1676,7 +1676,7 @@
"null"
],
"items": {
"type": "string"
"$ref": "#/definitions/NameImports"
}
},
"section-order": {
@ -2293,6 +2293,9 @@
},
"additionalProperties": false
},
"NameImports": {
"type": "string"
},
"OutputFormat": {
"oneOf": [
{