mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
parent
1f22e035e3
commit
2d2630ef07
9 changed files with 400 additions and 94 deletions
7
crates/ruff/resources/test/fixtures/isort/sections.py
vendored
Normal file
7
crates/ruff/resources/test/fixtures/isort/sections.py
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import pytz
|
||||
import django.settings
|
||||
from library import foo
|
||||
from . import local
|
|
@ -6,7 +6,7 @@ use ruff_python_semantic::binding::{
|
|||
Binding, BindingKind, ExecutionContext, FromImportation, Importation, SubmoduleImportation,
|
||||
};
|
||||
|
||||
use crate::rules::isort::{categorize, ImportType};
|
||||
use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// ## What it does
|
||||
|
@ -294,25 +294,31 @@ pub fn typing_only_runtime_import(
|
|||
&settings.isort.known_modules,
|
||||
settings.target_version,
|
||||
) {
|
||||
ImportType::LocalFolder | ImportType::FirstParty => Some(Diagnostic::new(
|
||||
TypingOnlyFirstPartyImport {
|
||||
full_name: full_name.to_string(),
|
||||
},
|
||||
binding.range,
|
||||
)),
|
||||
ImportType::ThirdParty => Some(Diagnostic::new(
|
||||
TypingOnlyThirdPartyImport {
|
||||
full_name: full_name.to_string(),
|
||||
},
|
||||
binding.range,
|
||||
)),
|
||||
ImportType::StandardLibrary => Some(Diagnostic::new(
|
||||
ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => {
|
||||
Some(Diagnostic::new(
|
||||
TypingOnlyFirstPartyImport {
|
||||
full_name: full_name.to_string(),
|
||||
},
|
||||
binding.range,
|
||||
))
|
||||
}
|
||||
ImportSection::Known(ImportType::ThirdParty) | ImportSection::UserDefined(_) => {
|
||||
Some(Diagnostic::new(
|
||||
TypingOnlyThirdPartyImport {
|
||||
full_name: full_name.to_string(),
|
||||
},
|
||||
binding.range,
|
||||
))
|
||||
}
|
||||
ImportSection::Known(ImportType::StandardLibrary) => Some(Diagnostic::new(
|
||||
TypingOnlyStandardLibraryImport {
|
||||
full_name: full_name.to_string(),
|
||||
},
|
||||
binding.range,
|
||||
)),
|
||||
ImportType::Future => unreachable!("`__future__` imports should be marked as used"),
|
||||
ImportSection::Known(ImportType::Future) => {
|
||||
unreachable!("`__future__` imports should be marked as used")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fs, iter};
|
||||
|
||||
use log::debug;
|
||||
use rustc_hash::FxHashMap;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
|
@ -11,6 +12,7 @@ use ruff_macros::CacheKey;
|
|||
use ruff_python_stdlib::sys::KNOWN_STANDARD_LIBRARY;
|
||||
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::warn_user_once;
|
||||
|
||||
use super::types::{ImportBlock, Importable};
|
||||
|
||||
|
@ -37,6 +39,15 @@ pub enum ImportType {
|
|||
LocalFolder,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema, CacheKey,
|
||||
)]
|
||||
#[serde(untagged)]
|
||||
pub enum ImportSection {
|
||||
Known(ImportType),
|
||||
UserDefined(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Reason<'a> {
|
||||
NonZeroLevel,
|
||||
|
@ -49,23 +60,27 @@ enum Reason<'a> {
|
|||
SamePackage,
|
||||
SourceMatch(&'a Path),
|
||||
NoMatch,
|
||||
UserDefinedSection,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn categorize(
|
||||
pub fn categorize<'a>(
|
||||
module_name: &str,
|
||||
level: Option<usize>,
|
||||
src: &[PathBuf],
|
||||
package: Option<&Path>,
|
||||
known_modules: &KnownModules,
|
||||
known_modules: &'a KnownModules,
|
||||
target_version: PythonVersion,
|
||||
) -> ImportType {
|
||||
) -> &'a ImportSection {
|
||||
let module_base = module_name.split('.').next().unwrap();
|
||||
let (import_type, reason) = {
|
||||
if level.map_or(false, |level| level > 0) {
|
||||
(ImportType::LocalFolder, Reason::NonZeroLevel)
|
||||
(
|
||||
&ImportSection::Known(ImportType::LocalFolder),
|
||||
Reason::NonZeroLevel,
|
||||
)
|
||||
} else if module_base == "__future__" {
|
||||
(ImportType::Future, Reason::Future)
|
||||
(&ImportSection::Known(ImportType::Future), Reason::Future)
|
||||
} else if let Some((import_type, reason)) = known_modules.categorize(module_name) {
|
||||
(import_type, reason)
|
||||
} else if KNOWN_STANDARD_LIBRARY
|
||||
|
@ -73,13 +88,25 @@ pub fn categorize(
|
|||
.unwrap()
|
||||
.contains(module_base)
|
||||
{
|
||||
(ImportType::StandardLibrary, Reason::KnownStandardLibrary)
|
||||
(
|
||||
&ImportSection::Known(ImportType::StandardLibrary),
|
||||
Reason::KnownStandardLibrary,
|
||||
)
|
||||
} else if same_package(package, module_base) {
|
||||
(ImportType::FirstParty, Reason::SamePackage)
|
||||
(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::SamePackage,
|
||||
)
|
||||
} else if let Some(src) = match_sources(src, module_base) {
|
||||
(ImportType::FirstParty, Reason::SourceMatch(src))
|
||||
(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::SourceMatch(src),
|
||||
)
|
||||
} else {
|
||||
(ImportType::ThirdParty, Reason::NoMatch)
|
||||
(
|
||||
&ImportSection::Known(ImportType::ThirdParty),
|
||||
Reason::NoMatch,
|
||||
)
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
|
@ -114,10 +141,10 @@ pub fn categorize_imports<'a>(
|
|||
block: ImportBlock<'a>,
|
||||
src: &[PathBuf],
|
||||
package: Option<&Path>,
|
||||
known_modules: &KnownModules,
|
||||
known_modules: &'a KnownModules,
|
||||
target_version: PythonVersion,
|
||||
) -> BTreeMap<ImportType, ImportBlock<'a>> {
|
||||
let mut block_by_type: BTreeMap<ImportType, ImportBlock> = BTreeMap::default();
|
||||
) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> {
|
||||
let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default();
|
||||
// Categorize `StmtKind::Import`.
|
||||
for (alias, comments) in block.import {
|
||||
let import_type = categorize(
|
||||
|
@ -188,13 +215,17 @@ pub fn categorize_imports<'a>(
|
|||
#[derive(Debug, Default, CacheKey)]
|
||||
pub struct KnownModules {
|
||||
/// A set of user-provided first-party modules.
|
||||
pub first_party: BTreeSet<String>,
|
||||
pub first_party: Vec<String>,
|
||||
/// A set of user-provided third-party modules.
|
||||
pub third_party: BTreeSet<String>,
|
||||
pub third_party: Vec<String>,
|
||||
/// A set of user-provided local folder modules.
|
||||
pub local_folder: BTreeSet<String>,
|
||||
pub local_folder: Vec<String>,
|
||||
/// A set of user-provided standard library modules.
|
||||
pub standard_library: BTreeSet<String>,
|
||||
pub standard_library: Vec<String>,
|
||||
/// A map of additional user-provided sections.
|
||||
pub user_defined: FxHashMap<String, Vec<String>>,
|
||||
/// A map of known modules to their section.
|
||||
pub known: FxHashMap<String, ImportSection>,
|
||||
/// Whether any of the known modules are submodules (e.g., `foo.bar`, as opposed to `foo`).
|
||||
has_submodules: bool,
|
||||
}
|
||||
|
@ -205,29 +236,67 @@ impl KnownModules {
|
|||
third_party: Vec<String>,
|
||||
local_folder: Vec<String>,
|
||||
standard_library: Vec<String>,
|
||||
user_defined: FxHashMap<String, Vec<String>>,
|
||||
) -> Self {
|
||||
let first_party = BTreeSet::from_iter(first_party);
|
||||
let third_party = BTreeSet::from_iter(third_party);
|
||||
let local_folder = BTreeSet::from_iter(local_folder);
|
||||
let standard_library = BTreeSet::from_iter(standard_library);
|
||||
let has_submodules = first_party
|
||||
let modules = user_defined
|
||||
.iter()
|
||||
.chain(third_party.iter())
|
||||
.chain(local_folder.iter())
|
||||
.chain(standard_library.iter())
|
||||
.any(|m| m.contains('.'));
|
||||
.flat_map(|(section, modules)| {
|
||||
modules
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|module| (module, ImportSection::UserDefined(section.clone())))
|
||||
})
|
||||
.chain(
|
||||
first_party
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|module| (module, ImportSection::Known(ImportType::FirstParty))),
|
||||
)
|
||||
.chain(
|
||||
third_party
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|module| (module, ImportSection::Known(ImportType::ThirdParty))),
|
||||
)
|
||||
.chain(
|
||||
local_folder
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|module| (module, ImportSection::Known(ImportType::LocalFolder))),
|
||||
)
|
||||
.chain(
|
||||
standard_library
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|module| (module, ImportSection::Known(ImportType::StandardLibrary))),
|
||||
);
|
||||
|
||||
let mut known = FxHashMap::with_capacity_and_hasher(
|
||||
modules.size_hint().0,
|
||||
std::hash::BuildHasherDefault::default(),
|
||||
);
|
||||
modules.for_each(|(module, section)| {
|
||||
if known.insert(module, section).is_some() {
|
||||
warn_user_once!("One or more modules are part of multiple import sections.");
|
||||
}
|
||||
});
|
||||
|
||||
let has_submodules = known.keys().any(|module| module.contains('.'));
|
||||
|
||||
Self {
|
||||
first_party,
|
||||
third_party,
|
||||
local_folder,
|
||||
standard_library,
|
||||
user_defined,
|
||||
known,
|
||||
has_submodules,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`ImportType`] for a given module, if it's been categorized as a known module
|
||||
/// Return the [`ImportSection`] for a given module, if it's been categorized as a known module
|
||||
/// by the user.
|
||||
fn categorize(&self, module_name: &str) -> Option<(ImportType, Reason)> {
|
||||
fn categorize(&self, module_name: &str) -> Option<(&ImportSection, Reason)> {
|
||||
if self.has_submodules {
|
||||
// Check all module prefixes from the longest to the shortest (e.g., given
|
||||
// `foo.bar.baz`, check `foo.bar.baz`, then `foo.bar`, then `foo`, taking the first,
|
||||
|
@ -239,34 +308,33 @@ impl KnownModules {
|
|||
.rev()
|
||||
{
|
||||
let submodule = &module_name[0..i];
|
||||
if self.first_party.contains(submodule) {
|
||||
return Some((ImportType::FirstParty, Reason::KnownFirstParty));
|
||||
}
|
||||
if self.third_party.contains(submodule) {
|
||||
return Some((ImportType::ThirdParty, Reason::KnownThirdParty));
|
||||
}
|
||||
if self.local_folder.contains(submodule) {
|
||||
return Some((ImportType::LocalFolder, Reason::KnownLocalFolder));
|
||||
}
|
||||
if self.standard_library.contains(submodule) {
|
||||
return Some((ImportType::StandardLibrary, Reason::ExtraStandardLibrary));
|
||||
if let Some(result) = self.categorize_submodule(submodule) {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
None
|
||||
} else {
|
||||
// Happy path: no submodules, so we can check the module base and be done.
|
||||
let module_base = module_name.split('.').next().unwrap();
|
||||
if self.first_party.contains(module_base) {
|
||||
Some((ImportType::FirstParty, Reason::KnownFirstParty))
|
||||
} else if self.third_party.contains(module_base) {
|
||||
Some((ImportType::ThirdParty, Reason::KnownThirdParty))
|
||||
} else if self.local_folder.contains(module_base) {
|
||||
Some((ImportType::LocalFolder, Reason::KnownLocalFolder))
|
||||
} else if self.standard_library.contains(module_base) {
|
||||
Some((ImportType::StandardLibrary, Reason::ExtraStandardLibrary))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.categorize_submodule(module_base)
|
||||
}
|
||||
}
|
||||
|
||||
fn categorize_submodule(&self, submodule: &str) -> Option<(&ImportSection, Reason)> {
|
||||
if let Some(section) = self.known.get(submodule) {
|
||||
let reason = match section {
|
||||
ImportSection::UserDefined(_) => Reason::UserDefinedSection,
|
||||
ImportSection::Known(ImportType::FirstParty) => Reason::KnownFirstParty,
|
||||
ImportSection::Known(ImportType::ThirdParty) => Reason::KnownThirdParty,
|
||||
ImportSection::Known(ImportType::LocalFolder) => Reason::KnownLocalFolder,
|
||||
ImportSection::Known(ImportType::StandardLibrary) => Reason::ExtraStandardLibrary,
|
||||
ImportSection::Known(ImportType::Future) => {
|
||||
unreachable!("__future__ imports are not known")
|
||||
}
|
||||
};
|
||||
Some((section, reason))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use strum::IntoEnumIterator;
|
|||
use crate::rules::isort::categorize::KnownModules;
|
||||
use annotate::annotate_imports;
|
||||
use categorize::categorize_imports;
|
||||
pub use categorize::{categorize, ImportType};
|
||||
pub use categorize::{categorize, ImportSection, ImportType};
|
||||
use comments::Comment;
|
||||
use normalize::normalize_imports;
|
||||
use order::order_imports;
|
||||
|
@ -22,6 +22,7 @@ use types::{AliasData, CommentSet, EitherImport, OrderedImportBlock, TrailingCom
|
|||
|
||||
use crate::rules::isort::types::ImportBlock;
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::warn_user_once;
|
||||
|
||||
mod annotate;
|
||||
mod categorize;
|
||||
|
@ -131,11 +132,12 @@ pub fn format_imports(
|
|||
classes: &BTreeSet<String>,
|
||||
constants: &BTreeSet<String>,
|
||||
variables: &BTreeSet<String>,
|
||||
no_lines_before: &BTreeSet<ImportType>,
|
||||
no_lines_before: &BTreeSet<ImportSection>,
|
||||
lines_after_imports: isize,
|
||||
lines_between_types: usize,
|
||||
forced_separate: &[String],
|
||||
target_version: PythonVersion,
|
||||
section_order: &[ImportSection],
|
||||
) -> String {
|
||||
let trailer = &block.trailer;
|
||||
let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma);
|
||||
|
@ -143,6 +145,50 @@ pub fn format_imports(
|
|||
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
|
||||
let block = normalize_imports(block, combine_as_imports, force_single_line);
|
||||
|
||||
// Make sure all sections (built-in and user-defined) are present in the section order.
|
||||
let mut section_order: Vec<_> = section_order.to_vec();
|
||||
for known_section in ImportType::iter().map(ImportSection::Known) {
|
||||
if !section_order.contains(&known_section) {
|
||||
section_order.push(known_section);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that all sections listed in `section-order` are defined in `sections`.
|
||||
for user_defined in §ion_order {
|
||||
if let ImportSection::UserDefined(section_name) = user_defined {
|
||||
if !known_modules.user_defined.contains_key(section_name) {
|
||||
warn_user_once!(
|
||||
"`section-order` contains unknown user-defined section: `{}`.",
|
||||
section_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that all sections listed in `no-lines-before` are defined in `sections`.
|
||||
for user_defined in no_lines_before {
|
||||
if let ImportSection::UserDefined(section_name) = user_defined {
|
||||
if !known_modules.user_defined.contains_key(section_name) {
|
||||
warn_user_once!(
|
||||
"`no-lines-before` contains unknown user-defined section: `{}`.",
|
||||
section_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that all sections defined in `sections` are listed in `section-order`.
|
||||
for section_name in known_modules.user_defined.keys() {
|
||||
let section = ImportSection::UserDefined(section_name.clone());
|
||||
if !section_order.contains(§ion) {
|
||||
warn_user_once!(
|
||||
"`section-order` is missing user-defined section: `{}`.",
|
||||
section_name
|
||||
);
|
||||
section_order.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
for block in split::split_by_forced_separate(block, forced_separate) {
|
||||
|
@ -167,6 +213,7 @@ pub fn format_imports(
|
|||
no_lines_before,
|
||||
lines_between_types,
|
||||
target_version,
|
||||
§ion_order,
|
||||
);
|
||||
|
||||
if !block_output.is_empty() && !output.is_empty() {
|
||||
|
@ -221,9 +268,10 @@ fn format_import_block(
|
|||
classes: &BTreeSet<String>,
|
||||
constants: &BTreeSet<String>,
|
||||
variables: &BTreeSet<String>,
|
||||
no_lines_before: &BTreeSet<ImportType>,
|
||||
no_lines_before: &BTreeSet<ImportSection>,
|
||||
lines_between_types: usize,
|
||||
target_version: PythonVersion,
|
||||
section_order: &[ImportSection],
|
||||
) -> String {
|
||||
// Categorize by type (e.g., first-party vs. third-party).
|
||||
let mut block_by_type = categorize_imports(block, src, package, known_modules, target_version);
|
||||
|
@ -233,10 +281,10 @@ fn format_import_block(
|
|||
// Generate replacement source code.
|
||||
let mut is_first_block = true;
|
||||
let mut pending_lines_before = false;
|
||||
for import_type in ImportType::iter() {
|
||||
let import_block = block_by_type.remove(&import_type);
|
||||
for import_section in section_order {
|
||||
let import_block = block_by_type.remove(import_section);
|
||||
|
||||
if !no_lines_before.contains(&import_type) {
|
||||
if !no_lines_before.contains(import_section) {
|
||||
pending_lines_before = true;
|
||||
}
|
||||
let Some(import_block) = import_block else {
|
||||
|
@ -327,6 +375,7 @@ fn format_import_block(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
|
@ -336,7 +385,7 @@ mod tests {
|
|||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::isort::categorize::KnownModules;
|
||||
use crate::rules::isort::categorize::{ImportSection, KnownModules};
|
||||
use crate::settings::Settings;
|
||||
use crate::test::{test_path, test_resource_path};
|
||||
|
||||
|
@ -413,6 +462,7 @@ mod tests {
|
|||
vec!["foo".to_string(), "__future__".to_string()],
|
||||
vec![],
|
||||
vec![],
|
||||
FxHashMap::default(),
|
||||
),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
|
@ -436,6 +486,7 @@ mod tests {
|
|||
vec!["foo.bar".to_string()],
|
||||
vec![],
|
||||
vec![],
|
||||
FxHashMap::default(),
|
||||
),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
|
@ -477,6 +528,7 @@ mod tests {
|
|||
vec![],
|
||||
vec!["ruff".to_string()],
|
||||
vec![],
|
||||
FxHashMap::default(),
|
||||
),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
|
@ -831,11 +883,11 @@ mod tests {
|
|||
&Settings {
|
||||
isort: super::settings::Settings {
|
||||
no_lines_before: BTreeSet::from([
|
||||
ImportType::Future,
|
||||
ImportType::StandardLibrary,
|
||||
ImportType::ThirdParty,
|
||||
ImportType::FirstParty,
|
||||
ImportType::LocalFolder,
|
||||
ImportSection::Known(ImportType::Future),
|
||||
ImportSection::Known(ImportType::StandardLibrary),
|
||||
ImportSection::Known(ImportType::ThirdParty),
|
||||
ImportSection::Known(ImportType::FirstParty),
|
||||
ImportSection::Known(ImportType::LocalFolder),
|
||||
]),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
|
@ -859,8 +911,8 @@ mod tests {
|
|||
&Settings {
|
||||
isort: super::settings::Settings {
|
||||
no_lines_before: BTreeSet::from([
|
||||
ImportType::StandardLibrary,
|
||||
ImportType::LocalFolder,
|
||||
ImportSection::Known(ImportType::StandardLibrary),
|
||||
ImportSection::Known(ImportType::LocalFolder),
|
||||
]),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
|
@ -935,4 +987,60 @@ mod tests {
|
|||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("sections.py"))]
|
||||
fn sections(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("sections_{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("isort").join(path).as_path(),
|
||||
&Settings {
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
isort: super::settings::Settings {
|
||||
known_modules: KnownModules::new(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
FxHashMap::from_iter([("django".to_string(), vec!["django".to_string()])]),
|
||||
),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
..Settings::for_rule(Rule::UnsortedImports)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("sections.py"))]
|
||||
fn section_order(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("section_order_{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("isort").join(path).as_path(),
|
||||
&Settings {
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
isort: super::settings::Settings {
|
||||
known_modules: KnownModules::new(
|
||||
vec!["library".to_string()],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
FxHashMap::from_iter([("django".to_string(), vec!["django".to_string()])]),
|
||||
),
|
||||
section_order: vec![
|
||||
ImportSection::Known(ImportType::Future),
|
||||
ImportSection::Known(ImportType::StandardLibrary),
|
||||
ImportSection::Known(ImportType::ThirdParty),
|
||||
ImportSection::UserDefined("django".to_string()),
|
||||
ImportSection::Known(ImportType::FirstParty),
|
||||
ImportSection::Known(ImportType::LocalFolder),
|
||||
],
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
..Settings::for_rule(Rule::UnsortedImports)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,6 +139,7 @@ pub fn organize_imports(
|
|||
settings.isort.lines_between_types,
|
||||
&settings.isort.forced_separate,
|
||||
settings.target_version,
|
||||
&settings.isort.section_order,
|
||||
);
|
||||
|
||||
// Expand the span the entire range, including leading and trailing space.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Settings for the `isort` plugin.
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
|
@ -8,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::rules::isort::categorize::KnownModules;
|
||||
use ruff_macros::{CacheKey, ConfigurationOptions};
|
||||
|
||||
use super::categorize::ImportType;
|
||||
use super::categorize::ImportSection;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey, JsonSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||
|
@ -226,14 +227,14 @@ pub struct Options {
|
|||
pub variables: Option<Vec<String>>,
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder"]"#,
|
||||
value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str]"#,
|
||||
example = r#"
|
||||
no-lines-before = ["future", "standard-library"]
|
||||
"#
|
||||
)]
|
||||
/// A list of sections that should _not_ be delineated from the previous
|
||||
/// section via empty lines.
|
||||
pub no_lines_before: Option<Vec<ImportType>>,
|
||||
pub no_lines_before: Option<Vec<ImportSection>>,
|
||||
#[option(
|
||||
default = r#"-1"#,
|
||||
value_type = "int",
|
||||
|
@ -265,6 +266,28 @@ pub struct Options {
|
|||
/// A list of modules to separate into auxiliary block(s) of imports,
|
||||
/// in the order specified.
|
||||
pub forced_separate: Option<Vec<String>>,
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str]"#,
|
||||
example = r#"
|
||||
section-order = ["future", "standard-library", "first-party", "local-folder", "third-party"]
|
||||
"#
|
||||
)]
|
||||
/// Override in which order the sections should be output. Can be used to move custom sections.
|
||||
pub section_order: Option<Vec<ImportSection>>,
|
||||
// Tables are required to go last.
|
||||
#[option(
|
||||
default = "{}",
|
||||
value_type = "dict[str, list[str]]",
|
||||
example = r#"
|
||||
# Group all Django imports into a separate section.
|
||||
[tool.ruff.isort.sections]
|
||||
"django" = ["django"]
|
||||
"#
|
||||
)]
|
||||
/// A list of mappings from section names to modules.
|
||||
/// By default custom sections are output last, but this can be overridden with `section-order`.
|
||||
pub sections: Option<FxHashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, CacheKey)]
|
||||
|
@ -284,10 +307,11 @@ pub struct Settings {
|
|||
pub classes: BTreeSet<String>,
|
||||
pub constants: BTreeSet<String>,
|
||||
pub variables: BTreeSet<String>,
|
||||
pub no_lines_before: BTreeSet<ImportType>,
|
||||
pub no_lines_before: BTreeSet<ImportSection>,
|
||||
pub lines_after_imports: isize,
|
||||
pub lines_between_types: usize,
|
||||
pub forced_separate: Vec<String>,
|
||||
pub section_order: Vec<ImportSection>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
|
@ -311,6 +335,7 @@ impl Default for Settings {
|
|||
lines_after_imports: -1,
|
||||
lines_between_types: 0,
|
||||
forced_separate: Vec::new(),
|
||||
section_order: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -329,6 +354,7 @@ impl From<Options> for Settings {
|
|||
options.known_third_party.unwrap_or_default(),
|
||||
options.known_local_folder.unwrap_or_default(),
|
||||
options.extra_standard_library.unwrap_or_default(),
|
||||
options.sections.unwrap_or_default(),
|
||||
),
|
||||
order_by_type: options.order_by_type.unwrap_or(true),
|
||||
relative_imports_order: options.relative_imports_order.unwrap_or_default(),
|
||||
|
@ -343,6 +369,7 @@ impl From<Options> for Settings {
|
|||
lines_after_imports: options.lines_after_imports.unwrap_or(-1),
|
||||
lines_between_types: options.lines_between_types.unwrap_or_default(),
|
||||
forced_separate: Vec::from_iter(options.forced_separate.unwrap_or_default()),
|
||||
section_order: Vec::from_iter(options.section_order.unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -352,20 +379,14 @@ impl From<Settings> for Options {
|
|||
Self {
|
||||
required_imports: Some(settings.required_imports.into_iter().collect()),
|
||||
combine_as_imports: Some(settings.combine_as_imports),
|
||||
extra_standard_library: Some(
|
||||
settings
|
||||
.known_modules
|
||||
.standard_library
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
extra_standard_library: Some(settings.known_modules.standard_library),
|
||||
force_single_line: Some(settings.force_single_line),
|
||||
force_sort_within_sections: Some(settings.force_sort_within_sections),
|
||||
force_wrap_aliases: Some(settings.force_wrap_aliases),
|
||||
force_to_top: Some(settings.force_to_top.into_iter().collect()),
|
||||
known_first_party: Some(settings.known_modules.first_party.into_iter().collect()),
|
||||
known_third_party: Some(settings.known_modules.third_party.into_iter().collect()),
|
||||
known_local_folder: Some(settings.known_modules.local_folder.into_iter().collect()),
|
||||
known_first_party: Some(settings.known_modules.first_party),
|
||||
known_third_party: Some(settings.known_modules.third_party),
|
||||
known_local_folder: Some(settings.known_modules.local_folder),
|
||||
order_by_type: Some(settings.order_by_type),
|
||||
relative_imports_order: Some(settings.relative_imports_order),
|
||||
single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()),
|
||||
|
@ -377,6 +398,8 @@ impl From<Settings> for Options {
|
|||
lines_after_imports: Some(settings.lines_after_imports),
|
||||
lines_between_types: Some(settings.lines_between_types),
|
||||
forced_separate: Some(settings.forced_separate.into_iter().collect()),
|
||||
section_order: Some(settings.section_order.into_iter().collect()),
|
||||
sections: Some(settings.known_modules.user_defined),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/isort/mod.rs
|
||||
---
|
||||
sections.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
|
|
||||
1 | / from __future__ import annotations
|
||||
2 | | import os
|
||||
3 | | import sys
|
||||
4 | | import pytz
|
||||
5 | | import django.settings
|
||||
6 | | from library import foo
|
||||
7 | | from . import local
|
||||
|
|
||||
= help: Organize imports
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | from __future__ import annotations
|
||||
2 |+
|
||||
2 3 | import os
|
||||
3 4 | import sys
|
||||
5 |+
|
||||
4 6 | import pytz
|
||||
7 |+
|
||||
5 8 | import django.settings
|
||||
9 |+
|
||||
6 10 | from library import foo
|
||||
11 |+
|
||||
7 12 | from . import local
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/isort/mod.rs
|
||||
---
|
||||
sections.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
|
|
||||
1 | / from __future__ import annotations
|
||||
2 | | import os
|
||||
3 | | import sys
|
||||
4 | | import pytz
|
||||
5 | | import django.settings
|
||||
6 | | from library import foo
|
||||
7 | | from . import local
|
||||
|
|
||||
= help: Organize imports
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | from __future__ import annotations
|
||||
2 |+
|
||||
2 3 | import os
|
||||
3 4 | import sys
|
||||
5 |+
|
||||
4 6 | import pytz
|
||||
5 |-import django.settings
|
||||
6 7 | from library import foo
|
||||
8 |+
|
||||
7 9 | from . import local
|
||||
10 |+
|
||||
11 |+import django.settings
|
||||
|
||||
|
35
ruff.schema.json
generated
35
ruff.schema.json
generated
|
@ -1019,6 +1019,16 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ImportSection": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ImportType"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ImportType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
@ -1164,7 +1174,7 @@
|
|||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/ImportType"
|
||||
"$ref": "#/definitions/ImportSection"
|
||||
}
|
||||
},
|
||||
"order-by-type": {
|
||||
|
@ -1195,6 +1205,29 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"section-order": {
|
||||
"description": "Override in which order the sections should be output. Can be used to move custom sections.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/ImportSection"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"description": "A list of mappings from section names to modules. By default custom sections are output last, but this can be overridden with `section-order`.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"single-line-exclusions": {
|
||||
"description": "One or more modules to exclude from the single line rule.",
|
||||
"type": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue