Compare commits

..

2 Commits

Author SHA1 Message Date
Micha Reiser
b93d3e6f21 Add formatter to runes 2024-05-01 10:42:53 +02:00
Micha Reiser
523235d6ea First format draft 2024-05-01 10:42:51 +02:00
167 changed files with 2357 additions and 1643 deletions

View File

@@ -59,6 +59,7 @@ jobs:
- "!crates/ruff_python_formatter/**"
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**"
- scripts/*
- python/**
- .github/workflows/ci.yaml

View File

@@ -1,53 +1,5 @@
# Changelog
## 0.4.3
### Enhancements
- Add support for PEP 696 syntax ([#11120](https://github.com/astral-sh/ruff/pull/11120))
### Preview features
- \[`refurb`\] Use function range for `reimplemented-operator` diagnostics ([#11271](https://github.com/astral-sh/ruff/pull/11271))
- \[`refurb`\] Ignore methods in `reimplemented-operator` (`FURB118`) ([#11270](https://github.com/astral-sh/ruff/pull/11270))
- \[`refurb`\] Implement `fstring-number-format` (`FURB116`) ([#10921](https://github.com/astral-sh/ruff/pull/10921))
- \[`ruff`\] Implement `redirected-noqa` (`RUF101`) ([#11052](https://github.com/astral-sh/ruff/pull/11052))
- \[`pyflakes`\] Distinguish between first-party and third-party imports for fix suggestions ([#11168](https://github.com/astral-sh/ruff/pull/11168))
### Rule changes
- \[`flake8-bugbear`\] Ignore non-abstract class attributes when enforcing `B024` ([#11210](https://github.com/astral-sh/ruff/pull/11210))
- \[`flake8-logging`\] Include inline instantiations when detecting loggers ([#11154](https://github.com/astral-sh/ruff/pull/11154))
- \[`pylint`\] Also emit `PLR0206` for properties with variadic parameters ([#11200](https://github.com/astral-sh/ruff/pull/11200))
- \[`ruff`\] Detect duplicate codes as part of `unused-noqa` (`RUF100`) ([#10850](https://github.com/astral-sh/ruff/pull/10850))
### Formatter
- Avoid multiline expression if format specifier is present ([#11123](https://github.com/astral-sh/ruff/pull/11123))
### LSP
- Write `ruff server` setup guide for Helix ([#11183](https://github.com/astral-sh/ruff/pull/11183))
- `ruff server` no longer hangs after shutdown ([#11222](https://github.com/astral-sh/ruff/pull/11222))
- `ruff server` reads from a configuration TOML file in the user configuration directory if no local configuration exists ([#11225](https://github.com/astral-sh/ruff/pull/11225))
- `ruff server` respects `per-file-ignores` configuration ([#11224](https://github.com/astral-sh/ruff/pull/11224))
- `ruff server`: Support a custom TOML configuration file ([#11140](https://github.com/astral-sh/ruff/pull/11140))
- `ruff server`: Support setting to prioritize project configuration over editor configuration ([#11086](https://github.com/astral-sh/ruff/pull/11086))
### Bug fixes
- Avoid debug assertion around NFKC renames ([#11249](https://github.com/astral-sh/ruff/pull/11249))
- \[`pyflakes`\] Prioritize `redefined-while-unused` over `unused-import` ([#11173](https://github.com/astral-sh/ruff/pull/11173))
- \[`ruff`\] Respect `async` expressions in comprehension bodies ([#11219](https://github.com/astral-sh/ruff/pull/11219))
- \[`pygrep_hooks`\] Fix `blanket-noqa` panic when last line has noqa with no newline (`PGH004`) ([#11108](https://github.com/astral-sh/ruff/pull/11108))
- \[`perflint`\] Ignore list-copy recommendations for async `for` loops ([#11250](https://github.com/astral-sh/ruff/pull/11250))
- \[`pyflakes`\] Improve `invalid-print-syntax` documentation ([#11171](https://github.com/astral-sh/ruff/pull/11171))
### Performance
- Avoid allocations for isort module names ([#11251](https://github.com/astral-sh/ruff/pull/11251))
- Build a separate ARM wheel for macOS ([#11149](https://github.com/astral-sh/ruff/pull/11149))
## 0.4.2
### Rule changes

39
Cargo.lock generated
View File

@@ -1457,6 +1457,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
@@ -1825,9 +1835,11 @@ dependencies = [
"notify",
"parking_lot",
"rayon",
"ruff_formatter",
"ruff_index",
"ruff_notebook",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_text_size",
@@ -1943,7 +1955,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.4.3"
version = "0.4.2"
dependencies = [
"anyhow",
"argfile",
@@ -1964,6 +1976,7 @@ dependencies = [
"log",
"mimalloc",
"notify",
"num_cpus",
"path-absolutize",
"rayon",
"regex",
@@ -2104,7 +2117,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.4.3"
version = "0.4.2"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2377,6 +2390,22 @@ dependencies = [
"walkdir",
]
[[package]]
name = "ruff_shrinking"
version = "0.4.2"
dependencies = [
"anyhow",
"clap",
"fs-err",
"regex",
"ruff_python_ast",
"ruff_python_parser",
"ruff_text_size",
"shlex",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "ruff_source_file"
version = "0.0.0"
@@ -2702,6 +2731,12 @@ dependencies = [
"dirs 5.0.1",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "similar"
version = "2.5.0"

View File

@@ -66,6 +66,7 @@ memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
num_cpus = { version = "1.16.0" }
once_cell = { version = "1.19.0" }
path-absolutize = { version = "3.1.1" }
path-slash = { version = "0.2.1" }
@@ -90,6 +91,7 @@ serde_json = { version = "1.0.113" }
serde_test = { version = "1.0.152" }
serde_with = { version = "3.6.0", default-features = false, features = ["macros"] }
shellexpand = { version = "3.0.0" }
shlex = { version = "1.3.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.2" }
static_assertions = "1.1.0"

View File

@@ -152,7 +152,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.3
rev: v0.4.2
hooks:
# Run the linter.
- id: ruff

View File

@@ -3,10 +3,5 @@ doc-valid-idents = [
"CodeQL",
"IPython",
"NumPy",
"LibCST",
"SCREAMING_SNAKE_CASE",
"SQLAlchemy",
"McCabe",
"FastAPI",
"..",
]

View File

@@ -12,12 +12,14 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_text_size = { path = "../ruff_text_size" }
ruff_formatter = { path = "../ruff_formatter" }
ruff_index = { path = "../ruff_index" }
ruff_notebook = { path = "../ruff_notebook" }
ruff_python_ast = { path = "../ruff_python_ast" }
ruff_python_formatter = { path = "../ruff_python_formatter" }
ruff_python_parser = { path = "../ruff_python_parser" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_text_size = { path = "../ruff_text_size" }
anyhow = { workspace = true }
bitflags = { workspace = true }

View File

@@ -57,7 +57,6 @@ pub trait ParallelDatabase: Database + Send {
/// We should avoid creating snapshots while running a query because we might want to adopt Salsa in the future (if we can figure out persistent caching).
/// Unfortunately, the infrastructure doesn't provide an automated way of knowing when a query is run, that's
/// why we have to "enforce" this constraint manually.
#[must_use]
fn snapshot(&self) -> Snapshot<Self>;
}

View File

@@ -15,9 +15,9 @@ type Map<K, V> = hashbrown::HashMap<K, V, ()>;
pub struct FileId;
// TODO we'll need a higher level virtual file system abstraction that allows testing if a file exists
// or retrieving its content (ideally lazily and in a way that the memory can be retained later)
// I suspect that we'll end up with a FileSystem trait and our own Path abstraction.
#[derive(Default)]
// or retrieving its content (ideally lazily and in a way that the memory can be retained later)
// I suspect that we'll end up with a FileSystem trait and our own Path abstraction.
#[derive(Clone, Default)]
pub struct Files {
inner: Arc<RwLock<FilesInner>>,
}
@@ -36,16 +36,6 @@ impl Files {
pub fn path(&self, id: FileId) -> Arc<Path> {
self.inner.read().path(id)
}
/// Snapshots files for a new database snapshot.
///
/// This method should not be used outside a database snapshot.
#[must_use]
pub fn snapshot(&self) -> Files {
Files {
inner: self.inner.clone(),
}
}
}
impl Debug for Files {
@@ -73,7 +63,7 @@ struct FilesInner {
by_path: Map<FileId, ()>,
// TODO should we use a map here to reclaim the space for removed files?
// TODO I think we should use our own path abstraction here to avoid having to normalize paths
// and dealing with non-utf paths everywhere.
// and dealing with non-utf paths everywhere.
by_id: IndexVec<FileId, Arc<Path>>,
}

View File

@@ -0,0 +1,135 @@
use std::ops::{Deref, DerefMut};
use ruff_formatter::PrintedRange;
use ruff_python_formatter::{FormatModuleError, PyFormatOptions};
use ruff_text_size::TextRange;
use crate::cache::KeyValueCache;
use crate::db::{HasJar, QueryError, SourceDb};
use crate::files::FileId;
use crate::lint::Diagnostics;
use crate::FxDashSet;
pub(crate) trait FormatDb: SourceDb {
/// Formats a file and returns its formatted content or an indicator that it is unchanged.
fn format_file(&self, file_id: FileId) -> Result<FormattedFile, FormatError>;
/// Formats a range in a file.
fn format_file_range(
&self,
file_id: FileId,
range: TextRange,
) -> Result<PrintedRange, FormatError>;
fn check_file_formatted(&self, file_id: FileId) -> Result<Diagnostics, FormatError>;
}
#[tracing::instrument(level = "trace", skip(db))]
pub(crate) fn format_file<Db>(db: &Db, file_id: FileId) -> Result<FormattedFile, FormatError>
where
Db: FormatDb + HasJar<FormatJar>,
{
let formatted = &db.jar()?.formatted;
if formatted.contains(&file_id) {
return Ok(FormattedFile::Unchanged);
}
let source = db.source(file_id)?;
// TODO use the `format_module` method here to re-use the AST.
let printed =
ruff_python_formatter::format_module_source(source.text(), PyFormatOptions::default())?;
Ok(if printed.as_code() == source.text() {
formatted.insert(file_id);
FormattedFile::Unchanged
} else {
FormattedFile::Formatted(printed.into_code())
})
}
#[tracing::instrument(level = "trace", skip(db))]
pub(crate) fn format_file_range<Db: FormatDb + HasJar<FormatJar>>(
db: &Db,
file_id: FileId,
range: TextRange,
) -> Result<PrintedRange, FormatError> {
let formatted = &db.jar()?.formatted;
let source = db.source(file_id)?;
if formatted.contains(&file_id) {
return Ok(PrintedRange::new(source.text()[range].into(), range));
}
// TODO use the `format_module` method here to re-use the AST.
let result =
ruff_python_formatter::format_range(source.text(), range, PyFormatOptions::default())?;
Ok(result)
}
/// Checks if the file is correctly formatted. It creates a diagnostic for formatting issues.
#[tracing::instrument(level = "trace", skip(db))]
pub(crate) fn check_formatted<Db>(db: &Db, file_id: FileId) -> Result<Diagnostics, FormatError>
where
Db: FormatDb + HasJar<FormatJar>,
{
Ok(if db.format_file(file_id)?.is_unchanged() {
Diagnostics::Empty
} else {
Diagnostics::from(vec!["File is not formatted".to_string()])
})
}
#[derive(Debug)]
pub(crate) enum FormatError {
Format(FormatModuleError),
Query(QueryError),
}
impl From<FormatModuleError> for FormatError {
fn from(value: FormatModuleError) -> Self {
Self::Format(value)
}
}
impl From<QueryError> for FormatError {
fn from(value: QueryError) -> Self {
Self::Query(value)
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub(crate) enum FormattedFile {
Formatted(String),
Unchanged,
}
impl FormattedFile {
pub(crate) const fn is_unchanged(&self) -> bool {
matches!(self, FormattedFile::Unchanged)
}
}
#[derive(Debug, Default)]
pub struct FormatJar {
pub formatted: FxDashSet<FileId>,
}
#[derive(Default, Debug)]
pub(crate) struct FormattedStorage(KeyValueCache<FileId, ()>);
impl Deref for FormattedStorage {
type Target = KeyValueCache<FileId, ()>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FormattedStorage {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@@ -12,6 +12,7 @@ pub mod cache;
pub mod cancellation;
pub mod db;
pub mod files;
mod format;
pub mod hir;
pub mod lint;
pub mod module;

View File

@@ -1,9 +1,11 @@
#![allow(clippy::dbg_macro)]
use std::collections::hash_map::Entry;
use std::path::Path;
use std::sync::Mutex;
use crossbeam::channel as crossbeam_channel;
use rustc_hash::FxHashMap;
use tracing::subscriber::Interest;
use tracing::{Level, Metadata};
use tracing_subscriber::filter::LevelFilter;
@@ -11,10 +13,13 @@ use tracing_subscriber::layer::{Context, Filter, SubscriberExt};
use tracing_subscriber::{Layer, Registry};
use tracing_tree::time::Uptime;
use red_knot::db::{HasJar, ParallelDatabase, QueryError, SemanticDb, SourceDb, SourceJar};
use red_knot::db::{
Database, HasJar, ParallelDatabase, QueryError, SemanticDb, SourceDb, SourceJar,
};
use red_knot::files::FileId;
use red_knot::module::{ModuleSearchPath, ModuleSearchPathKind};
use red_knot::program::check::ExecutionMode;
use red_knot::program::{FileWatcherChange, Program};
use red_knot::program::{FileChange, FileChangeKind, Program};
use red_knot::watch::FileWatcher;
use red_knot::Workspace;
@@ -69,9 +74,12 @@ fn main() -> anyhow::Result<()> {
let file_changes_notifier = main_loop.file_changes_notifier();
// Watch for file changes and re-trigger the analysis.
let mut file_watcher = FileWatcher::new(move |changes| {
file_changes_notifier.notify(changes);
})?;
let mut file_watcher = FileWatcher::new(
move |changes| {
file_changes_notifier.notify(changes);
},
program.files().clone(),
)?;
file_watcher.watch_folder(workspace_folder)?;
@@ -132,26 +140,32 @@ impl MainLoop {
match message {
MainLoopMessage::CheckProgram { revision } => {
let program = program.snapshot();
let sender = self.orchestrator_sender.clone();
{
let program = program.snapshot();
let sender = self.orchestrator_sender.clone();
// Spawn a new task that checks the program. This needs to be done in a separate thread
// to prevent blocking the main loop here.
rayon::spawn(move || match program.check(ExecutionMode::ThreadPool) {
Ok(result) => {
sender
.send(OrchestratorMessage::CheckProgramCompleted {
diagnostics: result,
revision,
})
.unwrap();
}
Err(QueryError::Cancelled) => {}
});
// Spawn a new task that checks the program. This needs to be done in a separate thread
// to prevent blocking the main loop here.
rayon::spawn(move || match program.check(ExecutionMode::ThreadPool) {
Ok(result) => {
sender
.send(OrchestratorMessage::CheckProgramCompleted {
diagnostics: result,
revision,
})
.unwrap();
}
Err(QueryError::Cancelled) => {}
});
}
if !program.is_cancelled() {
let _ = program.format();
}
}
MainLoopMessage::ApplyChanges(changes) => {
// Automatically cancels any pending queries and waits for them to complete.
program.apply_changes(changes);
program.apply_changes(changes.iter());
}
MainLoopMessage::CheckCompleted(diagnostics) => {
dbg!(diagnostics);
@@ -178,7 +192,7 @@ struct FileChangesNotifier {
}
impl FileChangesNotifier {
fn notify(&self, changes: Vec<FileWatcherChange>) {
fn notify(&self, changes: Vec<FileChange>) {
self.sender
.send(OrchestratorMessage::FileChanges(changes))
.unwrap();
@@ -244,7 +258,10 @@ impl Orchestrator {
}
}
fn debounce_changes(&self, mut changes: Vec<FileWatcherChange>) {
fn debounce_changes(&self, changes: Vec<FileChange>) {
let mut aggregated_changes = AggregatedChanges::default();
aggregated_changes.extend(changes);
loop {
// Consume possibly incoming file change messages before running a new analysis, but don't wait for more than 100ms.
crossbeam_channel::select! {
@@ -254,7 +271,7 @@ impl Orchestrator {
return self.shutdown();
}
Ok(OrchestratorMessage::FileChanges(file_changes)) => {
changes.extend(file_changes);
aggregated_changes.extend(file_changes);
}
Ok(OrchestratorMessage::CheckProgramCompleted { .. })=> {
@@ -270,7 +287,7 @@ impl Orchestrator {
},
default(std::time::Duration::from_millis(10)) => {
// No more file changes after 10 ms, send the changes and schedule a new analysis
self.sender.send(MainLoopMessage::ApplyChanges(changes)).unwrap();
self.sender.send(MainLoopMessage::ApplyChanges(aggregated_changes)).unwrap();
self.sender.send(MainLoopMessage::CheckProgram { revision: self.revision}).unwrap();
return;
}
@@ -289,7 +306,7 @@ impl Orchestrator {
enum MainLoopMessage {
CheckProgram { revision: usize },
CheckCompleted(Vec<String>),
ApplyChanges(Vec<FileWatcherChange>),
ApplyChanges(AggregatedChanges),
Exit,
}
@@ -303,7 +320,77 @@ enum OrchestratorMessage {
revision: usize,
},
FileChanges(Vec<FileWatcherChange>),
FileChanges(Vec<FileChange>),
}
#[derive(Default, Debug)]
struct AggregatedChanges {
changes: FxHashMap<FileId, FileChangeKind>,
}
impl AggregatedChanges {
fn add(&mut self, change: FileChange) {
match self.changes.entry(change.file_id()) {
Entry::Occupied(mut entry) => {
let merged = entry.get_mut();
match (merged, change.kind()) {
(FileChangeKind::Created, FileChangeKind::Deleted) => {
// Deletion after creations means that ruff never saw the file.
entry.remove();
}
(FileChangeKind::Created, FileChangeKind::Modified) => {
// No-op, for ruff, modifying a file that it doesn't yet know that it exists is still considered a creation.
}
(FileChangeKind::Modified, FileChangeKind::Created) => {
// Uhh, that should probably not happen. Continue considering it a modification.
}
(FileChangeKind::Modified, FileChangeKind::Deleted) => {
*entry.get_mut() = FileChangeKind::Deleted;
}
(FileChangeKind::Deleted, FileChangeKind::Created) => {
*entry.get_mut() = FileChangeKind::Modified;
}
(FileChangeKind::Deleted, FileChangeKind::Modified) => {
// That's weird, but let's consider it a modification.
*entry.get_mut() = FileChangeKind::Modified;
}
(FileChangeKind::Created, FileChangeKind::Created)
| (FileChangeKind::Modified, FileChangeKind::Modified)
| (FileChangeKind::Deleted, FileChangeKind::Deleted) => {
// No-op transitions. Some of them should be impossible but we handle them anyway.
}
}
}
Entry::Vacant(entry) => {
entry.insert(change.kind());
}
}
}
fn extend<I>(&mut self, changes: I)
where
I: IntoIterator<Item = FileChange>,
I::IntoIter: ExactSizeIterator,
{
let iter = changes.into_iter();
self.changes.reserve(iter.len());
for change in iter {
self.add(change);
}
}
fn iter(&self) -> impl Iterator<Item = FileChange> + '_ {
self.changes
.iter()
.map(|(id, kind)| FileChange::new(*id, *kind))
}
}
fn setup_tracing() {

View File

@@ -3,6 +3,7 @@ use rustc_hash::FxHashSet;
use crate::db::{Database, LintDb, QueryError, QueryResult, SemanticDb};
use crate::files::FileId;
use crate::format::{FormatDb, FormatError};
use crate::lint::Diagnostics;
use crate::program::Program;
use crate::symbols::Dependency;
@@ -64,6 +65,18 @@ impl Program {
if self.workspace().is_file_open(file) {
diagnostics.extend_from_slice(&self.lint_syntax(file)?);
diagnostics.extend_from_slice(&self.lint_semantic(file)?);
match self.check_file_formatted(file) {
Ok(format_diagnostics) => {
diagnostics.extend_from_slice(&format_diagnostics);
}
Err(FormatError::Query(err)) => {
return Err(err);
}
Err(FormatError::Format(error)) => {
diagnostics.push(format!("Error formatting file: {error}"));
}
}
}
Ok(Diagnostics::from(diagnostics))

View File

@@ -0,0 +1,44 @@
use crate::db::{QueryResult, SourceDb};
use crate::format::{FormatDb, FormatError, FormattedFile};
use crate::program::Program;
impl Program {
#[tracing::instrument(level = "trace", skip(self))]
pub fn format(&mut self) -> QueryResult<()> {
// Formats all open files
// TODO make `Executor` from `check` reusable.
for file in self.workspace.open_files() {
match self.format_file(file) {
Ok(FormattedFile::Formatted(content)) => {
let path = self.file_path(file);
// TODO: This is problematic because it immediately re-triggers the file watcher.
// A possible solution is to track the self "inflicted" changes inside of programs
// by tracking the file revision right after the write. It could then use the revision
// to determine which changes are safe to ignore (and in which context).
// An other alternative is to not write as part of the `format` command and instead
// return a Vec with the format results and leave the writing to the caller.
// I think that's undesired because a) we still need a way to tell the formatter
// that it won't be necessary to format the content again and
// b) it would reduce concurrency because the writing would need to wait for the file
// formatting to be complete, unless we use some form of communication channel.
std::fs::write(path, content).expect("Unable to write file");
}
Ok(FormattedFile::Unchanged) => {
// No op
}
Err(FormatError::Query(error)) => {
return Err(error);
}
Err(FormatError::Format(error)) => {
// TODO proper error handling. We should either propagate this error or
// emit a diagnostic (probably this).
tracing::warn!("Failed to format file: {}", error);
}
}
}
Ok(())
}
}

View File

@@ -1,14 +1,17 @@
use std::collections::hash_map::Entry;
use std::path::{Path, PathBuf};
use ruff_formatter::PrintedRange;
use ruff_text_size::TextRange;
use std::path::Path;
use std::sync::Arc;
use rustc_hash::FxHashMap;
use crate::db::{
Database, Db, DbRuntime, HasJar, HasJars, JarsStorage, LintDb, LintJar, ParallelDatabase,
QueryResult, SemanticDb, SemanticJar, Snapshot, SourceDb, SourceJar,
};
use crate::files::{FileId, Files};
use crate::format::{
check_formatted, format_file, format_file_range, FormatDb, FormatError, FormatJar,
FormattedFile,
};
use crate::lint::{lint_semantic, lint_syntax, Diagnostics};
use crate::module::{
add_module, file_to_module, path_to_module, resolve_module, set_module_search_paths, Module,
@@ -21,6 +24,7 @@ use crate::types::{infer_symbol_type, Type};
use crate::Workspace;
pub mod check;
mod format;
#[derive(Debug)]
pub struct Program {
@@ -40,17 +44,10 @@ impl Program {
pub fn apply_changes<I>(&mut self, changes: I)
where
I: IntoIterator<Item = FileWatcherChange>,
I: IntoIterator<Item = FileChange>,
{
let mut aggregated_changes = AggregatedChanges::default();
aggregated_changes.extend(changes.into_iter().map(|change| FileChange {
id: self.files.intern(&change.path),
kind: change.kind,
}));
let (source, semantic, lint) = self.jars_mut();
for change in aggregated_changes.iter() {
let (source, semantic, lint, format) = self.jars_mut();
for change in changes {
semantic.module_resolver.remove_module(change.id);
semantic.symbol_tables.remove(&change.id);
source.sources.remove(&change.id);
@@ -59,6 +56,7 @@ impl Program {
semantic.type_store.remove_module(change.id);
lint.lint_syntax.remove(&change.id);
lint.lint_semantic.remove(&change.id);
format.formatted.remove(&change.id);
}
}
@@ -134,6 +132,24 @@ impl LintDb for Program {
}
}
impl FormatDb for Program {
fn format_file(&self, file_id: FileId) -> Result<FormattedFile, FormatError> {
format_file(self, file_id)
}
fn format_file_range(
&self,
file_id: FileId,
range: TextRange,
) -> Result<PrintedRange, FormatError> {
format_file_range(self, file_id, range)
}
fn check_file_formatted(&self, file_id: FileId) -> Result<Diagnostics, FormatError> {
check_formatted(self, file_id)
}
}
impl Db for Program {}
impl Database for Program {
@@ -150,14 +166,14 @@ impl ParallelDatabase for Program {
fn snapshot(&self) -> Snapshot<Self> {
Snapshot::new(Self {
jars: self.jars.snapshot(),
files: self.files.snapshot(),
files: self.files.clone(),
workspace: self.workspace.clone(),
})
}
}
impl HasJars for Program {
type Jars = (SourceJar, SemanticJar, LintJar);
type Jars = (SourceJar, SemanticJar, LintJar, FormatJar);
fn jars(&self) -> QueryResult<&Self::Jars> {
self.jars.jars()
@@ -198,30 +214,32 @@ impl HasJar<LintJar> for Program {
}
}
#[derive(Clone, Debug)]
pub struct FileWatcherChange {
path: PathBuf,
kind: FileChangeKind,
}
impl HasJar<FormatJar> for Program {
fn jar(&self) -> QueryResult<&FormatJar> {
Ok(&self.jars()?.3)
}
impl FileWatcherChange {
pub fn new(path: PathBuf, kind: FileChangeKind) -> Self {
Self { path, kind }
fn jar_mut(&mut self) -> &mut FormatJar {
&mut self.jars_mut().3
}
}
#[derive(Copy, Clone, Debug)]
struct FileChange {
pub struct FileChange {
id: FileId,
kind: FileChangeKind,
}
impl FileChange {
fn file_id(self) -> FileId {
pub fn new(file_id: FileId, kind: FileChangeKind) -> Self {
Self { id: file_id, kind }
}
pub fn file_id(&self) -> FileId {
self.id
}
fn kind(self) -> FileChangeKind {
pub fn kind(&self) -> FileChangeKind {
self.kind
}
}
@@ -232,74 +250,3 @@ pub enum FileChangeKind {
Modified,
Deleted,
}
#[derive(Default, Debug)]
struct AggregatedChanges {
changes: FxHashMap<FileId, FileChangeKind>,
}
impl AggregatedChanges {
fn add(&mut self, change: FileChange) {
match self.changes.entry(change.file_id()) {
Entry::Occupied(mut entry) => {
let merged = entry.get_mut();
match (merged, change.kind()) {
(FileChangeKind::Created, FileChangeKind::Deleted) => {
// Deletion after creations means that ruff never saw the file.
entry.remove();
}
(FileChangeKind::Created, FileChangeKind::Modified) => {
// No-op, for ruff, modifying a file that it doesn't yet know that it exists is still considered a creation.
}
(FileChangeKind::Modified, FileChangeKind::Created) => {
// Uhh, that should probably not happen. Continue considering it a modification.
}
(FileChangeKind::Modified, FileChangeKind::Deleted) => {
*entry.get_mut() = FileChangeKind::Deleted;
}
(FileChangeKind::Deleted, FileChangeKind::Created) => {
*entry.get_mut() = FileChangeKind::Modified;
}
(FileChangeKind::Deleted, FileChangeKind::Modified) => {
// That's weird, but let's consider it a modification.
*entry.get_mut() = FileChangeKind::Modified;
}
(FileChangeKind::Created, FileChangeKind::Created)
| (FileChangeKind::Modified, FileChangeKind::Modified)
| (FileChangeKind::Deleted, FileChangeKind::Deleted) => {
// No-op transitions. Some of them should be impossible but we handle them anyway.
}
}
}
Entry::Vacant(entry) => {
entry.insert(change.kind());
}
}
}
fn extend<I>(&mut self, changes: I)
where
I: IntoIterator<Item = FileChange>,
{
let iter = changes.into_iter();
let (lower, _) = iter.size_hint();
self.changes.reserve(lower);
for change in iter {
self.add(change);
}
}
fn iter(&self) -> impl Iterator<Item = FileChange> + '_ {
self.changes.iter().map(|(id, kind)| FileChange {
id: *id,
kind: *kind,
})
}
}

View File

@@ -68,7 +68,7 @@ pub(crate) struct Scope {
name: Name,
kind: ScopeKind,
child_scopes: Vec<ScopeId>,
/// symbol IDs, hashed by symbol name
// symbol IDs, hashed by symbol name
symbols_by_name: Map<SymbolId, ()>,
}
@@ -107,7 +107,6 @@ bitflags! {
pub(crate) struct Symbol {
name: Name,
flags: SymbolFlags,
scope_id: ScopeId,
// kind: Kind,
}
@@ -142,7 +141,7 @@ pub(crate) enum Definition {
// the small amount of information we need from the AST.
Import(ImportDefinition),
ImportFrom(ImportFromDefinition),
ClassDef(ClassDefinition),
ClassDef(TypedNodeKey<ast::StmtClassDef>),
FunctionDef(TypedNodeKey<ast::StmtFunctionDef>),
Assignment(TypedNodeKey<ast::StmtAssign>),
AnnotatedAssignment(TypedNodeKey<ast::StmtAnnAssign>),
@@ -175,12 +174,6 @@ impl ImportFromDefinition {
}
}
#[derive(Clone, Debug)]
pub(crate) struct ClassDefinition {
pub(crate) node_key: TypedNodeKey<ast::StmtClassDef>,
pub(crate) scope_id: ScopeId,
}
#[derive(Debug, Clone)]
pub enum Dependency {
Module(ModuleName),
@@ -339,11 +332,7 @@ impl SymbolTable {
*entry.key()
}
RawEntryMut::Vacant(entry) => {
let id = self.symbols_by_id.push(Symbol {
name,
flags,
scope_id,
});
let id = self.symbols_by_id.push(Symbol { name, flags });
entry.insert_with_hasher(hash, id, (), |_| hash);
id
}
@@ -470,8 +459,8 @@ impl SymbolTableBuilder {
symbol_id
}
fn push_scope(&mut self, name: &str, kind: ScopeKind) -> ScopeId {
let scope_id = self.table.add_child_scope(self.cur_scope(), name, kind);
fn push_scope(&mut self, child_of: ScopeId, name: &str, kind: ScopeKind) -> ScopeId {
let scope_id = self.table.add_child_scope(child_of, name, kind);
self.scopes.push(scope_id);
scope_id
}
@@ -493,10 +482,10 @@ impl SymbolTableBuilder {
&mut self,
name: &str,
params: &Option<Box<ast::TypeParams>>,
nested: impl FnOnce(&mut Self) -> ScopeId,
) -> ScopeId {
nested: impl FnOnce(&mut Self),
) {
if let Some(type_params) = params {
self.push_scope(name, ScopeKind::Annotation);
self.push_scope(self.cur_scope(), name, ScopeKind::Annotation);
for type_param in &type_params.type_params {
let name = match type_param {
ast::TypeParam::TypeVar(ast::TypeParamTypeVar { name, .. }) => name,
@@ -506,11 +495,10 @@ impl SymbolTableBuilder {
self.add_or_update_symbol(name, SymbolFlags::IS_DEFINED);
}
}
let scope_id = nested(self);
nested(self);
if params.is_some() {
self.pop_scope();
}
scope_id
}
}
@@ -537,26 +525,21 @@ impl PreorderVisitor<'_> for SymbolTableBuilder {
// TODO need to capture more definition statements here
match stmt {
ast::Stmt::ClassDef(node) => {
let scope_id = self.with_type_params(&node.name, &node.type_params, |builder| {
let scope_id = builder.push_scope(&node.name, ScopeKind::Class);
let def = Definition::ClassDef(TypedNodeKey::from_node(node));
self.add_or_update_symbol_with_def(&node.name, def);
self.with_type_params(&node.name, &node.type_params, |builder| {
builder.push_scope(builder.cur_scope(), &node.name, ScopeKind::Class);
ast::visitor::preorder::walk_stmt(builder, stmt);
builder.pop_scope();
scope_id
});
let def = Definition::ClassDef(ClassDefinition {
node_key: TypedNodeKey::from_node(node),
scope_id,
});
self.add_or_update_symbol_with_def(&node.name, def);
}
ast::Stmt::FunctionDef(node) => {
let def = Definition::FunctionDef(TypedNodeKey::from_node(node));
self.add_or_update_symbol_with_def(&node.name, def);
self.with_type_params(&node.name, &node.type_params, |builder| {
let scope_id = builder.push_scope(&node.name, ScopeKind::Function);
builder.push_scope(builder.cur_scope(), &node.name, ScopeKind::Function);
ast::visitor::preorder::walk_stmt(builder, stmt);
builder.pop_scope();
scope_id
});
}
ast::Stmt::Import(ast::StmtImport { names, .. }) => {

View File

@@ -1,8 +1,7 @@
#![allow(dead_code)]
use crate::ast_ids::NodeKey;
use crate::db::{HasJar, QueryResult, SemanticDb, SemanticJar};
use crate::files::FileId;
use crate::symbols::{ScopeId, SymbolId};
use crate::symbols::SymbolId;
use crate::{FxDashMap, FxIndexSet, Name};
use ruff_index::{newtype_index, IndexVec};
use rustc_hash::FxHashMap;
@@ -120,20 +119,12 @@ impl TypeStore {
self.modules.get(&file_id)
}
fn add_function(&self, file_id: FileId, name: &str, decorators: Vec<Type>) -> FunctionTypeId {
self.add_or_get_module(file_id)
.add_function(name, decorators)
fn add_function(&self, file_id: FileId, name: &str) -> FunctionTypeId {
self.add_or_get_module(file_id).add_function(name)
}
fn add_class(
&self,
file_id: FileId,
name: &str,
scope_id: ScopeId,
bases: Vec<Type>,
) -> ClassTypeId {
self.add_or_get_module(file_id)
.add_class(name, scope_id, bases)
fn add_class(&self, file_id: FileId, name: &str, bases: Vec<Type>) -> ClassTypeId {
self.add_or_get_module(file_id).add_class(name, bases)
}
fn add_union(&mut self, file_id: FileId, elems: &[Type]) -> UnionTypeId {
@@ -261,24 +252,6 @@ pub struct ClassTypeId {
class_id: ModuleClassTypeId,
}
impl ClassTypeId {
fn get_own_class_member<Db>(self, db: &Db, name: &Name) -> QueryResult<Option<Type>>
where
Db: SemanticDb + HasJar<SemanticJar>,
{
// TODO: this should distinguish instance-only members (e.g. `x: int`) and not return them
let ClassType { scope_id, .. } = *db.jar()?.type_store.get_class(self);
let table = db.symbol_table(self.file_id)?;
if let Some(symbol_id) = table.symbol_id_by_name(scope_id, name) {
Ok(Some(db.infer_symbol_type(self.file_id, symbol_id)?))
} else {
Ok(None)
}
}
// TODO: get_own_instance_member, get_class_member, get_instance_member
}
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct UnionTypeId {
file_id: FileId,
@@ -333,10 +306,9 @@ impl ModuleTypeStore {
}
}
fn add_function(&mut self, name: &str, decorators: Vec<Type>) -> FunctionTypeId {
fn add_function(&mut self, name: &str) -> FunctionTypeId {
let func_id = self.functions.push(FunctionType {
name: Name::new(name),
decorators,
});
FunctionTypeId {
file_id: self.file_id,
@@ -344,10 +316,9 @@ impl ModuleTypeStore {
}
}
fn add_class(&mut self, name: &str, scope_id: ScopeId, bases: Vec<Type>) -> ClassTypeId {
fn add_class(&mut self, name: &str, bases: Vec<Type>) -> ClassTypeId {
let class_id = self.classes.push(ClassType {
name: Name::new(name),
scope_id,
// TODO: if no bases are given, that should imply [object]
bases,
});
@@ -432,11 +403,7 @@ impl std::fmt::Display for DisplayType<'_> {
#[derive(Debug)]
pub(crate) struct ClassType {
/// Name of the class at definition
name: Name,
/// `ScopeId` of the class body
pub(crate) scope_id: ScopeId,
/// Types of all class bases
bases: Vec<Type>,
}
@@ -453,17 +420,12 @@ impl ClassType {
#[derive(Debug)]
pub(crate) struct FunctionType {
name: Name,
decorators: Vec<Type>,
}
impl FunctionType {
fn name(&self) -> &str {
self.name.as_str()
}
fn decorators(&self) -> &[Type] {
self.decorators.as_slice()
}
}
#[derive(Debug)]
@@ -527,7 +489,6 @@ impl IntersectionType {
#[cfg(test)]
mod tests {
use crate::files::Files;
use crate::symbols::SymbolTable;
use crate::types::{Type, TypeStore};
use crate::FxIndexSet;
use std::path::Path;
@@ -537,7 +498,7 @@ mod tests {
let store = TypeStore::default();
let files = Files::default();
let file_id = files.intern(Path::new("/foo"));
let id = store.add_class(file_id, "C", SymbolTable::root_scope_id(), Vec::new());
let id = store.add_class(file_id, "C", Vec::new());
assert_eq!(store.get_class(id).name(), "C");
let inst = Type::Instance(id);
assert_eq!(format!("{}", inst.display(&store)), "C");
@@ -548,9 +509,8 @@ mod tests {
let store = TypeStore::default();
let files = Files::default();
let file_id = files.intern(Path::new("/foo"));
let id = store.add_function(file_id, "func", vec![Type::Unknown]);
let id = store.add_function(file_id, "func");
assert_eq!(store.get_function(id).name(), "func");
assert_eq!(store.get_function(id).decorators(), vec![Type::Unknown]);
let func = Type::Function(id);
assert_eq!(format!("{}", func.display(&store)), "func");
}
@@ -560,8 +520,8 @@ mod tests {
let mut store = TypeStore::default();
let files = Files::default();
let file_id = files.intern(Path::new("/foo"));
let c1 = store.add_class(file_id, "C1", SymbolTable::root_scope_id(), Vec::new());
let c2 = store.add_class(file_id, "C2", SymbolTable::root_scope_id(), Vec::new());
let c1 = store.add_class(file_id, "C1", Vec::new());
let c2 = store.add_class(file_id, "C2", Vec::new());
let elems = vec![Type::Instance(c1), Type::Instance(c2)];
let id = store.add_union(file_id, &elems);
assert_eq!(
@@ -577,9 +537,9 @@ mod tests {
let mut store = TypeStore::default();
let files = Files::default();
let file_id = files.intern(Path::new("/foo"));
let c1 = store.add_class(file_id, "C1", SymbolTable::root_scope_id(), Vec::new());
let c2 = store.add_class(file_id, "C2", SymbolTable::root_scope_id(), Vec::new());
let c3 = store.add_class(file_id, "C3", SymbolTable::root_scope_id(), Vec::new());
let c1 = store.add_class(file_id, "C1", Vec::new());
let c2 = store.add_class(file_id, "C2", Vec::new());
let c3 = store.add_class(file_id, "C3", Vec::new());
let pos = vec![Type::Instance(c1), Type::Instance(c2)];
let neg = vec![Type::Instance(c3)];
let id = store.add_intersection(file_id, &pos, &neg);

View File

@@ -4,7 +4,7 @@ use ruff_python_ast::AstNode;
use crate::db::{HasJar, QueryResult, SemanticDb, SemanticJar};
use crate::module::ModuleName;
use crate::symbols::{ClassDefinition, Definition, ImportFromDefinition, SymbolId};
use crate::symbols::{Definition, ImportFromDefinition, SymbolId};
use crate::types::Type;
use crate::FileId;
use ruff_python_ast as ast;
@@ -51,7 +51,7 @@ where
Type::Unknown
}
}
Definition::ClassDef(ClassDefinition { node_key, scope_id }) => {
Definition::ClassDef(node_key) => {
if let Some(ty) = type_store.get_cached_node_type(file_id, node_key.erased()) {
ty
} else {
@@ -65,8 +65,7 @@ where
bases.push(infer_expr_type(db, file_id, base)?);
}
let ty =
Type::Class(type_store.add_class(file_id, &node.name.id, *scope_id, bases));
let ty = Type::Class(type_store.add_class(file_id, &node.name.id, bases));
type_store.cache_node_type(file_id, *node_key.erased(), ty);
ty
}
@@ -81,15 +80,7 @@ where
.resolve(ast.as_any_node_ref())
.expect("node key should resolve");
let decorator_tys = node
.decorator_list
.iter()
.map(|decorator| infer_expr_type(db, file_id, &decorator.expression))
.collect::<QueryResult<_>>()?;
let ty = type_store
.add_function(file_id, &node.name.id, decorator_tys)
.into();
let ty = type_store.add_function(file_id, &node.name.id).into();
type_store.cache_node_type(file_id, *node_key.erased(), ty);
ty
}
@@ -134,7 +125,6 @@ mod tests {
use crate::db::{HasJar, SemanticDb, SemanticJar};
use crate::module::{ModuleName, ModuleSearchPath, ModuleSearchPathKind};
use crate::types::Type;
use crate::Name;
// TODO with virtual filesystem we shouldn't have to write files to disk for these
// tests
@@ -224,42 +214,4 @@ mod tests {
Ok(())
}
#[test]
fn resolve_method() -> anyhow::Result<()> {
let case = create_test()?;
let db = &case.db;
let path = case.src.path().join("mod.py");
std::fs::write(path, "class C:\n def f(self): pass")?;
let file = db
.resolve_module(ModuleName::new("mod"))?
.expect("module should be found")
.path(db)?
.file();
let syms = db.symbol_table(file)?;
let sym = syms
.root_symbol_id_by_name("C")
.expect("C symbol should be found");
let ty = db.infer_symbol_type(file, sym)?;
let Type::Class(class_id) = ty else {
panic!("C is not a Class");
};
let member_ty = class_id
.get_own_class_member(db, &Name::new("f"))
.expect("C.f to resolve");
let Some(Type::Function(func_id)) = member_ty else {
panic!("C.f is not a Function");
};
let jar = HasJar::<SemanticJar>::jar(db)?;
let function = jar.type_store.get_function(func_id);
assert_eq!(function.name(), "f");
Ok(())
}
}

View File

@@ -1,38 +1,38 @@
use anyhow::Context;
use std::path::Path;
use anyhow::Context;
use crate::files::Files;
use crate::program::{FileChange, FileChangeKind};
use notify::event::{CreateKind, RemoveKind};
use notify::{recommended_watcher, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use crate::program::{FileChangeKind, FileWatcherChange};
pub struct FileWatcher {
watcher: RecommendedWatcher,
}
pub trait EventHandler: Send + 'static {
fn handle(&self, changes: Vec<FileWatcherChange>);
fn handle(&self, changes: Vec<FileChange>);
}
impl<F> EventHandler for F
where
F: Fn(Vec<FileWatcherChange>) + Send + 'static,
F: Fn(Vec<FileChange>) + Send + 'static,
{
fn handle(&self, changes: Vec<FileWatcherChange>) {
fn handle(&self, changes: Vec<FileChange>) {
let f = self;
f(changes);
}
}
impl FileWatcher {
pub fn new<E>(handler: E) -> anyhow::Result<Self>
pub fn new<E>(handler: E, files: Files) -> anyhow::Result<Self>
where
E: EventHandler,
{
Self::from_handler(Box::new(handler))
Self::from_handler(Box::new(handler), files)
}
fn from_handler(handler: Box<dyn EventHandler>) -> anyhow::Result<Self> {
fn from_handler(handler: Box<dyn EventHandler>, files: Files) -> anyhow::Result<Self> {
let watcher = recommended_watcher(move |changes: notify::Result<Event>| {
match changes {
Ok(event) => {
@@ -50,7 +50,8 @@ impl FileWatcher {
for path in event.paths {
if path.is_file() {
changes.push(FileWatcherChange::new(path, change_kind));
let id = files.intern(&path);
changes.push(FileChange::new(id, change_kind));
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.4.3"
version = "0.4.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -41,6 +41,7 @@ is-macro = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
notify = { workspace = true }
num_cpus = { workspace = true }
path-absolutize = { workspace = true, features = ["once_cell_cache"] }
rayon = { workspace = true }
regex = { workspace = true }

View File

@@ -338,7 +338,7 @@ pub struct CheckCommand {
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// List of mappings from file extension to language (one of `python`, `ipynb`, `pyi`). For
/// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For
/// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`.
#[arg(long, value_delimiter = ',')]
pub extension: Option<Vec<ExtensionPair>>,
@@ -466,7 +466,7 @@ pub struct FormatCommand {
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// List of mappings from file extension to language (one of `python`, `ipynb`, `pyi`). For
/// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For
/// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`.
#[arg(long, value_delimiter = ',')]
pub extension: Option<Vec<ExtensionPair>>,

View File

@@ -23,6 +23,7 @@ use ruff_linter::message::Message;
use ruff_linter::{warn_user, VERSION};
use ruff_macros::CacheKey;
use ruff_notebook::NotebookIndex;
use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::Resolver;
@@ -347,7 +348,7 @@ impl FileCache {
} else {
FxHashMap::default()
};
Diagnostics::new(messages, notebook_indexes)
Diagnostics::new(messages, lint.imports.clone(), notebook_indexes)
})
}
}
@@ -393,7 +394,7 @@ pub(crate) fn init(path: &Path) -> Result<()> {
#[derive(Deserialize, Debug, Serialize, PartialEq)]
pub(crate) struct LintCacheData {
/// Imports made.
// pub(super) imports: ImportMap,
pub(super) imports: ImportMap,
/// Diagnostic messages.
pub(super) messages: Vec<CacheMessage>,
/// Source code of the file.
@@ -409,6 +410,7 @@ pub(crate) struct LintCacheData {
impl LintCacheData {
pub(crate) fn from_messages(
messages: &[Message],
imports: ImportMap,
notebook_index: Option<NotebookIndex>,
) -> Self {
let source = if let Some(msg) = messages.first() {
@@ -436,6 +438,7 @@ impl LintCacheData {
.collect();
Self {
imports,
messages,
source,
notebook_index,

View File

@@ -17,6 +17,7 @@ use ruff_linter::registry::Rule;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::{fs, warn_user_once, IOError};
use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::{
@@ -133,6 +134,7 @@ pub(crate) fn check(
dummy,
TextSize::default(),
)],
ImportMap::default(),
FxHashMap::default(),
)
} else {

View File

@@ -23,6 +23,7 @@ use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::{fs, IOError, SyntaxError};
use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
@@ -34,17 +35,20 @@ use crate::cache::{Cache, FileCacheKey, LintCacheData};
pub(crate) struct Diagnostics {
pub(crate) messages: Vec<Message>,
pub(crate) fixed: FixMap,
pub(crate) imports: ImportMap,
pub(crate) notebook_indexes: FxHashMap<String, NotebookIndex>,
}
impl Diagnostics {
pub(crate) fn new(
messages: Vec<Message>,
imports: ImportMap,
notebook_indexes: FxHashMap<String, NotebookIndex>,
) -> Self {
Self {
messages,
fixed: FixMap::default(),
imports,
notebook_indexes,
}
}
@@ -88,6 +92,7 @@ impl Diagnostics {
dummy,
TextSize::default(),
)],
ImportMap::default(),
FxHashMap::default(),
)
} else {
@@ -122,6 +127,7 @@ impl Add for Diagnostics {
impl AddAssign for Diagnostics {
fn add_assign(&mut self, other: Self) {
self.messages.extend(other.messages);
self.imports.extend(other.imports);
self.fixed += other.fixed;
self.notebook_indexes.extend(other.notebook_indexes);
}
@@ -261,7 +267,7 @@ pub(crate) fn lint_path(
// Lint the file.
let (
LinterResult {
data: messages,
data: (messages, imports),
error: parse_error,
},
transformed,
@@ -329,6 +335,8 @@ pub(crate) fn lint_path(
(result, transformed, fixed)
};
let imports = imports.unwrap_or_default();
if let Some((cache, relative_path, key)) = caching {
// We don't cache parsing errors.
if parse_error.is_none() {
@@ -346,6 +354,7 @@ pub(crate) fn lint_path(
&key,
LintCacheData::from_messages(
&messages,
imports.clone(),
transformed.as_ipy_notebook().map(Notebook::index).cloned(),
),
);
@@ -369,6 +378,7 @@ pub(crate) fn lint_path(
Ok(Diagnostics {
messages,
fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]),
imports,
notebook_indexes,
})
}
@@ -406,7 +416,7 @@ pub(crate) fn lint_stdin(
// Lint the inputs.
let (
LinterResult {
data: messages,
data: (messages, imports),
error: parse_error,
},
transformed,
@@ -484,6 +494,8 @@ pub(crate) fn lint_stdin(
(result, transformed, fixed)
};
let imports = imports.unwrap_or_default();
if let Some(error) = parse_error {
error!(
"{}",
@@ -506,6 +518,7 @@ pub(crate) fn lint_stdin(
fs::relativize_path(path.unwrap_or_else(|| Path::new("-"))),
fixed,
)]),
imports,
notebook_indexes,
})
}

View File

@@ -214,14 +214,13 @@ fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result<ExitS
fn server(args: ServerCommand, log_level: LogLevel) -> Result<ExitStatus> {
let ServerCommand { preview } = args;
let four = NonZeroUsize::new(4).unwrap();
// by default, we set the number of worker threads to `num_cpus`, with a maximum of 4.
let worker_threads = std::thread::available_parallelism()
.unwrap_or(four)
.max(four);
commands::server::run_server(preview, worker_threads, log_level)
let worker_threads = num_cpus::get().max(4);
commands::server::run_server(
preview,
NonZeroUsize::try_from(worker_threads).expect("a non-zero worker thread count"),
log_level,
)
}
pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<ExitStatus> {

View File

@@ -344,7 +344,7 @@ pub struct RemoveSoftLinesBuffer<'a, Context> {
/// Caches the interned elements after the soft line breaks have been removed.
///
/// The `key` is the [Interned] element as it has been passed to [`Self::write_element`] or the child of another
/// The `key` is the [Interned] element as it has been passed to [Self::write_element] or the child of another
/// [Interned] element. The `value` is the matching document of the key where all soft line breaks have been removed.
///
/// It's fine to not snapshot the cache. The worst that can happen is that it holds on interned elements

View File

@@ -18,10 +18,10 @@ pub enum FormatError {
InvalidDocument(InvalidDocumentError),
/// Formatting failed because some content encountered a situation where a layout
/// choice by an enclosing [`crate::Format`] resulted in a poor layout for a child [`crate::Format`].
/// choice by an enclosing [crate::Format] resulted in a poor layout for a child [crate::Format].
///
/// It's up to an enclosing [`crate::Format`] to handle the error and pick another layout.
/// This error should not be raised if there's no outer [`crate::Format`] handling the poor layout error,
/// It's up to an enclosing [crate::Format] to handle the error and pick another layout.
/// This error should not be raised if there's no outer [crate::Format] handling the poor layout error,
/// avoiding that formatting of the whole document fails.
PoorLayout,
}

View File

@@ -19,10 +19,10 @@ use ruff_text_size::TextSize;
/// Use the helper functions like [`crate::builders::space`], [`crate::builders::soft_line_break`] etc. defined in this file to create elements.
#[derive(Clone, Eq, PartialEq)]
pub enum FormatElement {
/// A space token, see [`crate::builders::space`] for documentation.
/// A space token, see [crate::builders::space] for documentation.
Space,
/// A new line, see [`crate::builders::soft_line_break`], [`crate::builders::hard_line_break`], and [`crate::builders::soft_line_break_or_space`] for documentation.
/// A new line, see [crate::builders::soft_line_break], [crate::builders::hard_line_break], and [crate::builders::soft_line_break_or_space] for documentation.
Line(LineMode),
/// Forces the parent group to print in expanded mode.
@@ -108,13 +108,13 @@ impl std::fmt::Debug for FormatElement {
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum LineMode {
/// See [`crate::builders::soft_line_break_or_space`] for documentation.
/// See [crate::builders::soft_line_break_or_space] for documentation.
SoftOrSpace,
/// See [`crate::builders::soft_line_break`] for documentation.
/// See [crate::builders::soft_line_break] for documentation.
Soft,
/// See [`crate::builders::hard_line_break`] for documentation.
/// See [crate::builders::hard_line_break] for documentation.
Hard,
/// See [`crate::builders::empty_line`] for documentation.
/// See [crate::builders::empty_line] for documentation.
Empty,
}

View File

@@ -9,14 +9,14 @@ use std::num::NonZeroU8;
/// will be applied to all elements in between the start/end tags.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Tag {
/// Indents the content one level deeper, see [`crate::builders::indent`] for documentation and examples.
/// Indents the content one level deeper, see [crate::builders::indent] for documentation and examples.
StartIndent,
EndIndent,
/// Variant of [`TagKind::Indent`] that indents content by a number of spaces. For example, `Align(2)`
/// Variant of [TagKind::Indent] that indents content by a number of spaces. For example, `Align(2)`
/// indents any content following a line break by an additional two spaces.
///
/// Nesting (Aligns)[`TagKind::Align`] has the effect that all except the most inner align are handled as (Indent)[`TagKind::Indent`].
/// Nesting (Aligns)[TagKind::Align] has the effect that all except the most inner align are handled as (Indent)[TagKind::Indent].
StartAlign(Align),
EndAlign,
@@ -29,7 +29,7 @@ pub enum Tag {
/// - on a single line: Omitting `LineMode::Soft` line breaks and printing spaces for `LineMode::SoftOrSpace`
/// - on multiple lines: Printing all line breaks
///
/// See [`crate::builders::group`] for documentation and examples.
/// See [crate::builders::group] for documentation and examples.
StartGroup(Group),
EndGroup,
@@ -44,22 +44,22 @@ pub enum Tag {
EndConditionalGroup,
/// Allows to specify content that gets printed depending on whatever the enclosing group
/// is printed on a single line or multiple lines. See [`crate::builders::if_group_breaks`] for examples.
/// is printed on a single line or multiple lines. See [crate::builders::if_group_breaks] for examples.
StartConditionalContent(Condition),
EndConditionalContent,
/// Optimized version of [`Tag::StartConditionalContent`] for the case where some content
/// Optimized version of [Tag::StartConditionalContent] for the case where some content
/// should be indented if the specified group breaks.
StartIndentIfGroupBreaks(GroupId),
EndIndentIfGroupBreaks,
/// Concatenates multiple elements together with a given separator printed in either
/// flat or expanded mode to fill the print width. Expect that the content is a list of alternating
/// [element, separator] See [`crate::Formatter::fill`].
/// [element, separator] See [crate::Formatter::fill].
StartFill,
EndFill,
/// Entry inside of a [`Tag::StartFill`]
/// Entry inside of a [Tag::StartFill]
StartEntry,
EndEntry,
@@ -77,7 +77,7 @@ pub enum Tag {
/// Special semantic element marking the content with a label.
/// This does not directly influence how the content will be printed.
///
/// See [`crate::builders::labelled`] for documentation.
/// See [crate::builders::labelled] for documentation.
StartLabelled(LabelId),
EndLabelled,

View File

@@ -189,6 +189,27 @@ impl<'a, 'print> Queue<'a> for FitsQueue<'a, 'print> {
}
}
/// Iterator that calls [`Queue::pop`] until it reaches the end of the document.
///
/// The iterator traverses into the content of any [`FormatElement::Interned`].
pub(super) struct QueueIterator<'a, 'q, Q: Queue<'a>> {
queue: &'q mut Q,
lifetime: PhantomData<&'a ()>,
}
impl<'a, Q> Iterator for QueueIterator<'a, '_, Q>
where
Q: Queue<'a>,
{
type Item = &'a FormatElement;
fn next(&mut self) -> Option<Self::Item> {
self.queue.pop()
}
}
impl<'a, Q> FusedIterator for QueueIterator<'a, '_, Q> where Q: Queue<'a> {}
pub(super) struct QueueContentIterator<'a, 'q, Q: Queue<'a>> {
queue: &'q mut Q,
kind: TagKind,

View File

@@ -8,6 +8,9 @@ pub(super) trait Stack<T> {
/// Returns the last element if any
fn top(&self) -> Option<&T>;
/// Returns `true` if the stack is empty
fn is_empty(&self) -> bool;
}
impl<T> Stack<T> for Vec<T> {
@@ -22,6 +25,10 @@ impl<T> Stack<T> for Vec<T> {
fn top(&self) -> Option<&T> {
self.last()
}
fn is_empty(&self) -> bool {
self.is_empty()
}
}
/// A Stack that is stacked on top of another stack. Guarantees that the underlying stack remains unchanged.
@@ -73,6 +80,10 @@ where
.last()
.or_else(|| self.original.as_slice().last())
}
fn is_empty(&self) -> bool {
self.stack.is_empty() && self.original.len() == 0
}
}
#[cfg(test)]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.4.3"
version = "0.4.2"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -8,6 +8,7 @@ class Class:
pass
if False:
def extra_bad_method(this):
pass
@@ -93,7 +94,6 @@ class ModelClass:
def badstatic(foo):
pass
class SelfInArgsClass:
def self_as_argument(this, self):
pass
@@ -110,7 +110,6 @@ class SelfInArgsClass:
def self_as_kwargs(this, **self):
pass
class RenamingInMethodBodyClass:
def bad_method(this):
this = this
@@ -118,8 +117,3 @@ class RenamingInMethodBodyClass:
def bad_method(this):
self = this
class RenamingWithNFKC:
def formula(household):
hºusehold(1)

View File

@@ -72,18 +72,3 @@ def f():
result = Foo()
for i in items:
result.append(i) # Ok
def f():
items = [1, 2, 3, 4]
result = []
async for i in items:
if i % 2:
result.append(i) # PERF401
def f():
items = [1, 2, 3, 4]
result = []
async for i in items:
result.append(i) # PERF401

View File

@@ -43,10 +43,3 @@ def f():
for path in ("foo", "bar"):
sys.path.append(path) # OK
def f():
items = [1, 2, 3, 4]
result = []
async for i in items:
result.append(i) # PERF402

View File

@@ -1,36 +0,0 @@
"""__init__.py without __all__
Unused stdlib and third party imports are unsafe removals
Unused first party imports get changed to redundant aliases
"""
# stdlib
import os # Ok: is used
_ = os
import argparse as argparse # Ok: is redundant alias
import sys # F401: remove unused
# first-party
from . import used # Ok: is used
_ = used
from . import aliased as aliased # Ok: is redundant alias
from . import unused # F401: change to redundant alias
from . import renamed as bees # F401: no fix

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1,42 +0,0 @@
"""__init__.py with __all__
Unused stdlib and third party imports are unsafe removals
Unused first party imports get added to __all__
"""
# stdlib
import os # Ok: is used
_ = os
import argparse # Ok: is exported in __all__
import sys # F401: remove unused
# first-party
from . import used # Ok: is used
_ = used
from . import aliased as aliased # Ok: is redundant alias
from . import exported # Ok: is exported in __all__
# from . import unused # F401: add to __all__
# from . import renamed as bees # F401: add to __all__
__all__ = ["argparse", "exported"]

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -1 +0,0 @@
# empty module imported by __init__.py for test fixture

View File

@@ -73,10 +73,3 @@ def op_add4(x, y=1):
def op_add5(x, y):
print("op_add5")
return x + y
# OK
class Class:
@staticmethod
def add(x, y):
return x + y

View File

@@ -109,7 +109,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
};
let definitions = std::mem::take(&mut checker.semantic.definitions);
let mut overloaded_name: Option<&str> = None;
let mut overloaded_name: Option<String> = None;
for ContextualizedDefinition {
definition,
visibility,
@@ -127,7 +127,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
if !overloaded_name.is_some_and(|overloaded_name| {
flake8_annotations::helpers::is_overload_impl(
definition,
overloaded_name,
&overloaded_name,
&checker.semantic,
)
}) {

View File

@@ -78,6 +78,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::DuplicateUnionMember,
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
Rule::NeverUnion,
]) {
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.

View File

@@ -877,7 +877,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithNestedImportStarUsage {
name: helpers::format_import_from(level, module).to_string(),
name: helpers::format_import_from(level, module),
},
stmt.range(),
));
@@ -886,7 +886,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UndefinedLocalWithImportStar) {
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithImportStar {
name: helpers::format_import_from(level, module).to_string(),
name: helpers::format_import_from(level, module),
},
stmt.range(),
));
@@ -1323,10 +1323,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::dict_iter_missing_items(checker, target, iter);
}
if checker.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(checker, for_stmt);
perflint::rules::manual_list_comprehension(checker, target, body);
}
if checker.enabled(Rule::ManualListCopy) {
perflint::rules::manual_list_copy(checker, for_stmt);
perflint::rules::manual_list_copy(checker, target, body);
}
if checker.enabled(Rule::ManualDictComprehension) {
perflint::rules::manual_dict_comprehension(checker, target, body);

View File

@@ -1,13 +1,17 @@
//! Lint rules based on import analysis.
use std::borrow::Cow;
use std::path::Path;
use ruff_diagnostics::Diagnostic;
use ruff_notebook::CellOffsets;
use ruff_python_ast::helpers::to_module_path;
use ruff_python_ast::imports::{ImportMap, ModuleImport};
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{PySourceType, Suite};
use ruff_python_ast::{self as ast, PySourceType, Stmt, Suite};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use crate::directives::IsortDirectives;
use crate::registry::Rule;
@@ -15,6 +19,57 @@ use crate::rules::isort;
use crate::rules::isort::block::{Block, BlockBuilder};
use crate::settings::LinterSettings;
fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option<ImportMap> {
let module_path = to_module_path(package?, path)?;
let num_imports = blocks.iter().map(|block| block.imports.len()).sum();
let mut module_imports = Vec::with_capacity(num_imports);
for stmt in blocks.iter().flat_map(|block| &block.imports) {
match stmt {
Stmt::Import(ast::StmtImport { names, range: _ }) => {
module_imports.extend(
names
.iter()
.map(|name| ModuleImport::new(name.name.to_string(), stmt.range())),
);
}
Stmt::ImportFrom(ast::StmtImportFrom {
module,
names,
level,
range: _,
}) => {
let level = *level as usize;
let module = if let Some(module) = module {
let module: &String = module.as_ref();
if level == 0 {
Cow::Borrowed(module)
} else {
if module_path.len() <= level {
continue;
}
let prefix = module_path[..module_path.len() - level].join(".");
Cow::Owned(format!("{prefix}.{module}"))
}
} else {
if module_path.len() <= level {
continue;
}
Cow::Owned(module_path[..module_path.len() - level].join("."))
};
module_imports.extend(names.iter().map(|name| {
ModuleImport::new(format!("{}.{}", module, name.name), name.range())
}));
}
_ => panic!("Expected Stmt::Import | Stmt::ImportFrom"),
}
}
let mut import_map = ImportMap::default();
import_map.insert(module_path.join("."), module_imports);
Some(import_map)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn check_imports(
python_ast: &Suite,
@@ -23,10 +78,11 @@ pub(crate) fn check_imports(
directives: &IsortDirectives,
settings: &LinterSettings,
stylist: &Stylist,
path: &Path,
package: Option<&Path>,
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
) -> Vec<Diagnostic> {
) -> (Vec<Diagnostic>, Option<ImportMap>) {
// Extract all import blocks from the AST.
let tracker = {
let mut tracker =
@@ -66,5 +122,8 @@ pub(crate) fn check_imports(
));
}
diagnostics
// Extract import map.
let imports = extract_import_map(path, package, &blocks);
(diagnostics, imports)
}

View File

@@ -273,7 +273,7 @@ pub(crate) struct TodoComment<'a> {
pub(crate) directive: TodoDirective<'a>,
/// The comment's actual [`TextRange`].
pub(crate) range: TextRange,
/// The comment range's position in [`Indexer::comment_ranges`]
/// The comment range's position in [`Indexer`].comment_ranges()
pub(crate) range_index: usize,
}

View File

@@ -122,28 +122,6 @@ pub(crate) fn remove_unused_imports<'a>(
}
}
/// Edits to make the specified imports explicit, e.g. change `import x` to `import x as x`.
pub(crate) fn make_redundant_alias<'a>(
member_names: impl Iterator<Item = &'a str>,
stmt: &Stmt,
) -> Vec<Edit> {
let aliases = match stmt {
Stmt::Import(ast::StmtImport { names, .. }) => names,
Stmt::ImportFrom(ast::StmtImportFrom { names, .. }) => names,
_ => {
return Vec::new();
}
};
member_names
.filter_map(|name| {
aliases
.iter()
.find(|alias| alias.asname.is_none() && name == alias.name.id)
.map(|alias| Edit::range_replacement(format!("{name} as {name}"), alias.range))
})
.collect()
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum Parentheses {
/// Remove parentheses, if the removed argument is the only argument left.
@@ -479,12 +457,11 @@ fn all_lines_fit(
mod tests {
use anyhow::Result;
use ruff_diagnostics::Edit;
use ruff_python_parser::parse_suite;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{Ranged, TextSize};
use crate::fix::edits::{make_redundant_alias, next_stmt_break, trailing_semicolon};
use crate::fix::edits::{next_stmt_break, trailing_semicolon};
#[test]
fn find_semicolon() -> Result<()> {
@@ -555,35 +532,4 @@ x = 1 \
TextSize::from(12)
);
}
#[test]
fn redundant_alias() {
let contents = "import x, y as y, z as bees";
let program = parse_suite(contents).unwrap();
let stmt = program.first().unwrap();
assert_eq!(
make_redundant_alias(["x"].into_iter(), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
)],
"make just one item redundant"
);
assert_eq!(
make_redundant_alias(vec!["x", "y"].into_iter(), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
)],
"the second item is already a redundant alias"
);
assert_eq!(
make_redundant_alias(vec!["x", "z"].into_iter(), stmt),
vec![Edit::range_replacement(
String::from("x as x"),
TextRange::new(TextSize::new(7), TextSize::new(8)),
)],
"the third item is already aliased to something else"
);
}
}

View File

@@ -56,7 +56,7 @@ impl CacheKey for LineLength {
pub enum ParseLineWidthError {
/// The string could not be parsed as a valid [u16]
ParseError(ParseIntError),
/// The [u16] value of the string is not a valid [`LineLength`]
/// The [u16] value of the string is not a valid [LineLength]
TryFromIntError(LineLengthFromIntError),
}

View File

@@ -10,6 +10,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_notebook::Notebook;
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::{PySourceType, Suite};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
@@ -61,7 +62,7 @@ pub type FixTable = FxHashMap<Rule, usize>;
pub struct FixerResult<'a> {
/// The result returned by the linter, after applying any fixes.
pub result: LinterResult<Vec<Message>>,
pub result: LinterResult<(Vec<Message>, Option<ImportMap>)>,
/// The resulting source code, after applying any fixes.
pub transformed: Cow<'a, SourceKind>,
/// The number of fixes applied for each [`Rule`].
@@ -83,9 +84,10 @@ pub fn check_path(
source_kind: &SourceKind,
source_type: PySourceType,
tokens: TokenSource,
) -> LinterResult<Vec<Diagnostic>> {
) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
let mut imports = None;
let mut error = None;
// Collect doc lines. This requires a rare mix of tokens (for comments) and AST
@@ -167,18 +169,19 @@ pub fn check_path(
));
}
if use_imports {
let import_diagnostics = check_imports(
let (import_diagnostics, module_imports) = check_imports(
&python_ast,
locator,
indexer,
&directives.isort,
settings,
stylist,
path,
package,
source_type,
cell_offsets,
);
imports = module_imports;
diagnostics.extend(import_diagnostics);
}
if use_doc_lines {
@@ -337,7 +340,7 @@ pub fn check_path(
}
}
LinterResult::new(diagnostics, error)
LinterResult::new((diagnostics, imports), error)
}
const MAX_ITERATIONS: usize = 100;
@@ -407,7 +410,7 @@ pub fn add_noqa_to_path(
// TODO(dhruvmanila): Add support for Jupyter Notebooks
add_noqa(
path,
&diagnostics,
&diagnostics.0,
&locator,
indexer.comment_ranges(),
&settings.external,
@@ -426,7 +429,7 @@ pub fn lint_only(
source_kind: &SourceKind,
source_type: PySourceType,
data: ParseSource,
) -> LinterResult<Vec<Message>> {
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens = data.into_token_source(source_kind, source_type);
@@ -462,7 +465,12 @@ pub fn lint_only(
tokens,
);
result.map(|diagnostics| diagnostics_to_messages(diagnostics, path, &locator, &directives))
result.map(|(diagnostics, imports)| {
(
diagnostics_to_messages(diagnostics, path, &locator, &directives),
imports,
)
})
}
/// Convert from diagnostics to messages.
@@ -575,7 +583,7 @@ pub fn lint_fix<'a>(
code: fixed_contents,
fixes: applied,
source_map,
}) = fix_file(&result.data, &locator, unsafe_fixes)
}) = fix_file(&result.data.0, &locator, unsafe_fixes)
{
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.
@@ -592,12 +600,15 @@ pub fn lint_fix<'a>(
continue;
}
report_failed_to_converge_error(path, transformed.source_code(), &result.data);
report_failed_to_converge_error(path, transformed.source_code(), &result.data.0);
}
return Ok(FixerResult {
result: result.map(|diagnostics| {
diagnostics_to_messages(diagnostics, path, &locator, &directives)
result: result.map(|(diagnostics, imports)| {
(
diagnostics_to_messages(diagnostics, path, &locator, &directives),
imports,
)
}),
transformed,
fixed,

View File

@@ -207,15 +207,6 @@ impl RuleSet {
*self = set.union(&RuleSet::from_rule(rule));
}
#[inline]
pub fn set(&mut self, rule: Rule, enabled: bool) {
if enabled {
self.insert(rule);
} else {
self.remove(rule);
}
}
/// Removes `rule` from the set.
///
/// ## Examples

View File

@@ -6,7 +6,7 @@ use itertools::Itertools;
use ruff_diagnostics::Edit;
use ruff_python_codegen::Stylist;
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextSize};
pub(crate) struct Renamer;
@@ -215,6 +215,7 @@ impl Renamer {
let quote = stylist.quote();
format!("{quote}{target}{quote}")
} else {
debug_assert_eq!(TextSize::of(name), reference.range().len());
target.to_string()
}
};

View File

@@ -17,13 +17,10 @@ use crate::importer::{ImportRequest, Importer};
use crate::settings::types::PythonVersion;
/// Return the name of the function, if it's overloaded.
pub(crate) fn overloaded_name<'a>(
definition: &'a Definition,
semantic: &SemanticModel,
) -> Option<&'a str> {
pub(crate) fn overloaded_name(definition: &Definition, semantic: &SemanticModel) -> Option<String> {
let function = definition.as_function_def()?;
if visibility::is_overload(&function.decorator_list, semantic) {
Some(function.name.as_str())
Some(function.name.to_string())
} else {
None
}

View File

@@ -531,7 +531,7 @@ pub(crate) fn fix_unnecessary_double_cast_or_process(
.find(|argument| argument.keyword.is_none())
{
let mut arg = arg.clone();
arg.comma.clone_from(&first.comma);
arg.comma = first.comma.clone();
arg.whitespace_after_arg = first.whitespace_after_arg.clone();
iter::once(arg)
.chain(rest.iter().cloned())

View File

@@ -8,8 +8,8 @@ use ruff_text_size::Ranged;
/// Checks for incorrect import of pytest.
///
/// ## Why is this bad?
/// For consistency, `pytest` should be imported as `import pytest` and its members should be
/// accessed in the form of `pytest.xxx.yyy` for consistency
/// `pytest` should be imported as `import pytest` and its members should be accessed in the form of
/// `pytest.xxx.yyy` for consistency and to make it easier for linting tools to analyze the code.
///
/// ## Example
/// ```python
@@ -27,7 +27,7 @@ pub struct PytestIncorrectPytestImport;
impl Violation for PytestIncorrectPytestImport {
#[derive_message_formats]
fn message(&self) -> String {
format!("Incorrect import of `pytest`; use `import pytest` instead")
format!("Found incorrect import of pytest, use simple `import pytest` instead")
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
---
PT013.py:11:1: PT013 Incorrect import of `pytest`; use `import pytest` instead
PT013.py:11:1: PT013 Found incorrect import of pytest, use simple `import pytest` instead
|
9 | # Error
10 |
@@ -11,7 +11,7 @@ PT013.py:11:1: PT013 Incorrect import of `pytest`; use `import pytest` instead
13 | from pytest import fixture as other_name
|
PT013.py:12:1: PT013 Incorrect import of `pytest`; use `import pytest` instead
PT013.py:12:1: PT013 Found incorrect import of pytest, use simple `import pytest` instead
|
11 | import pytest as other_name
12 | from pytest import fixture
@@ -19,10 +19,12 @@ PT013.py:12:1: PT013 Incorrect import of `pytest`; use `import pytest` instead
13 | from pytest import fixture as other_name
|
PT013.py:13:1: PT013 Incorrect import of `pytest`; use `import pytest` instead
PT013.py:13:1: PT013 Found incorrect import of pytest, use simple `import pytest` instead
|
11 | import pytest as other_name
12 | from pytest import fixture
13 | from pytest import fixture as other_name
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT013
|

View File

@@ -67,8 +67,8 @@ pub(crate) fn fix_multiple_with_statements(
outer_with.items.append(&mut inner_with.items);
if outer_with.lpar.is_none() {
outer_with.lpar.clone_from(&inner_with.lpar);
outer_with.rpar.clone_from(&inner_with.rpar);
outer_with.lpar = inner_with.lpar.clone();
outer_with.rpar = inner_with.rpar.clone();
}
outer_with.body = inner_with.body.clone();

View File

@@ -322,7 +322,7 @@ pub(crate) fn unused_arguments(
return;
}
let Some(parent) = checker.semantic().first_non_type_parent_scope(scope) else {
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};

View File

@@ -29,38 +29,29 @@ pub(crate) struct CommentSet<'a> {
pub(crate) inline: Vec<Cow<'a, str>>,
}
pub(crate) trait Importable<'a> {
fn module_name(&self) -> Cow<'a, str>;
pub(crate) trait Importable {
fn module_name(&self) -> String;
fn module_base(&self) -> String;
}
fn module_base(&self) -> Cow<'a, str> {
match self.module_name() {
Cow::Borrowed(module_name) => Cow::Borrowed(
module_name
.split('.')
.next()
.expect("module to include at least one segment"),
),
Cow::Owned(module_name) => Cow::Owned(
module_name
.split('.')
.next()
.expect("module to include at least one segment")
.to_owned(),
),
}
impl Importable for AliasData<'_> {
fn module_name(&self) -> String {
self.name.to_string()
}
fn module_base(&self) -> String {
self.module_name().split('.').next().unwrap().to_string()
}
}
impl<'a> Importable<'a> for AliasData<'a> {
fn module_name(&self) -> Cow<'a, str> {
Cow::Borrowed(self.name)
}
}
impl<'a> Importable<'a> for ImportFromData<'a> {
fn module_name(&self) -> Cow<'a, str> {
impl Importable for ImportFromData<'_> {
fn module_name(&self) -> String {
format_import_from(self.level, self.module)
}
fn module_base(&self) -> String {
self.module_name().split('.').next().unwrap().to_string()
}
}
#[derive(Debug, Default)]

View File

@@ -190,7 +190,7 @@ pub(crate) fn invalid_first_argument_name(
panic!("Expected ScopeKind::Function")
};
let Some(parent) = checker.semantic().first_non_type_parent_scope(scope) else {
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};

View File

@@ -20,159 +20,160 @@ N805.py:7:20: N805 [*] First argument of a method should be named `self`
9 9 |
10 10 | if False:
N805.py:11:30: N805 [*] First argument of a method should be named `self`
N805.py:12:30: N805 [*] First argument of a method should be named `self`
|
10 | if False:
11 | def extra_bad_method(this):
11 |
12 | def extra_bad_method(this):
| ^^^^ N805
12 | pass
13 | pass
|
= help: Rename `this` to `self`
Unsafe fix
8 8 | pass
9 9 |
10 10 | if False:
11 |- def extra_bad_method(this):
11 |+ def extra_bad_method(self):
12 12 | pass
13 13 |
14 14 | def good_method(self):
11 11 |
12 |- def extra_bad_method(this):
12 |+ def extra_bad_method(self):
13 13 | pass
14 14 |
15 15 | def good_method(self):
N805.py:30:15: N805 [*] First argument of a method should be named `self`
N805.py:31:15: N805 [*] First argument of a method should be named `self`
|
29 | @pydantic.validator
30 | def lower(cls, my_field: str) -> str:
30 | @pydantic.validator
31 | def lower(cls, my_field: str) -> str:
| ^^^ N805
31 | pass
32 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
27 27 | return x
28 28 |
29 29 | @pydantic.validator
30 |- def lower(cls, my_field: str) -> str:
30 |+ def lower(self, my_field: str) -> str:
31 31 | pass
32 32 |
33 33 | @pydantic.validator("my_field")
28 28 | return x
29 29 |
30 30 | @pydantic.validator
31 |- def lower(cls, my_field: str) -> str:
31 |+ def lower(self, my_field: str) -> str:
32 32 | pass
33 33 |
34 34 | @pydantic.validator("my_field")
N805.py:34:15: N805 [*] First argument of a method should be named `self`
N805.py:35:15: N805 [*] First argument of a method should be named `self`
|
33 | @pydantic.validator("my_field")
34 | def lower(cls, my_field: str) -> str:
34 | @pydantic.validator("my_field")
35 | def lower(cls, my_field: str) -> str:
| ^^^ N805
35 | pass
36 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
31 31 | pass
32 32 |
33 33 | @pydantic.validator("my_field")
34 |- def lower(cls, my_field: str) -> str:
34 |+ def lower(self, my_field: str) -> str:
35 35 | pass
36 36 |
37 37 | def __init__(self):
32 32 | pass
33 33 |
34 34 | @pydantic.validator("my_field")
35 |- def lower(cls, my_field: str) -> str:
35 |+ def lower(self, my_field: str) -> str:
36 36 | pass
37 37 |
38 38 | def __init__(self):
N805.py:63:29: N805 [*] First argument of a method should be named `self`
N805.py:64:29: N805 [*] First argument of a method should be named `self`
|
61 | pass
62 |
63 | def bad_method_pos_only(this, blah, /, something: str):
62 | pass
63 |
64 | def bad_method_pos_only(this, blah, /, something: str):
| ^^^^ N805
64 | pass
65 | pass
|
= help: Rename `this` to `self`
Unsafe fix
60 60 | def good_method_pos_only(self, blah, /, something: str):
61 61 | pass
62 62 |
63 |- def bad_method_pos_only(this, blah, /, something: str):
63 |+ def bad_method_pos_only(self, blah, /, something: str):
64 64 | pass
65 65 |
61 61 | def good_method_pos_only(self, blah, /, something: str):
62 62 | pass
63 63 |
64 |- def bad_method_pos_only(this, blah, /, something: str):
64 |+ def bad_method_pos_only(self, blah, /, something: str):
65 65 | pass
66 66 |
67 67 |
N805.py:69:13: N805 [*] First argument of a method should be named `self`
N805.py:70:13: N805 [*] First argument of a method should be named `self`
|
67 | class ModelClass:
68 | @hybrid_property
69 | def bad(cls):
68 | class ModelClass:
69 | @hybrid_property
70 | def bad(cls):
| ^^^ N805
70 | pass
71 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
66 66 |
67 67 | class ModelClass:
68 68 | @hybrid_property
69 |- def bad(cls):
69 |+ def bad(self):
70 70 | pass
71 71 |
72 72 | @bad.expression
67 67 |
68 68 | class ModelClass:
69 69 | @hybrid_property
70 |- def bad(cls):
70 |+ def bad(self):
71 71 | pass
72 72 |
73 73 | @bad.expression
N805.py:77:13: N805 [*] First argument of a method should be named `self`
N805.py:78:13: N805 [*] First argument of a method should be named `self`
|
76 | @bad.wtf
77 | def bad(cls):
77 | @bad.wtf
78 | def bad(cls):
| ^^^ N805
78 | pass
79 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
74 74 | pass
75 75 |
76 76 | @bad.wtf
77 |- def bad(cls):
77 |+ def bad(self):
78 78 | pass
79 79 |
80 80 | @hybrid_property
75 75 | pass
76 76 |
77 77 | @bad.wtf
78 |- def bad(cls):
78 |+ def bad(self):
79 79 | pass
80 80 |
81 81 | @hybrid_property
N805.py:85:14: N805 [*] First argument of a method should be named `self`
N805.py:86:14: N805 [*] First argument of a method should be named `self`
|
84 | @good.expression
85 | def good(cls):
85 | @good.expression
86 | def good(cls):
| ^^^ N805
86 | pass
87 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
82 82 | pass
83 83 |
84 84 | @good.expression
85 |- def good(cls):
85 |+ def good(self):
86 86 | pass
87 87 |
88 88 | @good.wtf
83 83 | pass
84 84 |
85 85 | @good.expression
86 |- def good(cls):
86 |+ def good(self):
87 87 | pass
88 88 |
89 89 | @good.wtf
N805.py:93:19: N805 [*] First argument of a method should be named `self`
N805.py:94:19: N805 [*] First argument of a method should be named `self`
|
92 | @foobar.thisisstatic
93 | def badstatic(foo):
93 | @foobar.thisisstatic
94 | def badstatic(foo):
| ^^^ N805
94 | pass
95 | pass
|
= help: Rename `foo` to `self`
Unsafe fix
90 90 | pass
91 91 |
92 92 | @foobar.thisisstatic
93 |- def badstatic(foo):
93 |+ def badstatic(self):
94 94 | pass
95 95 |
91 91 | pass
92 92 |
93 93 | @foobar.thisisstatic
94 |- def badstatic(foo):
94 |+ def badstatic(self):
95 95 | pass
96 96 |
97 97 | class SelfInArgsClass:
N805.py:98:26: N805 First argument of a method should be named `self`
|
@@ -223,66 +224,45 @@ N805.py:110:24: N805 First argument of a method should be named `self`
|
= help: Rename `this` to `self`
N805.py:115:20: N805 [*] First argument of a method should be named `self`
N805.py:114:20: N805 [*] First argument of a method should be named `self`
|
114 | class RenamingInMethodBodyClass:
115 | def bad_method(this):
113 | class RenamingInMethodBodyClass:
114 | def bad_method(this):
| ^^^^ N805
116 | this = this
117 | this
115 | this = this
116 | this
|
= help: Rename `this` to `self`
Unsafe fix
111 111 | pass
112 112 |
113 113 |
114 114 | class RenamingInMethodBodyClass:
115 |- def bad_method(this):
116 |- this = this
117 |- this
115 |+ def bad_method(self):
116 |+ self = self
117 |+ self
118 118 |
119 119 | def bad_method(this):
120 120 | self = this
113 113 | class RenamingInMethodBodyClass:
114 |- def bad_method(this):
115 |- this = this
116 |- this
114 |+ def bad_method(self):
115 |+ self = self
116 |+ self
117 117 |
118 118 | def bad_method(this):
119 119 | self = this
N805.py:119:20: N805 [*] First argument of a method should be named `self`
N805.py:118:20: N805 [*] First argument of a method should be named `self`
|
117 | this
118 |
119 | def bad_method(this):
116 | this
117 |
118 | def bad_method(this):
| ^^^^ N805
120 | self = this
119 | self = this
|
= help: Rename `this` to `self`
Unsafe fix
116 116 | this = this
117 117 | this
118 118 |
119 |- def bad_method(this):
120 |- self = this
119 |+ def bad_method(self):
120 |+ self = self
121 121 |
122 122 |
123 123 | class RenamingWithNFKC:
N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
123 | class RenamingWithNFKC:
124 | def formula(household):
| ^^^^^^^^^ N805
125 | hºusehold(1)
|
= help: Rename `household` to `self`
Unsafe fix
121 121 |
122 122 |
123 123 | class RenamingWithNFKC:
124 |- def formula(household):
125 |- hºusehold(1)
124 |+ def formula(self):
125 |+ self(1)
115 115 | this = this
116 116 | this
117 117 |
118 |- def bad_method(this):
119 |- self = this
118 |+ def bad_method(self):
119 |+ self = self

View File

@@ -20,102 +20,103 @@ N805.py:7:20: N805 [*] First argument of a method should be named `self`
9 9 |
10 10 | if False:
N805.py:11:30: N805 [*] First argument of a method should be named `self`
N805.py:12:30: N805 [*] First argument of a method should be named `self`
|
10 | if False:
11 | def extra_bad_method(this):
11 |
12 | def extra_bad_method(this):
| ^^^^ N805
12 | pass
13 | pass
|
= help: Rename `this` to `self`
Unsafe fix
8 8 | pass
9 9 |
10 10 | if False:
11 |- def extra_bad_method(this):
11 |+ def extra_bad_method(self):
12 12 | pass
13 13 |
14 14 | def good_method(self):
11 11 |
12 |- def extra_bad_method(this):
12 |+ def extra_bad_method(self):
13 13 | pass
14 14 |
15 15 | def good_method(self):
N805.py:63:29: N805 [*] First argument of a method should be named `self`
N805.py:64:29: N805 [*] First argument of a method should be named `self`
|
61 | pass
62 |
63 | def bad_method_pos_only(this, blah, /, something: str):
62 | pass
63 |
64 | def bad_method_pos_only(this, blah, /, something: str):
| ^^^^ N805
64 | pass
65 | pass
|
= help: Rename `this` to `self`
Unsafe fix
60 60 | def good_method_pos_only(self, blah, /, something: str):
61 61 | pass
62 62 |
63 |- def bad_method_pos_only(this, blah, /, something: str):
63 |+ def bad_method_pos_only(self, blah, /, something: str):
64 64 | pass
65 65 |
61 61 | def good_method_pos_only(self, blah, /, something: str):
62 62 | pass
63 63 |
64 |- def bad_method_pos_only(this, blah, /, something: str):
64 |+ def bad_method_pos_only(self, blah, /, something: str):
65 65 | pass
66 66 |
67 67 |
N805.py:69:13: N805 [*] First argument of a method should be named `self`
N805.py:70:13: N805 [*] First argument of a method should be named `self`
|
67 | class ModelClass:
68 | @hybrid_property
69 | def bad(cls):
68 | class ModelClass:
69 | @hybrid_property
70 | def bad(cls):
| ^^^ N805
70 | pass
71 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
66 66 |
67 67 | class ModelClass:
68 68 | @hybrid_property
69 |- def bad(cls):
69 |+ def bad(self):
70 70 | pass
71 71 |
72 72 | @bad.expression
67 67 |
68 68 | class ModelClass:
69 69 | @hybrid_property
70 |- def bad(cls):
70 |+ def bad(self):
71 71 | pass
72 72 |
73 73 | @bad.expression
N805.py:77:13: N805 [*] First argument of a method should be named `self`
N805.py:78:13: N805 [*] First argument of a method should be named `self`
|
76 | @bad.wtf
77 | def bad(cls):
77 | @bad.wtf
78 | def bad(cls):
| ^^^ N805
78 | pass
79 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
74 74 | pass
75 75 |
76 76 | @bad.wtf
77 |- def bad(cls):
77 |+ def bad(self):
78 78 | pass
79 79 |
80 80 | @hybrid_property
75 75 | pass
76 76 |
77 77 | @bad.wtf
78 |- def bad(cls):
78 |+ def bad(self):
79 79 | pass
80 80 |
81 81 | @hybrid_property
N805.py:93:19: N805 [*] First argument of a method should be named `self`
N805.py:94:19: N805 [*] First argument of a method should be named `self`
|
92 | @foobar.thisisstatic
93 | def badstatic(foo):
93 | @foobar.thisisstatic
94 | def badstatic(foo):
| ^^^ N805
94 | pass
95 | pass
|
= help: Rename `foo` to `self`
Unsafe fix
90 90 | pass
91 91 |
92 92 | @foobar.thisisstatic
93 |- def badstatic(foo):
93 |+ def badstatic(self):
94 94 | pass
95 95 |
91 91 | pass
92 92 |
93 93 | @foobar.thisisstatic
94 |- def badstatic(foo):
94 |+ def badstatic(self):
95 95 | pass
96 96 |
97 97 | class SelfInArgsClass:
N805.py:98:26: N805 First argument of a method should be named `self`
|
@@ -166,66 +167,45 @@ N805.py:110:24: N805 First argument of a method should be named `self`
|
= help: Rename `this` to `self`
N805.py:115:20: N805 [*] First argument of a method should be named `self`
N805.py:114:20: N805 [*] First argument of a method should be named `self`
|
114 | class RenamingInMethodBodyClass:
115 | def bad_method(this):
113 | class RenamingInMethodBodyClass:
114 | def bad_method(this):
| ^^^^ N805
116 | this = this
117 | this
115 | this = this
116 | this
|
= help: Rename `this` to `self`
Unsafe fix
111 111 | pass
112 112 |
113 113 |
114 114 | class RenamingInMethodBodyClass:
115 |- def bad_method(this):
116 |- this = this
117 |- this
115 |+ def bad_method(self):
116 |+ self = self
117 |+ self
118 118 |
119 119 | def bad_method(this):
120 120 | self = this
113 113 | class RenamingInMethodBodyClass:
114 |- def bad_method(this):
115 |- this = this
116 |- this
114 |+ def bad_method(self):
115 |+ self = self
116 |+ self
117 117 |
118 118 | def bad_method(this):
119 119 | self = this
N805.py:119:20: N805 [*] First argument of a method should be named `self`
N805.py:118:20: N805 [*] First argument of a method should be named `self`
|
117 | this
118 |
119 | def bad_method(this):
116 | this
117 |
118 | def bad_method(this):
| ^^^^ N805
120 | self = this
119 | self = this
|
= help: Rename `this` to `self`
Unsafe fix
116 116 | this = this
117 117 | this
118 118 |
119 |- def bad_method(this):
120 |- self = this
119 |+ def bad_method(self):
120 |+ self = self
121 121 |
122 122 |
123 123 | class RenamingWithNFKC:
N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
123 | class RenamingWithNFKC:
124 | def formula(household):
| ^^^^^^^^^ N805
125 | hºusehold(1)
|
= help: Rename `household` to `self`
Unsafe fix
121 121 |
122 122 |
123 123 | class RenamingWithNFKC:
124 |- def formula(household):
125 |- hºusehold(1)
124 |+ def formula(self):
125 |+ self(1)
115 115 | this = this
116 116 | this
117 117 |
118 |- def bad_method(this):
119 |- self = this
118 |+ def bad_method(self):
119 |+ self = self

View File

@@ -20,140 +20,141 @@ N805.py:7:20: N805 [*] First argument of a method should be named `self`
9 9 |
10 10 | if False:
N805.py:11:30: N805 [*] First argument of a method should be named `self`
N805.py:12:30: N805 [*] First argument of a method should be named `self`
|
10 | if False:
11 | def extra_bad_method(this):
11 |
12 | def extra_bad_method(this):
| ^^^^ N805
12 | pass
13 | pass
|
= help: Rename `this` to `self`
Unsafe fix
8 8 | pass
9 9 |
10 10 | if False:
11 |- def extra_bad_method(this):
11 |+ def extra_bad_method(self):
12 12 | pass
13 13 |
14 14 | def good_method(self):
11 11 |
12 |- def extra_bad_method(this):
12 |+ def extra_bad_method(self):
13 13 | pass
14 14 |
15 15 | def good_method(self):
N805.py:30:15: N805 [*] First argument of a method should be named `self`
N805.py:31:15: N805 [*] First argument of a method should be named `self`
|
29 | @pydantic.validator
30 | def lower(cls, my_field: str) -> str:
30 | @pydantic.validator
31 | def lower(cls, my_field: str) -> str:
| ^^^ N805
31 | pass
32 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
27 27 | return x
28 28 |
29 29 | @pydantic.validator
30 |- def lower(cls, my_field: str) -> str:
30 |+ def lower(self, my_field: str) -> str:
31 31 | pass
32 32 |
33 33 | @pydantic.validator("my_field")
28 28 | return x
29 29 |
30 30 | @pydantic.validator
31 |- def lower(cls, my_field: str) -> str:
31 |+ def lower(self, my_field: str) -> str:
32 32 | pass
33 33 |
34 34 | @pydantic.validator("my_field")
N805.py:34:15: N805 [*] First argument of a method should be named `self`
N805.py:35:15: N805 [*] First argument of a method should be named `self`
|
33 | @pydantic.validator("my_field")
34 | def lower(cls, my_field: str) -> str:
34 | @pydantic.validator("my_field")
35 | def lower(cls, my_field: str) -> str:
| ^^^ N805
35 | pass
36 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
31 31 | pass
32 32 |
33 33 | @pydantic.validator("my_field")
34 |- def lower(cls, my_field: str) -> str:
34 |+ def lower(self, my_field: str) -> str:
35 35 | pass
36 36 |
37 37 | def __init__(self):
32 32 | pass
33 33 |
34 34 | @pydantic.validator("my_field")
35 |- def lower(cls, my_field: str) -> str:
35 |+ def lower(self, my_field: str) -> str:
36 36 | pass
37 37 |
38 38 | def __init__(self):
N805.py:63:29: N805 [*] First argument of a method should be named `self`
N805.py:64:29: N805 [*] First argument of a method should be named `self`
|
61 | pass
62 |
63 | def bad_method_pos_only(this, blah, /, something: str):
62 | pass
63 |
64 | def bad_method_pos_only(this, blah, /, something: str):
| ^^^^ N805
64 | pass
65 | pass
|
= help: Rename `this` to `self`
Unsafe fix
60 60 | def good_method_pos_only(self, blah, /, something: str):
61 61 | pass
62 62 |
63 |- def bad_method_pos_only(this, blah, /, something: str):
63 |+ def bad_method_pos_only(self, blah, /, something: str):
64 64 | pass
65 65 |
61 61 | def good_method_pos_only(self, blah, /, something: str):
62 62 | pass
63 63 |
64 |- def bad_method_pos_only(this, blah, /, something: str):
64 |+ def bad_method_pos_only(self, blah, /, something: str):
65 65 | pass
66 66 |
67 67 |
N805.py:69:13: N805 [*] First argument of a method should be named `self`
N805.py:70:13: N805 [*] First argument of a method should be named `self`
|
67 | class ModelClass:
68 | @hybrid_property
69 | def bad(cls):
68 | class ModelClass:
69 | @hybrid_property
70 | def bad(cls):
| ^^^ N805
70 | pass
71 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
66 66 |
67 67 | class ModelClass:
68 68 | @hybrid_property
69 |- def bad(cls):
69 |+ def bad(self):
70 70 | pass
71 71 |
72 72 | @bad.expression
67 67 |
68 68 | class ModelClass:
69 69 | @hybrid_property
70 |- def bad(cls):
70 |+ def bad(self):
71 71 | pass
72 72 |
73 73 | @bad.expression
N805.py:77:13: N805 [*] First argument of a method should be named `self`
N805.py:78:13: N805 [*] First argument of a method should be named `self`
|
76 | @bad.wtf
77 | def bad(cls):
77 | @bad.wtf
78 | def bad(cls):
| ^^^ N805
78 | pass
79 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
74 74 | pass
75 75 |
76 76 | @bad.wtf
77 |- def bad(cls):
77 |+ def bad(self):
78 78 | pass
79 79 |
80 80 | @hybrid_property
75 75 | pass
76 76 |
77 77 | @bad.wtf
78 |- def bad(cls):
78 |+ def bad(self):
79 79 | pass
80 80 |
81 81 | @hybrid_property
N805.py:85:14: N805 [*] First argument of a method should be named `self`
N805.py:86:14: N805 [*] First argument of a method should be named `self`
|
84 | @good.expression
85 | def good(cls):
85 | @good.expression
86 | def good(cls):
| ^^^ N805
86 | pass
87 | pass
|
= help: Rename `cls` to `self`
Unsafe fix
82 82 | pass
83 83 |
84 84 | @good.expression
85 |- def good(cls):
85 |+ def good(self):
86 86 | pass
87 87 |
88 88 | @good.wtf
83 83 | pass
84 84 |
85 85 | @good.expression
86 |- def good(cls):
86 |+ def good(self):
87 87 | pass
88 88 |
89 89 | @good.wtf
N805.py:98:26: N805 First argument of a method should be named `self`
|
@@ -204,66 +205,45 @@ N805.py:110:24: N805 First argument of a method should be named `self`
|
= help: Rename `this` to `self`
N805.py:115:20: N805 [*] First argument of a method should be named `self`
N805.py:114:20: N805 [*] First argument of a method should be named `self`
|
114 | class RenamingInMethodBodyClass:
115 | def bad_method(this):
113 | class RenamingInMethodBodyClass:
114 | def bad_method(this):
| ^^^^ N805
116 | this = this
117 | this
115 | this = this
116 | this
|
= help: Rename `this` to `self`
Unsafe fix
111 111 | pass
112 112 |
113 113 |
114 114 | class RenamingInMethodBodyClass:
115 |- def bad_method(this):
116 |- this = this
117 |- this
115 |+ def bad_method(self):
116 |+ self = self
117 |+ self
118 118 |
119 119 | def bad_method(this):
120 120 | self = this
113 113 | class RenamingInMethodBodyClass:
114 |- def bad_method(this):
115 |- this = this
116 |- this
114 |+ def bad_method(self):
115 |+ self = self
116 |+ self
117 117 |
118 118 | def bad_method(this):
119 119 | self = this
N805.py:119:20: N805 [*] First argument of a method should be named `self`
N805.py:118:20: N805 [*] First argument of a method should be named `self`
|
117 | this
118 |
119 | def bad_method(this):
116 | this
117 |
118 | def bad_method(this):
| ^^^^ N805
120 | self = this
119 | self = this
|
= help: Rename `this` to `self`
Unsafe fix
116 116 | this = this
117 117 | this
118 118 |
119 |- def bad_method(this):
120 |- self = this
119 |+ def bad_method(self):
120 |+ self = self
121 121 |
122 122 |
123 123 | class RenamingWithNFKC:
N805.py:124:17: N805 [*] First argument of a method should be named `self`
|
123 | class RenamingWithNFKC:
124 | def formula(household):
| ^^^^^^^^^ N805
125 | hºusehold(1)
|
= help: Rename `household` to `self`
Unsafe fix
121 121 |
122 122 |
123 123 | class RenamingWithNFKC:
124 |- def formula(household):
125 |- hºusehold(1)
124 |+ def formula(self):
125 |+ self(1)
115 115 | this = this
116 116 | this
117 117 |
118 |- def bad_method(this):
119 |- self = this
118 |+ def bad_method(self):
119 |+ self = self

View File

@@ -44,28 +44,22 @@ use crate::checkers::ast::Checker;
/// filtered.extend(x for x in original if x % 2)
/// ```
#[violation]
pub struct ManualListComprehension {
is_async: bool,
}
pub struct ManualListComprehension;
impl Violation for ManualListComprehension {
#[derive_message_formats]
fn message(&self) -> String {
let ManualListComprehension { is_async } = self;
match is_async {
false => format!("Use a list comprehension to create a transformed list"),
true => format!("Use an async list comprehension to create a transformed list"),
}
format!("Use a list comprehension to create a transformed list")
}
}
/// PERF401
pub(crate) fn manual_list_comprehension(checker: &mut Checker, for_stmt: &ast::StmtFor) {
let Expr::Name(ast::ExprName { id, .. }) = &*for_stmt.target else {
pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
let Expr::Name(ast::ExprName { id, .. }) = target else {
return;
};
let (stmt, if_test) = match &*for_stmt.body {
let (stmt, if_test) = match body {
// ```python
// for x in y:
// if z:
@@ -127,13 +121,10 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, for_stmt: &ast::S
return;
}
// Ignore direct list copies (e.g., `for x in y: filtered.append(x)`), unless it's async, which
// `manual-list-copy` doesn't cover.
if !for_stmt.is_async {
if if_test.is_none() {
if arg.as_name_expr().is_some_and(|arg| arg.id == *id) {
return;
}
// Ignore direct list copies (e.g., `for x in y: filtered.append(x)`).
if if_test.is_none() {
if arg.as_name_expr().is_some_and(|arg| arg.id == *id) {
return;
}
}
@@ -188,10 +179,7 @@ pub(crate) fn manual_list_comprehension(checker: &mut Checker, for_stmt: &ast::S
return;
}
checker.diagnostics.push(Diagnostic::new(
ManualListComprehension {
is_async: for_stmt.is_async,
},
*range,
));
checker
.diagnostics
.push(Diagnostic::new(ManualListComprehension, *range));
}

View File

@@ -45,16 +45,12 @@ impl Violation for ManualListCopy {
}
/// PERF402
pub(crate) fn manual_list_copy(checker: &mut Checker, for_stmt: &ast::StmtFor) {
if for_stmt.is_async {
return;
}
let Expr::Name(ast::ExprName { id, .. }) = &*for_stmt.target else {
pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
let Expr::Name(ast::ExprName { id, .. }) = target else {
return;
};
let [stmt] = &*for_stmt.body else {
let [stmt] = body else {
return;
};

View File

@@ -17,18 +17,4 @@ PERF401.py:13:9: PERF401 Use a list comprehension to create a transformed list
| ^^^^^^^^^^^^^^^^^^^^ PERF401
|
PERF401.py:82:13: PERF401 Use an async list comprehension to create a transformed list
|
80 | async for i in items:
81 | if i % 2:
82 | result.append(i) # PERF401
| ^^^^^^^^^^^^^^^^ PERF401
|
PERF401.py:89:9: PERF401 Use an async list comprehension to create a transformed list
|
87 | result = []
88 | async for i in items:
89 | result.append(i) # PERF401
| ^^^^^^^^^^^^^^^^ PERF401
|

View File

@@ -31,21 +31,18 @@ use super::LogicalLine;
/// The rule is also incompatible with the [formatter] when using
/// `indent-width` with a value other than `4`.
///
/// ## Options
/// - `indent-width`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#indentation
/// [formatter]:https://docs.astral.sh/ruff/formatter/
#[violation]
pub struct IndentationWithInvalidMultiple {
indent_width: usize,
indent_size: usize,
}
impl Violation for IndentationWithInvalidMultiple {
#[derive_message_formats]
fn message(&self) -> String {
let Self { indent_width } = self;
format!("Indentation is not a multiple of {indent_width}")
let Self { indent_size } = self;
format!("Indentation is not a multiple of {indent_size}")
}
}
@@ -74,21 +71,18 @@ impl Violation for IndentationWithInvalidMultiple {
/// The rule is also incompatible with the [formatter] when using
/// `indent-width` with a value other than `4`.
///
/// ## Options
/// - `indent-width`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#indentation
/// [formatter]:https://docs.astral.sh/ruff/formatter/
#[violation]
pub struct IndentationWithInvalidMultipleComment {
indent_width: usize,
indent_size: usize,
}
impl Violation for IndentationWithInvalidMultipleComment {
#[derive_message_formats]
fn message(&self) -> String {
let Self { indent_width } = self;
format!("Indentation is not a multiple of {indent_width} (comment)")
let Self { indent_size } = self;
format!("Indentation is not a multiple of {indent_size} (comment)")
}
}
@@ -263,13 +257,9 @@ pub(crate) fn indentation(
if indent_level % indent_size != 0 {
diagnostics.push(if logical_line.is_comment_only() {
DiagnosticKind::from(IndentationWithInvalidMultipleComment {
indent_width: indent_size,
})
DiagnosticKind::from(IndentationWithInvalidMultipleComment { indent_size })
} else {
DiagnosticKind::from(IndentationWithInvalidMultiple {
indent_width: indent_size,
})
DiagnosticKind::from(IndentationWithInvalidMultiple { indent_size })
});
}
let indent_expect = prev_logical_line

View File

@@ -60,7 +60,7 @@ pub(crate) fn redundant_backslash(
let start = locator.line_start(token.start());
start_index = continuation_lines
.binary_search(&start)
.unwrap_or_else(|err_index| err_index);
.map_or_else(|err_index| err_index, |ok_index| ok_index);
}
parens += 1;
}
@@ -70,7 +70,7 @@ pub(crate) fn redundant_backslash(
let end = locator.line_start(token.start());
let end_index = continuation_lines
.binary_search(&end)
.unwrap_or_else(|err_index| err_index);
.map_or_else(|err_index| err_index, |ok_index| ok_index);
for continuation_line in &continuation_lines[start_index..end_index] {
let backslash_end = locator.line_end(*continuation_line);
let backslash_start = backslash_end - TextSize::new(1);

View File

@@ -205,9 +205,6 @@ mod tests {
}
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
#[test_case(Rule::UnusedImport, Path::new("__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_25__all/__init__.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
@@ -252,6 +249,19 @@ mod tests {
Ok(())
}
#[test]
fn init_unused_import_opt_in_to_fix() -> Result<()> {
let diagnostics = test_path(
Path::new("pyflakes/__init__.py"),
&LinterSettings {
ignore_init_module_imports: false,
..LinterSettings::for_rules(vec![Rule::UnusedImport])
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn default_builtins() -> Result<()> {
let diagnostics = test_path(
@@ -601,7 +611,7 @@ mod tests {
&indexer,
);
let LinterResult {
data: mut diagnostics,
data: (mut diagnostics, ..),
..
} = check_path(
Path::new("<filename>"),

View File

@@ -1,24 +1,21 @@
use std::borrow::Cow;
use std::iter;
use anyhow::{anyhow, bail, Result};
use anyhow::Result;
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Applicability, Diagnostic, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Stmt, StmtImportFrom};
use ruff_python_semantic::{AnyImport, Exceptions, Imported, NodeId, Scope};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix;
use crate::registry::Rule;
use crate::rules::{isort, isort::ImportSection, isort::ImportType};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum UnusedImportContext {
ExceptHandler,
Init { first_party: bool },
Init,
}
/// ## What it does
@@ -96,7 +93,7 @@ impl Violation for UnusedImport {
"`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
)
}
Some(UnusedImportContext::Init { .. }) => {
Some(UnusedImportContext::Init) => {
format!(
"`{name}` imported but unused; consider removing, adding to `__all__`, or using a redundant alias"
)
@@ -107,47 +104,14 @@ impl Violation for UnusedImport {
fn fix_title(&self) -> Option<String> {
let UnusedImport { name, multiple, .. } = self;
let resolution = match self.context {
Some(UnusedImportContext::Init { first_party: true }) => "Use a redundant alias",
_ => "Remove unused import",
};
Some(if *multiple {
resolution.to_string()
"Remove unused import".to_string()
} else {
format!("{resolution}: `{name}`")
format!("Remove unused import: `{name}`")
})
}
}
fn is_first_party(qualified_name: &str, level: u32, checker: &Checker) -> bool {
let category = isort::categorize(
qualified_name,
level,
&checker.settings.src,
checker.package(),
checker.settings.isort.detect_same_package,
&checker.settings.isort.known_modules,
checker.settings.target_version,
checker.settings.isort.no_sections,
&checker.settings.isort.section_order,
&checker.settings.isort.default_section,
);
matches! {
category,
ImportSection::Known(ImportType::FirstParty | ImportType::LocalFolder)
}
}
/// For some unused binding in an import statement...
///
/// __init__.py ∧ 1stpty → safe, convert to redundant-alias
/// __init__.py ∧ stdlib → unsafe, remove
/// __init__.py ∧ 3rdpty → unsafe, remove
///
/// ¬__init__.py ∧ 1stpty → safe, remove
/// ¬__init__.py ∧ stdlib → safe, remove
/// ¬__init__.py ∧ 3rdpty → safe, remove
///
pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut Vec<Diagnostic>) {
// Collect all unused imports by statement.
let mut unused: FxHashMap<(NodeId, Exceptions), Vec<ImportBinding>> = FxHashMap::default();
@@ -196,82 +160,42 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
}
let in_init = checker.path().ends_with("__init__.py");
let fix_init = checker.settings.preview.is_enabled();
let fix_init = !checker.settings.ignore_init_module_imports;
// Generate a diagnostic for every import, but share fixes across all imports within the same
// Generate a diagnostic for every import, but share a fix across all imports within the same
// statement (excluding those that are ignored).
for ((import_statement, exceptions), bindings) in unused {
for ((node_id, exceptions), imports) in unused {
let in_except_handler =
exceptions.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR);
let multiple = bindings.len() > 1;
let level = match checker.semantic().statement(import_statement) {
Stmt::Import(_) => 0,
Stmt::ImportFrom(StmtImportFrom { level, .. }) => *level,
_ => {
continue;
}
};
let multiple = imports.len() > 1;
// pair each binding with context; divide them by how we want to fix them
let (to_reexport, to_remove): (Vec<_>, Vec<_>) = bindings
.into_iter()
.map(|binding| {
let context = if in_except_handler {
Some(UnusedImportContext::ExceptHandler)
} else if in_init {
Some(UnusedImportContext::Init {
first_party: is_first_party(
&binding.import.qualified_name().to_string(),
level,
checker,
),
})
} else {
None
};
(binding, context)
})
.partition(|(_, context)| {
matches!(
context,
Some(UnusedImportContext::Init { first_party: true })
)
});
// generate fixes that are shared across bindings in the statement
let (fix_remove, fix_reexport) = if (!in_init || fix_init) && !in_except_handler {
(
fix_by_removing_imports(
checker,
import_statement,
to_remove.iter().map(|(binding, _)| binding),
in_init,
)
.ok(),
fix_by_reexporting(
checker,
import_statement,
to_reexport.iter().map(|(binding, _)| binding),
)
.ok(),
)
let fix = if (!in_init || fix_init) && !in_except_handler {
fix_imports(checker, node_id, &imports, in_init).ok()
} else {
(None, None)
None
};
for ((binding, context), fix) in iter::Iterator::chain(
iter::zip(to_remove, iter::repeat(fix_remove)),
iter::zip(to_reexport, iter::repeat(fix_reexport)),
) {
for ImportBinding {
import,
range,
parent_range,
} in imports
{
let mut diagnostic = Diagnostic::new(
UnusedImport {
name: binding.import.qualified_name().to_string(),
context,
name: import.qualified_name().to_string(),
context: if in_except_handler {
Some(UnusedImportContext::ExceptHandler)
} else if in_init {
Some(UnusedImportContext::Init)
} else {
None
},
multiple,
},
binding.range,
range,
);
if let Some(range) = binding.parent_range {
if let Some(range) = parent_range {
diagnostic.set_parent(range.start());
}
if !in_except_handler {
@@ -324,22 +248,20 @@ impl Ranged for ImportBinding<'_> {
}
/// Generate a [`Fix`] to remove unused imports from a statement.
fn fix_by_removing_imports<'a>(
fn fix_imports(
checker: &Checker,
node_id: NodeId,
imports: impl Iterator<Item = &'a ImportBinding<'a>>,
imports: &[ImportBinding],
in_init: bool,
) -> Result<Fix> {
let statement = checker.semantic().statement(node_id);
let parent = checker.semantic().parent_statement(node_id);
let member_names: Vec<Cow<'_, str>> = imports
.iter()
.map(|ImportBinding { import, .. }| import)
.map(Imported::member_name)
.collect();
if member_names.is_empty() {
bail!("Expected import bindings");
}
let edit = fix::edits::remove_unused_imports(
member_names.iter().map(AsRef::as_ref),
@@ -349,43 +271,15 @@ fn fix_by_removing_imports<'a>(
checker.stylist(),
checker.indexer(),
)?;
// It's unsafe to remove things from `__init__.py` because it can break public interfaces
let applicability = if in_init {
Applicability::Unsafe
} else {
Applicability::Safe
};
Ok(
Fix::applicable_edit(edit, applicability).isolate(Checker::isolation(
checker.semantic().parent_statement_id(node_id),
)),
)
}
/// Generate a [`Fix`] to make bindings in a statement explicit, by changing from `import a` to
/// `import a as a`.
fn fix_by_reexporting<'a>(
checker: &Checker,
node_id: NodeId,
imports: impl Iterator<Item = &'a ImportBinding<'a>>,
) -> Result<Fix> {
let statement = checker.semantic().statement(node_id);
let member_names = imports
.map(|binding| binding.import.member_name())
.collect::<Vec<_>>();
if member_names.is_empty() {
bail!("Expected import bindings");
}
let edits = fix::edits::make_redundant_alias(member_names.iter().map(AsRef::as_ref), statement);
// Only emit a fix if there are edits
let mut tail = edits.into_iter();
let head = tail.next().ok_or(anyhow!("No edits to make"))?;
let isolation = Checker::isolation(checker.semantic().parent_statement_id(node_id));
Ok(Fix::safe_edits(head, tail).isolate(isolation))
}

View File

@@ -1,42 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
__init__.py:19:8: F401 [*] `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
19 | import sys # F401: remove unused
| ^^^ F401
|
= help: Remove unused import: `sys`
Unsafe fix
16 16 | import argparse as argparse # Ok: is redundant alias
17 17 |
18 18 |
19 |-import sys # F401: remove unused
20 19 |
21 20 |
22 21 | # first-party
__init__.py:33:15: F401 [*] `.unused` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
33 | from . import unused # F401: change to redundant alias
| ^^^^^^ F401
|
= help: Use a redundant alias: `.unused`
Safe fix
30 30 | from . import aliased as aliased # Ok: is redundant alias
31 31 |
32 32 |
33 |-from . import unused # F401: change to redundant alias
33 |+from . import unused as unused # F401: change to redundant alias
34 34 |
35 35 |
36 36 | from . import renamed as bees # F401: no fix
__init__.py:36:26: F401 `.renamed` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
36 | from . import renamed as bees # F401: no fix
| ^^^^ F401
|
= help: Use a redundant alias: `.renamed`

View File

@@ -1,18 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
__init__.py:19:8: F401 [*] `sys` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
|
19 | import sys # F401: remove unused
| ^^^ F401
|
= help: Remove unused import: `sys`
Unsafe fix
16 16 | import argparse # Ok: is exported in __all__
17 17 |
18 18 |
19 |-import sys # F401: remove unused
20 19 |
21 20 |
22 21 | # first-party

View File

@@ -64,7 +64,7 @@ pub(crate) fn bad_staticmethod_argument(
..
} = func;
let Some(parent) = checker.semantic().first_non_type_parent_scope(scope) else {
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};

View File

@@ -49,7 +49,7 @@ pub(crate) fn no_self_use(
scope: &Scope,
diagnostics: &mut Vec<Diagnostic>,
) {
let Some(parent) = checker.semantic().first_non_type_parent_scope(scope) else {
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};

View File

@@ -74,7 +74,7 @@ pub(crate) fn singledispatch_method(
..
} = func;
let Some(parent) = checker.semantic().first_non_type_parent_scope(scope) else {
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};

View File

@@ -72,7 +72,7 @@ pub(crate) fn singledispatchmethod_function(
..
} = func;
let Some(parent) = checker.semantic().first_non_type_parent_scope(scope) else {
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};

View File

@@ -83,7 +83,7 @@ pub(crate) fn super_without_brackets(checker: &mut Checker, func: &Expr) {
return;
};
let Some(parent) = checker.semantic().first_non_type_parent_scope(scope) else {
let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else {
return;
};

View File

@@ -5,7 +5,6 @@ use anyhow::Result;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::{self as ast, Expr, ExprSlice, ExprSubscript, ExprTuple, Parameters, Stmt};
use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator;
@@ -29,7 +28,7 @@ use crate::importer::{ImportRequest, Importer};
/// import functools
///
/// nums = [1, 2, 3]
/// total = functools.reduce(lambda x, y: x + y, nums)
/// sum = functools.reduce(lambda x, y: x + y, nums)
/// ```
///
/// Use instead:
@@ -38,8 +37,10 @@ use crate::importer::{ImportRequest, Importer};
/// import operator
///
/// nums = [1, 2, 3]
/// total = functools.reduce(operator.add, nums)
/// sum = functools.reduce(operator.add, nums)
/// ```
///
/// ## References
#[violation]
pub struct ReimplementedOperator {
operator: Operator,
@@ -70,13 +71,6 @@ impl Violation for ReimplementedOperator {
/// FURB118
pub(crate) fn reimplemented_operator(checker: &mut Checker, target: &FunctionLike) {
// Ignore methods.
if target.kind() == FunctionLikeKind::Function {
if checker.semantic().current_scope().kind.is_class() {
return;
}
}
let Some(params) = target.parameters() else {
return;
};
@@ -119,7 +113,7 @@ impl Ranged for FunctionLike<'_> {
fn range(&self) -> TextRange {
match self {
Self::Lambda(expr) => expr.range(),
Self::Function(stmt) => stmt.identifier(),
Self::Function(stmt) => stmt.range(),
}
}
}

View File

@@ -778,18 +778,28 @@ FURB118.py:33:17: FURB118 [*] Use `operator.itemgetter(slice(None))` instead of
35 36 |
36 37 | def op_not2(x):
FURB118.py:36:5: FURB118 Use `operator.not_` instead of defining a function
FURB118.py:36:1: FURB118 Use `operator.not_` instead of defining a function
|
36 | def op_not2(x):
| ^^^^^^^ FURB118
37 | return not x
36 | / def op_not2(x):
37 | | return not x
| |________________^ FURB118
|
= help: Replace with `operator.not_`
FURB118.py:40:5: FURB118 Use `operator.add` instead of defining a function
FURB118.py:40:1: FURB118 Use `operator.add` instead of defining a function
|
40 | def op_add2(x, y):
| ^^^^^^^ FURB118
41 | return x + y
40 | / def op_add2(x, y):
41 | | return x + y
| |________________^ FURB118
|
= help: Replace with `operator.add`
FURB118.py:45:5: FURB118 Use `operator.add` instead of defining a function
|
44 | class Adder:
45 | def add(x, y):
| _____^
46 | | return x + y
| |____________________^ FURB118
|
= help: Replace with `operator.add`

View File

@@ -48,7 +48,7 @@ impl SortingStyle {
/// an "isort-style sort".
///
/// An isort-style sort sorts items first according to their casing:
/// SCREAMING_SNAKE_CASE names (conventionally used for global constants)
/// `SCREAMING_SNAKE_CASE` names (conventionally used for global constants)
/// come first, followed by CamelCase names (conventionally used for
/// classes), followed by anything else. Within each category,
/// a [natural sort](https://en.wikipedia.org/wiki/Natural_sort_order)

View File

@@ -10,9 +10,6 @@ use itertools::Itertools;
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Applicability, Diagnostic, FixAvailability};
use ruff_notebook::Notebook;
#[cfg(not(fuzzing))]
use ruff_notebook::NotebookError;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
@@ -32,6 +29,9 @@ use crate::rules::pycodestyle::rules::syntax_error;
use crate::settings::types::UnsafeFixes;
use crate::settings::{flags, LinterSettings};
use crate::source_kind::SourceKind;
use ruff_notebook::Notebook;
#[cfg(not(fuzzing))]
use ruff_notebook::NotebookError;
#[cfg(not(fuzzing))]
pub(crate) fn test_resource_path(path: impl AsRef<Path>) -> std::path::PathBuf {
@@ -123,7 +123,7 @@ pub(crate) fn test_contents<'a>(
&indexer,
);
let LinterResult {
data: diagnostics,
data: (diagnostics, _imports),
error,
} = check_path(
path,
@@ -190,7 +190,7 @@ pub(crate) fn test_contents<'a>(
);
let LinterResult {
data: fixed_diagnostics,
data: (fixed_diagnostics, _),
error: fixed_error,
} = check_path(
path,

View File

@@ -715,21 +715,17 @@ where
/// assert_eq!(format_import_from(1, None), ".".to_string());
/// assert_eq!(format_import_from(1, Some("foo")), ".foo".to_string());
/// ```
pub fn format_import_from(level: u32, module: Option<&str>) -> Cow<str> {
match (level, module) {
(0, Some(module)) => Cow::Borrowed(module),
(level, module) => {
let mut module_name =
String::with_capacity((level as usize) + module.map_or(0, str::len));
for _ in 0..level {
module_name.push('.');
}
if let Some(module) = module {
module_name.push_str(module);
}
Cow::Owned(module_name)
pub fn format_import_from(level: u32, module: Option<&str>) -> String {
let mut module_name = String::with_capacity(16);
if level > 0 {
for _ in 0..level {
module_name.push('.');
}
}
if let Some(module) = module {
module_name.push_str(module);
}
module_name
}
/// Format the member reference name for a relative import.
@@ -744,8 +740,9 @@ pub fn format_import_from(level: u32, module: Option<&str>) -> Cow<str> {
/// assert_eq!(format_import_from_member(1, Some("foo"), "bar"), ".foo.bar".to_string());
/// ```
pub fn format_import_from_member(level: u32, module: Option<&str>, member: &str) -> String {
let mut qualified_name =
String::with_capacity((level as usize) + module.map_or(0, str::len) + 1 + member.len());
let mut qualified_name = String::with_capacity(
(level as usize) + module.as_ref().map_or(0, |module| module.len()) + 1 + member.len(),
);
if level > 0 {
for _ in 0..level {
qualified_name.push('.');

View File

@@ -1,3 +1,8 @@
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// A representation of an individual name imported via any import statement.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnyImport<'a> {
@@ -112,3 +117,60 @@ impl FutureImport for AnyImport<'_> {
}
}
}
/// A representation of a module reference in an import statement.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ModuleImport {
module: String,
range: TextRange,
}
impl ModuleImport {
pub fn new(module: String, range: TextRange) -> Self {
Self { module, range }
}
}
impl From<&ModuleImport> for TextRange {
fn from(import: &ModuleImport) -> TextRange {
import.range
}
}
/// A representation of the import dependencies between modules.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ImportMap {
/// A map from dot-delimited module name to the list of imports in that module.
module_to_imports: FxHashMap<String, Vec<ModuleImport>>,
}
impl ImportMap {
pub fn new() -> Self {
Self {
module_to_imports: FxHashMap::default(),
}
}
pub fn insert(&mut self, module: String, imports_vec: Vec<ModuleImport>) {
self.module_to_imports.insert(module, imports_vec);
}
pub fn extend(&mut self, other: Self) {
self.module_to_imports.extend(other.module_to_imports);
}
pub fn iter(&self) -> std::collections::hash_map::Iter<String, Vec<ModuleImport>> {
self.module_to_imports.iter()
}
}
impl<'a> IntoIterator for &'a ImportMap {
type IntoIter = std::collections::hash_map::Iter<'a, String, Vec<ModuleImport>>;
type Item = (&'a String, &'a Vec<ModuleImport>);
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}

View File

@@ -1006,7 +1006,7 @@ impl ConversionFlag {
pub struct DebugText {
/// The text between the `{` and the expression node.
pub leading: String,
/// The text between the expression and the conversion, the `format_spec`, or the `}`, depending on what's present in the source
/// The text between the expression and the conversion, the format_spec, or the `}`, depending on what's present in the source
pub trailing: String,
}

View File

@@ -133,6 +133,21 @@ python scripts/check_ecosystem.py --checkouts target/checkouts --projects github
cargo run --bin ruff_dev -- format-dev --stability-check --error-file target/formatter-ecosystem-errors.txt --multi-project target/checkouts
```
**Shrinking** To shrink a formatter error from an entire file to a minimal reproducible example,
you can use `ruff_shrinking`:
```shell
cargo run --bin ruff_shrinking -- <your_file> target/shrinking.py "Unstable formatting" "target/debug/ruff_dev format-dev --stability-check target/shrinking.py"
```
The first argument is the input file, the second is the output file where the candidates
and the eventual minimized version will be written to. The third argument is a regex matching the
error message, e.g. "Unstable formatting" or "Formatter error". The last argument is the command
with the error, e.g. running the stability check on the candidate file. The script will try various
strategies to remove parts of the code. If the output of the command still matches, it will use that
slightly smaller code as starting point for the next iteration, otherwise it will revert and try
a different strategy until all strategies are exhausted.
## Helper structs
To abstract formatting something into a helper, create a new struct with the data you want to

View File

@@ -0,0 +1,75 @@
"""Take `format-dev --stability-check` output and shrink all stability errors into a
single Python file. Used to update https://github.com/astral-sh/ruff/issues/5828 ."""
from __future__ import annotations
import json
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from subprocess import check_output
from tempfile import NamedTemporaryFile
from tqdm import tqdm
root = Path(
check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip(),
)
target = root.joinpath("target")
error_report = target.joinpath("formatter-ecosystem-errors.txt")
error_lines_prefix = "Unstable formatting "
def get_filenames() -> list[str]:
files = []
for line in error_report.read_text().splitlines():
if not line.startswith(error_lines_prefix):
continue
files.append(line.removeprefix(error_lines_prefix))
return files
def shrink_file(file: str) -> tuple[str, str]:
"""Returns filename and minimization"""
with NamedTemporaryFile(suffix=".py") as temp_file:
print(f"Starting {file}")
ruff_dev = target.joinpath("release").joinpath("ruff_dev")
check_output(
[
target.joinpath("release").joinpath("ruff_shrinking"),
file,
temp_file.name,
"Unstable formatting",
f"{ruff_dev} format-dev --stability-check {temp_file.name}",
],
)
print(f"Finished {file}")
return file, Path(temp_file.name).read_text()
def main():
storage = target.joinpath("minimizations.json")
output_file = target.joinpath("minimizations.py")
if storage.is_file():
outputs = json.loads(storage.read_text())
else:
outputs = {}
files = sorted(set(get_filenames()) - set(outputs))
# Each process will saturate one core
with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
tasks = [executor.submit(shrink_file, file) for file in files]
for future in tqdm(as_completed(tasks), total=len(files)):
file, output = future.result()
outputs[file] = output
storage.write_text(json.dumps(outputs, indent=4))
# Write to one shareable python file
with output_file.open("w") as formatted:
for file, code in sorted(json.loads(storage.read_text()).items()):
file = file.split("/target/checkouts/")[1]
formatted.write(f"# {file}\n{code}\n")
if __name__ == "__main__":
main()

View File

@@ -246,7 +246,7 @@ impl<K: std::hash::Hash + Eq, V> MultiMap<K, V> {
/// Returns `true` if `key` has any *leading*, *dangling*, or *trailing* parts.
#[allow(unused)]
pub(super) fn has(&self, key: &K) -> bool {
self.index.contains_key(key)
self.index.get(key).is_some()
}
/// Returns the *leading*, *dangling*, and *trailing* parts of `key`.
@@ -382,16 +382,16 @@ where
#[derive(Clone, Debug)]
struct InOrderEntry {
/// Index into the [`MultiMap::parts`] vector where the leading parts of this entry start
/// Index into the [MultiMap::parts] vector where the leading parts of this entry start
leading_start: PartIndex,
/// Index into the [`MultiMap::parts`] vector where the dangling parts (and, thus, the leading parts end) start.
/// Index into the [MultiMap::parts] vector where the dangling parts (and, thus, the leading parts end) start.
dangling_start: PartIndex,
/// Index into the [`MultiMap::parts`] vector where the trailing parts (and, thus, the dangling parts end) of this entry start
/// Index into the [MultiMap::parts] vector where the trailing parts (and, thus, the dangling parts end) of this entry start
trailing_start: Option<PartIndex>,
/// Index into the [`MultiMap::parts`] vector where the trailing parts of this entry end
/// Index into the [MultiMap::parts] vector where the trailing parts of this entry end
trailing_end: Option<PartIndex>,
_count: Count<InOrderEntry>,
@@ -505,7 +505,7 @@ impl InOrderEntry {
#[derive(Clone, Debug)]
struct OutOfOrderEntry {
/// Index into the [`MultiMap::out_of_order`] vector at which offset the leaading vec is stored.
/// Index into the [MultiMap::out_of_order] vector at which offset the leaading vec is stored.
leading_index: usize,
_count: Count<OutOfOrderEntry>,
}

View File

@@ -195,9 +195,9 @@ type CommentsMap<'a> = MultiMap<NodeRefEqualityKey<'a>, SourceComment>;
/// Cloning `comments` is cheap as it only involves bumping a reference counter.
#[derive(Debug, Clone)]
pub(crate) struct Comments<'a> {
/// The implementation uses an [Rc] so that [Comments] has a lifetime independent from the [`crate::Formatter`].
/// Independent lifetimes are necessary to support the use case where a (formattable object)[`crate::Format`]
/// iterates over all comments, and writes them into the [`crate::Formatter`] (mutably borrowing the [`crate::Formatter`] and in turn its context).
/// The implementation uses an [Rc] so that [Comments] has a lifetime independent from the [crate::Formatter].
/// Independent lifetimes are necessary to support the use case where a (formattable object)[crate::Format]
/// iterates over all comments, and writes them into the [crate::Formatter] (mutably borrowing the [crate::Formatter] and in turn its context).
///
/// ```block
/// for leading in f.context().comments().leading_comments(node) {

View File

@@ -4,7 +4,7 @@ use ruff_python_ast::{Expr, ExprAttribute, ExprNumberLiteral, Number};
use ruff_python_trivia::{find_only_token_in_range, SimpleTokenKind};
use ruff_text_size::{Ranged, TextRange};
use crate::comments::dangling_comments;
use crate::comments::{dangling_comments, SourceComment};
use crate::expression::parentheses::{
is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
@@ -123,6 +123,15 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
write!(f, [format_inner])
}
}
fn fmt_dangling_comments(
&self,
_dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
// handle in `fmt_fields`
Ok(())
}
}
impl NeedsParentheses for ExprAttribute {

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprBinOp;
use crate::comments::SourceComment;
use crate::expression::binary_like::BinaryLike;
use crate::expression::has_parentheses;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
@@ -15,6 +16,15 @@ impl FormatNodeRule<ExprBinOp> for FormatExprBinOp {
fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> {
BinaryLike::Binary(item).fmt(f)
}
fn fmt_dangling_comments(
&self,
_dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
// Handled inside of `fmt_fields`
Ok(())
}
}
impl NeedsParentheses for ExprBinOp {

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprBytesLiteral;
use crate::comments::SourceComment;
use crate::expression::parentheses::{
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
};
@@ -20,6 +21,15 @@ impl FormatNodeRule<ExprBytesLiteral> for FormatExprBytesLiteral {
.fmt(f),
}
}
fn fmt_dangling_comments(
&self,
_dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
// Handled as part of `fmt_fields`
Ok(())
}
}
impl NeedsParentheses for ExprBytesLiteral {

View File

@@ -2,7 +2,7 @@ use ruff_formatter::FormatRuleWithOptions;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{Expr, ExprCall};
use crate::comments::dangling_comments;
use crate::comments::{dangling_comments, SourceComment};
use crate::expression::parentheses::{
is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses,
};
@@ -74,6 +74,14 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
fmt_func.fmt(f)
}
}
fn fmt_dangling_comments(
&self,
_dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
Ok(())
}
}
impl NeedsParentheses for ExprCall {

View File

@@ -2,6 +2,7 @@ use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{CmpOp, ExprCompare};
use crate::comments::SourceComment;
use crate::expression::binary_like::BinaryLike;
use crate::expression::has_parentheses;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
@@ -16,6 +17,16 @@ impl FormatNodeRule<ExprCompare> for FormatExprCompare {
fn fmt_fields(&self, item: &ExprCompare, f: &mut PyFormatter) -> FormatResult<()> {
BinaryLike::Compare(item).fmt(f)
}
fn fmt_dangling_comments(
&self,
dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
// Node can not have dangling comments
debug_assert!(dangling_comments.is_empty());
Ok(())
}
}
impl NeedsParentheses for ExprCompare {

View File

@@ -64,6 +64,15 @@ impl FormatNodeRule<ExprDict> for FormatExprDict {
.with_dangling_comments(open_parenthesis_comments)
.fmt(f)
}
fn fmt_dangling_comments(
&self,
_dangling_comments: &[SourceComment],
_f: &mut PyFormatter,
) -> FormatResult<()> {
// Handled by `fmt_fields`
Ok(())
}
}
impl NeedsParentheses for ExprDict {

Some files were not shown because too many files have changed in this diff Show More