mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34: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,
|
Binding, BindingKind, ExecutionContext, FromImportation, Importation, SubmoduleImportation,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::rules::isort::{categorize, ImportType};
|
use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
|
@ -294,25 +294,31 @@ pub fn typing_only_runtime_import(
|
||||||
&settings.isort.known_modules,
|
&settings.isort.known_modules,
|
||||||
settings.target_version,
|
settings.target_version,
|
||||||
) {
|
) {
|
||||||
ImportType::LocalFolder | ImportType::FirstParty => Some(Diagnostic::new(
|
ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => {
|
||||||
TypingOnlyFirstPartyImport {
|
Some(Diagnostic::new(
|
||||||
full_name: full_name.to_string(),
|
TypingOnlyFirstPartyImport {
|
||||||
},
|
full_name: full_name.to_string(),
|
||||||
binding.range,
|
},
|
||||||
)),
|
binding.range,
|
||||||
ImportType::ThirdParty => Some(Diagnostic::new(
|
))
|
||||||
TypingOnlyThirdPartyImport {
|
}
|
||||||
full_name: full_name.to_string(),
|
ImportSection::Known(ImportType::ThirdParty) | ImportSection::UserDefined(_) => {
|
||||||
},
|
Some(Diagnostic::new(
|
||||||
binding.range,
|
TypingOnlyThirdPartyImport {
|
||||||
)),
|
full_name: full_name.to_string(),
|
||||||
ImportType::StandardLibrary => Some(Diagnostic::new(
|
},
|
||||||
|
binding.range,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
ImportSection::Known(ImportType::StandardLibrary) => Some(Diagnostic::new(
|
||||||
TypingOnlyStandardLibraryImport {
|
TypingOnlyStandardLibraryImport {
|
||||||
full_name: full_name.to_string(),
|
full_name: full_name.to_string(),
|
||||||
},
|
},
|
||||||
binding.range,
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{fs, iter};
|
use std::{fs, iter};
|
||||||
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum_macros::EnumIter;
|
use strum_macros::EnumIter;
|
||||||
|
@ -11,6 +12,7 @@ use ruff_macros::CacheKey;
|
||||||
use ruff_python_stdlib::sys::KNOWN_STANDARD_LIBRARY;
|
use ruff_python_stdlib::sys::KNOWN_STANDARD_LIBRARY;
|
||||||
|
|
||||||
use crate::settings::types::PythonVersion;
|
use crate::settings::types::PythonVersion;
|
||||||
|
use crate::warn_user_once;
|
||||||
|
|
||||||
use super::types::{ImportBlock, Importable};
|
use super::types::{ImportBlock, Importable};
|
||||||
|
|
||||||
|
@ -37,6 +39,15 @@ pub enum ImportType {
|
||||||
LocalFolder,
|
LocalFolder,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema, CacheKey,
|
||||||
|
)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ImportSection {
|
||||||
|
Known(ImportType),
|
||||||
|
UserDefined(String),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Reason<'a> {
|
enum Reason<'a> {
|
||||||
NonZeroLevel,
|
NonZeroLevel,
|
||||||
|
@ -49,23 +60,27 @@ enum Reason<'a> {
|
||||||
SamePackage,
|
SamePackage,
|
||||||
SourceMatch(&'a Path),
|
SourceMatch(&'a Path),
|
||||||
NoMatch,
|
NoMatch,
|
||||||
|
UserDefinedSection,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn categorize(
|
pub fn categorize<'a>(
|
||||||
module_name: &str,
|
module_name: &str,
|
||||||
level: Option<usize>,
|
level: Option<usize>,
|
||||||
src: &[PathBuf],
|
src: &[PathBuf],
|
||||||
package: Option<&Path>,
|
package: Option<&Path>,
|
||||||
known_modules: &KnownModules,
|
known_modules: &'a KnownModules,
|
||||||
target_version: PythonVersion,
|
target_version: PythonVersion,
|
||||||
) -> ImportType {
|
) -> &'a ImportSection {
|
||||||
let module_base = module_name.split('.').next().unwrap();
|
let module_base = module_name.split('.').next().unwrap();
|
||||||
let (import_type, reason) = {
|
let (import_type, reason) = {
|
||||||
if level.map_or(false, |level| level > 0) {
|
if level.map_or(false, |level| level > 0) {
|
||||||
(ImportType::LocalFolder, Reason::NonZeroLevel)
|
(
|
||||||
|
&ImportSection::Known(ImportType::LocalFolder),
|
||||||
|
Reason::NonZeroLevel,
|
||||||
|
)
|
||||||
} else if module_base == "__future__" {
|
} 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) {
|
} else if let Some((import_type, reason)) = known_modules.categorize(module_name) {
|
||||||
(import_type, reason)
|
(import_type, reason)
|
||||||
} else if KNOWN_STANDARD_LIBRARY
|
} else if KNOWN_STANDARD_LIBRARY
|
||||||
|
@ -73,13 +88,25 @@ pub fn categorize(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.contains(module_base)
|
.contains(module_base)
|
||||||
{
|
{
|
||||||
(ImportType::StandardLibrary, Reason::KnownStandardLibrary)
|
(
|
||||||
|
&ImportSection::Known(ImportType::StandardLibrary),
|
||||||
|
Reason::KnownStandardLibrary,
|
||||||
|
)
|
||||||
} else if same_package(package, module_base) {
|
} 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) {
|
} else if let Some(src) = match_sources(src, module_base) {
|
||||||
(ImportType::FirstParty, Reason::SourceMatch(src))
|
(
|
||||||
|
&ImportSection::Known(ImportType::FirstParty),
|
||||||
|
Reason::SourceMatch(src),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(ImportType::ThirdParty, Reason::NoMatch)
|
(
|
||||||
|
&ImportSection::Known(ImportType::ThirdParty),
|
||||||
|
Reason::NoMatch,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
debug!(
|
debug!(
|
||||||
|
@ -114,10 +141,10 @@ pub fn categorize_imports<'a>(
|
||||||
block: ImportBlock<'a>,
|
block: ImportBlock<'a>,
|
||||||
src: &[PathBuf],
|
src: &[PathBuf],
|
||||||
package: Option<&Path>,
|
package: Option<&Path>,
|
||||||
known_modules: &KnownModules,
|
known_modules: &'a KnownModules,
|
||||||
target_version: PythonVersion,
|
target_version: PythonVersion,
|
||||||
) -> BTreeMap<ImportType, ImportBlock<'a>> {
|
) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> {
|
||||||
let mut block_by_type: BTreeMap<ImportType, ImportBlock> = BTreeMap::default();
|
let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default();
|
||||||
// Categorize `StmtKind::Import`.
|
// Categorize `StmtKind::Import`.
|
||||||
for (alias, comments) in block.import {
|
for (alias, comments) in block.import {
|
||||||
let import_type = categorize(
|
let import_type = categorize(
|
||||||
|
@ -188,13 +215,17 @@ pub fn categorize_imports<'a>(
|
||||||
#[derive(Debug, Default, CacheKey)]
|
#[derive(Debug, Default, CacheKey)]
|
||||||
pub struct KnownModules {
|
pub struct KnownModules {
|
||||||
/// A set of user-provided first-party modules.
|
/// 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.
|
/// 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.
|
/// 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.
|
/// 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`).
|
/// Whether any of the known modules are submodules (e.g., `foo.bar`, as opposed to `foo`).
|
||||||
has_submodules: bool,
|
has_submodules: bool,
|
||||||
}
|
}
|
||||||
|
@ -205,29 +236,67 @@ impl KnownModules {
|
||||||
third_party: Vec<String>,
|
third_party: Vec<String>,
|
||||||
local_folder: Vec<String>,
|
local_folder: Vec<String>,
|
||||||
standard_library: Vec<String>,
|
standard_library: Vec<String>,
|
||||||
|
user_defined: FxHashMap<String, Vec<String>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let first_party = BTreeSet::from_iter(first_party);
|
let modules = user_defined
|
||||||
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
|
|
||||||
.iter()
|
.iter()
|
||||||
.chain(third_party.iter())
|
.flat_map(|(section, modules)| {
|
||||||
.chain(local_folder.iter())
|
modules
|
||||||
.chain(standard_library.iter())
|
.iter()
|
||||||
.any(|m| m.contains('.'));
|
.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 {
|
Self {
|
||||||
first_party,
|
first_party,
|
||||||
third_party,
|
third_party,
|
||||||
local_folder,
|
local_folder,
|
||||||
standard_library,
|
standard_library,
|
||||||
|
user_defined,
|
||||||
|
known,
|
||||||
has_submodules,
|
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.
|
/// 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 {
|
if self.has_submodules {
|
||||||
// Check all module prefixes from the longest to the shortest (e.g., given
|
// 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,
|
// `foo.bar.baz`, check `foo.bar.baz`, then `foo.bar`, then `foo`, taking the first,
|
||||||
|
@ -239,34 +308,33 @@ impl KnownModules {
|
||||||
.rev()
|
.rev()
|
||||||
{
|
{
|
||||||
let submodule = &module_name[0..i];
|
let submodule = &module_name[0..i];
|
||||||
if self.first_party.contains(submodule) {
|
if let Some(result) = self.categorize_submodule(submodule) {
|
||||||
return Some((ImportType::FirstParty, Reason::KnownFirstParty));
|
return Some(result);
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
// Happy path: no submodules, so we can check the module base and be done.
|
// Happy path: no submodules, so we can check the module base and be done.
|
||||||
let module_base = module_name.split('.').next().unwrap();
|
let module_base = module_name.split('.').next().unwrap();
|
||||||
if self.first_party.contains(module_base) {
|
self.categorize_submodule(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) {
|
fn categorize_submodule(&self, submodule: &str) -> Option<(&ImportSection, Reason)> {
|
||||||
Some((ImportType::LocalFolder, Reason::KnownLocalFolder))
|
if let Some(section) = self.known.get(submodule) {
|
||||||
} else if self.standard_library.contains(module_base) {
|
let reason = match section {
|
||||||
Some((ImportType::StandardLibrary, Reason::ExtraStandardLibrary))
|
ImportSection::UserDefined(_) => Reason::UserDefinedSection,
|
||||||
} else {
|
ImportSection::Known(ImportType::FirstParty) => Reason::KnownFirstParty,
|
||||||
None
|
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 crate::rules::isort::categorize::KnownModules;
|
||||||
use annotate::annotate_imports;
|
use annotate::annotate_imports;
|
||||||
use categorize::categorize_imports;
|
use categorize::categorize_imports;
|
||||||
pub use categorize::{categorize, ImportType};
|
pub use categorize::{categorize, ImportSection, ImportType};
|
||||||
use comments::Comment;
|
use comments::Comment;
|
||||||
use normalize::normalize_imports;
|
use normalize::normalize_imports;
|
||||||
use order::order_imports;
|
use order::order_imports;
|
||||||
|
@ -22,6 +22,7 @@ use types::{AliasData, CommentSet, EitherImport, OrderedImportBlock, TrailingCom
|
||||||
|
|
||||||
use crate::rules::isort::types::ImportBlock;
|
use crate::rules::isort::types::ImportBlock;
|
||||||
use crate::settings::types::PythonVersion;
|
use crate::settings::types::PythonVersion;
|
||||||
|
use crate::warn_user_once;
|
||||||
|
|
||||||
mod annotate;
|
mod annotate;
|
||||||
mod categorize;
|
mod categorize;
|
||||||
|
@ -131,11 +132,12 @@ pub fn format_imports(
|
||||||
classes: &BTreeSet<String>,
|
classes: &BTreeSet<String>,
|
||||||
constants: &BTreeSet<String>,
|
constants: &BTreeSet<String>,
|
||||||
variables: &BTreeSet<String>,
|
variables: &BTreeSet<String>,
|
||||||
no_lines_before: &BTreeSet<ImportType>,
|
no_lines_before: &BTreeSet<ImportSection>,
|
||||||
lines_after_imports: isize,
|
lines_after_imports: isize,
|
||||||
lines_between_types: usize,
|
lines_between_types: usize,
|
||||||
forced_separate: &[String],
|
forced_separate: &[String],
|
||||||
target_version: PythonVersion,
|
target_version: PythonVersion,
|
||||||
|
section_order: &[ImportSection],
|
||||||
) -> String {
|
) -> String {
|
||||||
let trailer = &block.trailer;
|
let trailer = &block.trailer;
|
||||||
let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma);
|
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).
|
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
|
||||||
let block = normalize_imports(block, combine_as_imports, force_single_line);
|
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();
|
let mut output = String::new();
|
||||||
|
|
||||||
for block in split::split_by_forced_separate(block, forced_separate) {
|
for block in split::split_by_forced_separate(block, forced_separate) {
|
||||||
|
@ -167,6 +213,7 @@ pub fn format_imports(
|
||||||
no_lines_before,
|
no_lines_before,
|
||||||
lines_between_types,
|
lines_between_types,
|
||||||
target_version,
|
target_version,
|
||||||
|
§ion_order,
|
||||||
);
|
);
|
||||||
|
|
||||||
if !block_output.is_empty() && !output.is_empty() {
|
if !block_output.is_empty() && !output.is_empty() {
|
||||||
|
@ -221,9 +268,10 @@ fn format_import_block(
|
||||||
classes: &BTreeSet<String>,
|
classes: &BTreeSet<String>,
|
||||||
constants: &BTreeSet<String>,
|
constants: &BTreeSet<String>,
|
||||||
variables: &BTreeSet<String>,
|
variables: &BTreeSet<String>,
|
||||||
no_lines_before: &BTreeSet<ImportType>,
|
no_lines_before: &BTreeSet<ImportSection>,
|
||||||
lines_between_types: usize,
|
lines_between_types: usize,
|
||||||
target_version: PythonVersion,
|
target_version: PythonVersion,
|
||||||
|
section_order: &[ImportSection],
|
||||||
) -> String {
|
) -> String {
|
||||||
// Categorize by type (e.g., first-party vs. third-party).
|
// Categorize by type (e.g., first-party vs. third-party).
|
||||||
let mut block_by_type = categorize_imports(block, src, package, known_modules, target_version);
|
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.
|
// Generate replacement source code.
|
||||||
let mut is_first_block = true;
|
let mut is_first_block = true;
|
||||||
let mut pending_lines_before = false;
|
let mut pending_lines_before = false;
|
||||||
for import_type in ImportType::iter() {
|
for import_section in section_order {
|
||||||
let import_block = block_by_type.remove(&import_type);
|
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;
|
pending_lines_before = true;
|
||||||
}
|
}
|
||||||
let Some(import_block) = import_block else {
|
let Some(import_block) = import_block else {
|
||||||
|
@ -327,6 +375,7 @@ fn format_import_block(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
@ -336,7 +385,7 @@ mod tests {
|
||||||
use test_case::test_case;
|
use test_case::test_case;
|
||||||
|
|
||||||
use crate::registry::Rule;
|
use crate::registry::Rule;
|
||||||
use crate::rules::isort::categorize::KnownModules;
|
use crate::rules::isort::categorize::{ImportSection, KnownModules};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::test::{test_path, test_resource_path};
|
use crate::test::{test_path, test_resource_path};
|
||||||
|
|
||||||
|
@ -413,6 +462,7 @@ mod tests {
|
||||||
vec!["foo".to_string(), "__future__".to_string()],
|
vec!["foo".to_string(), "__future__".to_string()],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
|
FxHashMap::default(),
|
||||||
),
|
),
|
||||||
..super::settings::Settings::default()
|
..super::settings::Settings::default()
|
||||||
},
|
},
|
||||||
|
@ -436,6 +486,7 @@ mod tests {
|
||||||
vec!["foo.bar".to_string()],
|
vec!["foo.bar".to_string()],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
|
FxHashMap::default(),
|
||||||
),
|
),
|
||||||
..super::settings::Settings::default()
|
..super::settings::Settings::default()
|
||||||
},
|
},
|
||||||
|
@ -477,6 +528,7 @@ mod tests {
|
||||||
vec![],
|
vec![],
|
||||||
vec!["ruff".to_string()],
|
vec!["ruff".to_string()],
|
||||||
vec![],
|
vec![],
|
||||||
|
FxHashMap::default(),
|
||||||
),
|
),
|
||||||
..super::settings::Settings::default()
|
..super::settings::Settings::default()
|
||||||
},
|
},
|
||||||
|
@ -831,11 +883,11 @@ mod tests {
|
||||||
&Settings {
|
&Settings {
|
||||||
isort: super::settings::Settings {
|
isort: super::settings::Settings {
|
||||||
no_lines_before: BTreeSet::from([
|
no_lines_before: BTreeSet::from([
|
||||||
ImportType::Future,
|
ImportSection::Known(ImportType::Future),
|
||||||
ImportType::StandardLibrary,
|
ImportSection::Known(ImportType::StandardLibrary),
|
||||||
ImportType::ThirdParty,
|
ImportSection::Known(ImportType::ThirdParty),
|
||||||
ImportType::FirstParty,
|
ImportSection::Known(ImportType::FirstParty),
|
||||||
ImportType::LocalFolder,
|
ImportSection::Known(ImportType::LocalFolder),
|
||||||
]),
|
]),
|
||||||
..super::settings::Settings::default()
|
..super::settings::Settings::default()
|
||||||
},
|
},
|
||||||
|
@ -859,8 +911,8 @@ mod tests {
|
||||||
&Settings {
|
&Settings {
|
||||||
isort: super::settings::Settings {
|
isort: super::settings::Settings {
|
||||||
no_lines_before: BTreeSet::from([
|
no_lines_before: BTreeSet::from([
|
||||||
ImportType::StandardLibrary,
|
ImportSection::Known(ImportType::StandardLibrary),
|
||||||
ImportType::LocalFolder,
|
ImportSection::Known(ImportType::LocalFolder),
|
||||||
]),
|
]),
|
||||||
..super::settings::Settings::default()
|
..super::settings::Settings::default()
|
||||||
},
|
},
|
||||||
|
@ -935,4 +987,60 @@ mod tests {
|
||||||
assert_messages!(snapshot, diagnostics);
|
assert_messages!(snapshot, diagnostics);
|
||||||
Ok(())
|
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.lines_between_types,
|
||||||
&settings.isort.forced_separate,
|
&settings.isort.forced_separate,
|
||||||
settings.target_version,
|
settings.target_version,
|
||||||
|
&settings.isort.section_order,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Expand the span the entire range, including leading and trailing space.
|
// Expand the span the entire range, including leading and trailing space.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//! Settings for the `isort` plugin.
|
//! Settings for the `isort` plugin.
|
||||||
|
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
|
@ -8,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::rules::isort::categorize::KnownModules;
|
use crate::rules::isort::categorize::KnownModules;
|
||||||
use ruff_macros::{CacheKey, ConfigurationOptions};
|
use ruff_macros::{CacheKey, ConfigurationOptions};
|
||||||
|
|
||||||
use super::categorize::ImportType;
|
use super::categorize::ImportSection;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey, JsonSchema)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey, JsonSchema)]
|
||||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
|
@ -226,14 +227,14 @@ pub struct Options {
|
||||||
pub variables: Option<Vec<String>>,
|
pub variables: Option<Vec<String>>,
|
||||||
#[option(
|
#[option(
|
||||||
default = r#"[]"#,
|
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#"
|
example = r#"
|
||||||
no-lines-before = ["future", "standard-library"]
|
no-lines-before = ["future", "standard-library"]
|
||||||
"#
|
"#
|
||||||
)]
|
)]
|
||||||
/// A list of sections that should _not_ be delineated from the previous
|
/// A list of sections that should _not_ be delineated from the previous
|
||||||
/// section via empty lines.
|
/// section via empty lines.
|
||||||
pub no_lines_before: Option<Vec<ImportType>>,
|
pub no_lines_before: Option<Vec<ImportSection>>,
|
||||||
#[option(
|
#[option(
|
||||||
default = r#"-1"#,
|
default = r#"-1"#,
|
||||||
value_type = "int",
|
value_type = "int",
|
||||||
|
@ -265,6 +266,28 @@ pub struct Options {
|
||||||
/// A list of modules to separate into auxiliary block(s) of imports,
|
/// A list of modules to separate into auxiliary block(s) of imports,
|
||||||
/// in the order specified.
|
/// in the order specified.
|
||||||
pub forced_separate: Option<Vec<String>>,
|
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)]
|
#[derive(Debug, CacheKey)]
|
||||||
|
@ -284,10 +307,11 @@ pub struct Settings {
|
||||||
pub classes: BTreeSet<String>,
|
pub classes: BTreeSet<String>,
|
||||||
pub constants: BTreeSet<String>,
|
pub constants: BTreeSet<String>,
|
||||||
pub variables: 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_after_imports: isize,
|
||||||
pub lines_between_types: usize,
|
pub lines_between_types: usize,
|
||||||
pub forced_separate: Vec<String>,
|
pub forced_separate: Vec<String>,
|
||||||
|
pub section_order: Vec<ImportSection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
|
@ -311,6 +335,7 @@ impl Default for Settings {
|
||||||
lines_after_imports: -1,
|
lines_after_imports: -1,
|
||||||
lines_between_types: 0,
|
lines_between_types: 0,
|
||||||
forced_separate: Vec::new(),
|
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_third_party.unwrap_or_default(),
|
||||||
options.known_local_folder.unwrap_or_default(),
|
options.known_local_folder.unwrap_or_default(),
|
||||||
options.extra_standard_library.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),
|
order_by_type: options.order_by_type.unwrap_or(true),
|
||||||
relative_imports_order: options.relative_imports_order.unwrap_or_default(),
|
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_after_imports: options.lines_after_imports.unwrap_or(-1),
|
||||||
lines_between_types: options.lines_between_types.unwrap_or_default(),
|
lines_between_types: options.lines_between_types.unwrap_or_default(),
|
||||||
forced_separate: Vec::from_iter(options.forced_separate.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 {
|
Self {
|
||||||
required_imports: Some(settings.required_imports.into_iter().collect()),
|
required_imports: Some(settings.required_imports.into_iter().collect()),
|
||||||
combine_as_imports: Some(settings.combine_as_imports),
|
combine_as_imports: Some(settings.combine_as_imports),
|
||||||
extra_standard_library: Some(
|
extra_standard_library: Some(settings.known_modules.standard_library),
|
||||||
settings
|
|
||||||
.known_modules
|
|
||||||
.standard_library
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
force_single_line: Some(settings.force_single_line),
|
force_single_line: Some(settings.force_single_line),
|
||||||
force_sort_within_sections: Some(settings.force_sort_within_sections),
|
force_sort_within_sections: Some(settings.force_sort_within_sections),
|
||||||
force_wrap_aliases: Some(settings.force_wrap_aliases),
|
force_wrap_aliases: Some(settings.force_wrap_aliases),
|
||||||
force_to_top: Some(settings.force_to_top.into_iter().collect()),
|
force_to_top: Some(settings.force_to_top.into_iter().collect()),
|
||||||
known_first_party: Some(settings.known_modules.first_party.into_iter().collect()),
|
known_first_party: Some(settings.known_modules.first_party),
|
||||||
known_third_party: Some(settings.known_modules.third_party.into_iter().collect()),
|
known_third_party: Some(settings.known_modules.third_party),
|
||||||
known_local_folder: Some(settings.known_modules.local_folder.into_iter().collect()),
|
known_local_folder: Some(settings.known_modules.local_folder),
|
||||||
order_by_type: Some(settings.order_by_type),
|
order_by_type: Some(settings.order_by_type),
|
||||||
relative_imports_order: Some(settings.relative_imports_order),
|
relative_imports_order: Some(settings.relative_imports_order),
|
||||||
single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()),
|
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_after_imports: Some(settings.lines_after_imports),
|
||||||
lines_between_types: Some(settings.lines_between_types),
|
lines_between_types: Some(settings.lines_between_types),
|
||||||
forced_separate: Some(settings.forced_separate.into_iter().collect()),
|
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
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"ImportSection": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/ImportType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ImportType": {
|
"ImportType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
@ -1164,7 +1174,7 @@
|
||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/ImportType"
|
"$ref": "#/definitions/ImportSection"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"order-by-type": {
|
"order-by-type": {
|
||||||
|
@ -1195,6 +1205,29 @@
|
||||||
"type": "string"
|
"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": {
|
"single-line-exclusions": {
|
||||||
"description": "One or more modules to exclude from the single line rule.",
|
"description": "One or more modules to exclude from the single line rule.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue