Compare commits
38 Commits
alex/relat
...
gankra/nam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
901322f9de | ||
|
|
a59bf83854 | ||
|
|
4b569cea74 | ||
|
|
26bf64b9ef | ||
|
|
f87146ea54 | ||
|
|
b0abf9808e | ||
|
|
4cba2e8f91 | ||
|
|
1a7f53022a | ||
|
|
266a7bc4c5 | ||
|
|
3b7a5e4de8 | ||
|
|
93039d055d | ||
|
|
3b61da0da3 | ||
|
|
5933cc0101 | ||
|
|
2190fcebe0 | ||
|
|
df9d6886d4 | ||
|
|
5133fa4516 | ||
|
|
21c5cfe236 | ||
|
|
f97da18267 | ||
|
|
bc191f59b9 | ||
|
|
00f86c39e0 | ||
|
|
2ec29b7418 | ||
|
|
ab1ac254d9 | ||
|
|
01de8bef3e | ||
|
|
b59f6eb5e9 | ||
|
|
9ca78bdf76 | ||
|
|
d65542c05e | ||
|
|
98728b2c98 | ||
|
|
924b2972f2 | ||
|
|
d035744959 | ||
|
|
ce059c4857 | ||
|
|
acbc83d6d2 | ||
|
|
a9e5246786 | ||
|
|
8b8b174e4f | ||
|
|
28fa02129b | ||
|
|
a10e42294b | ||
|
|
12a4ca003f | ||
|
|
60f7ec90ef | ||
|
|
922d964bcb |
10
.github/CODEOWNERS
vendored
10
.github/CODEOWNERS
vendored
@@ -20,9 +20,11 @@
|
||||
# ty
|
||||
/crates/ty* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
|
||||
/crates/ruff_db/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty_project/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty_server/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty_project/ @carljm @MichaReiser @sharkdp @dcreager @Gankra
|
||||
/crates/ty_ide/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager @Gankra
|
||||
/crates/ty_server/ @carljm @MichaReiser @sharkdp @dcreager @Gankra
|
||||
/crates/ty/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty_wasm/ @carljm @MichaReiser @sharkdp @dcreager
|
||||
/crates/ty_wasm/ @carljm @MichaReiser @sharkdp @dcreager @Gankra
|
||||
/scripts/ty_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
|
||||
/crates/ty_python_semantic @carljm @AlexWaygood @sharkdp @dcreager
|
||||
/crates/ty_python_semantic/ @carljm @AlexWaygood @sharkdp @dcreager
|
||||
/crates/ty_module_resolver/ @carljm @MichaReiser @AlexWaygood @Gankra
|
||||
|
||||
@@ -10,6 +10,12 @@ Run all tests (using `nextest` for faster execution):
|
||||
cargo nextest run
|
||||
```
|
||||
|
||||
For faster test execution, use the `fast-test` profile which enables optimizations while retaining debug info:
|
||||
|
||||
```sh
|
||||
cargo nextest run --cargo-profile fast-test
|
||||
```
|
||||
|
||||
Run tests for a specific crate:
|
||||
|
||||
```sh
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4511,11 +4511,13 @@ dependencies = [
|
||||
"regex-automata",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
"ruff_macros",
|
||||
"ruff_memory_usage",
|
||||
"ruff_options_metadata",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_trivia",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"salsa",
|
||||
|
||||
@@ -335,6 +335,11 @@ strip = false
|
||||
debug = "full"
|
||||
lto = false
|
||||
|
||||
# Profile for faster iteration: applies minimal optimizations for faster tests.
|
||||
[profile.fast-test]
|
||||
inherits = "dev"
|
||||
opt-level = 1
|
||||
|
||||
# The profile that 'cargo dist' will build with.
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
@@ -221,7 +221,7 @@ fn setup_micro_case(code: &str) -> Case {
|
||||
let file_path = "src/test.py";
|
||||
fs.write_file_all(
|
||||
SystemPathBuf::from(file_path),
|
||||
ruff_python_trivia::textwrap::dedent(code),
|
||||
&*ruff_python_trivia::textwrap::dedent(code),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
@@ -49,3 +50,15 @@ impl CancellationToken {
|
||||
self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// The operation was canceled by the provided [`CancellationToken`].
|
||||
#[derive(Debug)]
|
||||
pub struct Canceled;
|
||||
|
||||
impl std::error::Error for Canceled {}
|
||||
|
||||
impl std::fmt::Display for Canceled {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("operation was canceled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,44 @@ impl Diagnostic {
|
||||
diag
|
||||
}
|
||||
|
||||
/// Adds sub diagnostics that tell the user that this is a bug in ty
|
||||
/// and asks them to open an issue on GitHub.
|
||||
pub fn add_bug_sub_diagnostics(&mut self, url_encoded_title: &str) {
|
||||
self.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"This indicates a bug in ty.",
|
||||
));
|
||||
|
||||
self.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format_args!(
|
||||
"If you could open an issue at https://github.com/astral-sh/ty/issues/new?title={url_encoded_title}, we'd be very appreciative!"
|
||||
),
|
||||
));
|
||||
self.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format!(
|
||||
"Platform: {os} {arch}",
|
||||
os = std::env::consts::OS,
|
||||
arch = std::env::consts::ARCH
|
||||
),
|
||||
));
|
||||
if let Some(version) = crate::program_version() {
|
||||
self.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format!("Version: {version}"),
|
||||
));
|
||||
}
|
||||
|
||||
self.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format!(
|
||||
"Args: {args:?}",
|
||||
args = std::env::args().collect::<Vec<_>>()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/// Add an annotation to this diagnostic.
|
||||
///
|
||||
/// Annotations for a diagnostic are optional, but if any are added,
|
||||
@@ -1019,6 +1057,13 @@ impl DiagnosticId {
|
||||
matches!(self, DiagnosticId::Lint(_))
|
||||
}
|
||||
|
||||
pub const fn as_lint(&self) -> Option<LintName> {
|
||||
match self {
|
||||
DiagnosticId::Lint(name) => Some(*name),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this `DiagnosticId` represents a lint with the given name.
|
||||
pub fn is_lint_named(&self, name: &str) -> bool {
|
||||
matches!(self, DiagnosticId::Lint(self_name) if self_name == name)
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::diagnostic::{Span, UnifiedFile};
|
||||
use crate::file_revision::FileRevision;
|
||||
use crate::files::file_root::FileRoots;
|
||||
use crate::files::private::FileStatus;
|
||||
use crate::source::SourceText;
|
||||
use crate::system::{SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf};
|
||||
use crate::vendored::{VendoredPath, VendoredPathBuf};
|
||||
use crate::{Db, FxDashMap, vendored};
|
||||
@@ -323,6 +324,17 @@ pub struct File {
|
||||
/// the file has been deleted is to change the status to `Deleted`.
|
||||
#[default]
|
||||
status: FileStatus,
|
||||
|
||||
/// Overrides the result of [`source_text`](crate::source::source_text).
|
||||
///
|
||||
/// This is useful when running queries after modifying a file's content but
|
||||
/// before the content is written to disk. For example, to verify that the applied fixes
|
||||
/// didn't introduce any new errors.
|
||||
///
|
||||
/// The override gets automatically removed the next time the file changes.
|
||||
#[default]
|
||||
#[returns(ref)]
|
||||
pub source_text_override: Option<SourceText>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
@@ -444,20 +456,28 @@ impl File {
|
||||
_ => (FileStatus::NotFound, FileRevision::zero(), None),
|
||||
};
|
||||
|
||||
let mut clear_override = false;
|
||||
|
||||
if file.status(db) != status {
|
||||
tracing::debug!("Updating the status of `{}`", file.path(db));
|
||||
file.set_status(db).to(status);
|
||||
clear_override = true;
|
||||
}
|
||||
|
||||
if file.revision(db) != revision {
|
||||
tracing::debug!("Updating the revision of `{}`", file.path(db));
|
||||
file.set_revision(db).to(revision);
|
||||
clear_override = true;
|
||||
}
|
||||
|
||||
if file.permissions(db) != permission {
|
||||
tracing::debug!("Updating the permissions of `{}`", file.path(db));
|
||||
file.set_permissions(db).to(permission);
|
||||
}
|
||||
|
||||
if clear_override && file.source_text_override(db).is_some() {
|
||||
file.set_source_text_override(db).to(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the file exists.
|
||||
@@ -526,7 +546,7 @@ impl VirtualFile {
|
||||
}
|
||||
|
||||
/// Increments the revision of the underlying [`File`].
|
||||
fn sync(&self, db: &mut dyn Db) {
|
||||
pub fn sync(&self, db: &mut dyn Db) {
|
||||
let file = self.0;
|
||||
tracing::debug!("Updating the revision of `{}`", file.path(db));
|
||||
let current_revision = file.revision(db);
|
||||
|
||||
@@ -85,6 +85,13 @@ pub fn max_parallelism() -> NonZeroUsize {
|
||||
})
|
||||
}
|
||||
|
||||
// Use a reasonably large stack size to avoid running into stack overflows too easily. The
|
||||
// size was chosen in such a way as to still be able to handle large expressions involving
|
||||
// binary operators (x + x + … + x) both during the AST walk in semantic index building as
|
||||
// well as during type checking. Using this stack size, we can handle handle expressions
|
||||
// that are several times larger than the corresponding limits in existing type checkers.
|
||||
pub const STACK_SIZE: usize = 16 * 1024 * 1024;
|
||||
|
||||
/// Trait for types that can provide Rust documentation.
|
||||
///
|
||||
/// Use `derive(RustDoc)` to automatically implement this trait for types that have a static string documentation.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_diagnostics::SourceMap;
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_source_file::LineIndex;
|
||||
@@ -16,6 +18,10 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
let _span = tracing::trace_span!("source_text", file = %path).entered();
|
||||
let mut read_error = None;
|
||||
|
||||
if let Some(source) = file.source_text_override(db) {
|
||||
return source.clone();
|
||||
}
|
||||
|
||||
let kind = if is_notebook(db.system(), path) {
|
||||
file.read_to_notebook(db)
|
||||
.unwrap_or_else(|error| {
|
||||
@@ -90,6 +96,45 @@ impl SourceText {
|
||||
pub fn read_error(&self) -> Option<&SourceTextError> {
|
||||
self.inner.read_error.as_ref()
|
||||
}
|
||||
|
||||
/// Returns a new instance for this file with the updated source text (Python code).
|
||||
///
|
||||
/// Uses the `source_map` to preserve the cell-boundaries.
|
||||
#[must_use]
|
||||
pub fn with_text(&self, new_text: String, source_map: &SourceMap) -> Self {
|
||||
let new_kind = match &self.inner.kind {
|
||||
SourceTextKind::Text(_) => SourceTextKind::Text(new_text),
|
||||
|
||||
SourceTextKind::Notebook { notebook } => {
|
||||
let mut new_notebook = notebook.as_ref().clone();
|
||||
new_notebook.update(source_map, new_text);
|
||||
SourceTextKind::Notebook {
|
||||
notebook: new_notebook.into(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
inner: Arc::new(SourceTextInner {
|
||||
kind: new_kind,
|
||||
read_error: self.inner.read_error.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Cow<'_, [u8]> {
|
||||
match &self.inner.kind {
|
||||
SourceTextKind::Text(source) => Cow::Borrowed(source.as_bytes()),
|
||||
SourceTextKind::Notebook { notebook } => {
|
||||
let mut output: Vec<u8> = Vec::new();
|
||||
notebook
|
||||
.write(&mut output)
|
||||
.expect("writing to a Vec should never fail");
|
||||
|
||||
Cow::Owned(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SourceText {
|
||||
@@ -117,13 +162,13 @@ impl std::fmt::Debug for SourceText {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize, Clone)]
|
||||
struct SourceTextInner {
|
||||
kind: SourceTextKind,
|
||||
read_error: Option<SourceTextError>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize, Clone)]
|
||||
enum SourceTextKind {
|
||||
Text(String),
|
||||
Notebook {
|
||||
|
||||
@@ -271,7 +271,12 @@ pub trait WritableSystem: System {
|
||||
fn create_new_file(&self, path: &SystemPath) -> Result<()>;
|
||||
|
||||
/// Writes the given content to the file at the given path.
|
||||
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()>;
|
||||
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
|
||||
self.write_file_bytes(path, content.as_bytes())
|
||||
}
|
||||
|
||||
/// Writes the given content to the file at the given path.
|
||||
fn write_file_bytes(&self, path: &SystemPath, content: &[u8]) -> Result<()>;
|
||||
|
||||
/// Creates a directory at `path` as well as any intermediate directories.
|
||||
fn create_directory_all(&self, path: &SystemPath) -> Result<()>;
|
||||
@@ -311,6 +316,8 @@ pub trait WritableSystem: System {
|
||||
|
||||
Ok(Some(cache_path))
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn WritableSystem>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -122,7 +122,9 @@ impl MemoryFileSystem {
|
||||
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
||||
|
||||
match entry {
|
||||
Entry::File(file) => Ok(file.content.clone()),
|
||||
Entry::File(file) => {
|
||||
String::from_utf8(file.content.to_vec()).map_err(|_| invalid_utf8())
|
||||
}
|
||||
Entry::Directory(_) => Err(is_a_directory()),
|
||||
}
|
||||
}
|
||||
@@ -139,7 +141,7 @@ impl MemoryFileSystem {
|
||||
.get(&path.as_ref().to_path_buf())
|
||||
.ok_or_else(not_found)?;
|
||||
|
||||
Ok(file.content.clone())
|
||||
String::from_utf8(file.content.to_vec()).map_err(|_| invalid_utf8())
|
||||
}
|
||||
|
||||
pub fn exists(&self, path: &SystemPath) -> bool {
|
||||
@@ -161,7 +163,7 @@ impl MemoryFileSystem {
|
||||
match by_path.entry(normalized) {
|
||||
btree_map::Entry::Vacant(entry) => {
|
||||
entry.insert(Entry::File(File {
|
||||
content: String::new(),
|
||||
content: Box::default(),
|
||||
last_modified: file_time_now(),
|
||||
}));
|
||||
|
||||
@@ -177,13 +179,17 @@ impl MemoryFileSystem {
|
||||
/// Stores a new file in the file system.
|
||||
///
|
||||
/// The operation overrides the content for an existing file with the same normalized `path`.
|
||||
pub fn write_file(&self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
|
||||
pub fn write_file(
|
||||
&self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
content: impl AsRef<[u8]>,
|
||||
) -> Result<()> {
|
||||
let mut by_path = self.inner.by_path.write().unwrap();
|
||||
|
||||
let normalized = self.normalize_path(path.as_ref());
|
||||
|
||||
let file = get_or_create_file(&mut by_path, &normalized)?;
|
||||
file.content = content.to_string();
|
||||
file.content = content.as_ref().to_vec().into_boxed_slice();
|
||||
file.last_modified = file_time_now();
|
||||
|
||||
Ok(())
|
||||
@@ -214,7 +220,7 @@ impl MemoryFileSystem {
|
||||
pub fn write_file_all(
|
||||
&self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
content: impl ToString,
|
||||
content: impl AsRef<[u8]>,
|
||||
) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
@@ -228,19 +234,24 @@ impl MemoryFileSystem {
|
||||
/// Stores a new virtual file in the file system.
|
||||
///
|
||||
/// The operation overrides the content for an existing virtual file with the same `path`.
|
||||
pub fn write_virtual_file(&self, path: impl AsRef<SystemVirtualPath>, content: impl ToString) {
|
||||
pub fn write_virtual_file(
|
||||
&self,
|
||||
path: impl AsRef<SystemVirtualPath>,
|
||||
content: impl AsRef<[u8]>,
|
||||
) {
|
||||
let path = path.as_ref();
|
||||
let mut virtual_files = self.inner.virtual_files.write().unwrap();
|
||||
let content = content.as_ref().to_vec().into_boxed_slice();
|
||||
|
||||
match virtual_files.entry(path.to_path_buf()) {
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(File {
|
||||
content: content.to_string(),
|
||||
content,
|
||||
last_modified: file_time_now(),
|
||||
});
|
||||
}
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().content = content.to_string();
|
||||
entry.get_mut().content = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,7 +479,7 @@ impl Entry {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct File {
|
||||
content: String,
|
||||
content: Box<[u8]>,
|
||||
last_modified: FileTime,
|
||||
}
|
||||
|
||||
@@ -497,6 +508,13 @@ fn directory_not_empty() -> std::io::Error {
|
||||
std::io::Error::other("directory not empty")
|
||||
}
|
||||
|
||||
fn invalid_utf8() -> std::io::Error {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"stream did not contain valid UTF-8",
|
||||
)
|
||||
}
|
||||
|
||||
fn create_dir_all(
|
||||
paths: &mut RwLockWriteGuard<BTreeMap<Utf8PathBuf, Entry>>,
|
||||
normalized: &Utf8Path,
|
||||
@@ -533,7 +551,7 @@ fn get_or_create_file<'a>(
|
||||
|
||||
let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
|
||||
Entry::File(File {
|
||||
content: String::new(),
|
||||
content: Box::default(),
|
||||
last_modified: file_time_now(),
|
||||
})
|
||||
});
|
||||
@@ -844,7 +862,7 @@ mod tests {
|
||||
let fs = with_files(["c.py"]);
|
||||
|
||||
let error = fs
|
||||
.write_file(SystemPath::new("a/b.py"), "content".to_string())
|
||||
.write_file(SystemPath::new("a/b.py"), "content")
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.kind(), ErrorKind::NotFound);
|
||||
@@ -855,7 +873,7 @@ mod tests {
|
||||
let fs = with_files(["a/b.py"]);
|
||||
|
||||
let error = fs
|
||||
.write_file_all(SystemPath::new("a/b.py/c"), "content".to_string())
|
||||
.write_file_all(SystemPath::new("a/b.py/c"), "content")
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.kind(), ErrorKind::Other);
|
||||
@@ -878,7 +896,7 @@ mod tests {
|
||||
let fs = MemoryFileSystem::new();
|
||||
let path = SystemPath::new("a.py");
|
||||
|
||||
fs.write_file_all(path, "Test content".to_string())?;
|
||||
fs.write_file_all(path, "Test content")?;
|
||||
|
||||
assert_eq!(fs.read_to_string(path)?, "Test content");
|
||||
|
||||
@@ -915,9 +933,7 @@ mod tests {
|
||||
|
||||
fs.create_directory_all("a")?;
|
||||
|
||||
let error = fs
|
||||
.write_file(SystemPath::new("a"), "content".to_string())
|
||||
.unwrap_err();
|
||||
let error = fs.write_file(SystemPath::new("a"), "content").unwrap_err();
|
||||
|
||||
assert_eq!(error.kind(), ErrorKind::Other);
|
||||
|
||||
|
||||
@@ -361,13 +361,17 @@ impl WritableSystem for OsSystem {
|
||||
std::fs::File::create_new(path).map(drop)
|
||||
}
|
||||
|
||||
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
|
||||
fn write_file_bytes(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
|
||||
std::fs::write(path.as_std_path(), content)
|
||||
}
|
||||
|
||||
fn create_directory_all(&self, path: &SystemPath) -> Result<()> {
|
||||
std::fs::create_dir_all(path.as_std_path())
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn WritableSystem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OsSystem {
|
||||
|
||||
@@ -205,13 +205,17 @@ impl WritableSystem for TestSystem {
|
||||
self.system().create_new_file(path)
|
||||
}
|
||||
|
||||
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
|
||||
self.system().write_file(path, content)
|
||||
fn write_file_bytes(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
|
||||
self.system().write_file_bytes(path, content)
|
||||
}
|
||||
|
||||
fn create_directory_all(&self, path: &SystemPath) -> Result<()> {
|
||||
self.system().create_directory_all(path)
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn WritableSystem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for databases that use a [`WritableSystem`].
|
||||
@@ -283,7 +287,11 @@ pub trait DbWithTestSystem: Db + Sized {
|
||||
///
|
||||
/// ## Panics
|
||||
/// If the db isn't using the [`InMemorySystem`].
|
||||
fn write_virtual_file(&mut self, path: impl AsRef<SystemVirtualPath>, content: impl ToString) {
|
||||
fn write_virtual_file(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemVirtualPath>,
|
||||
content: impl AsRef<[u8]>,
|
||||
) {
|
||||
let path = path.as_ref();
|
||||
self.test_system()
|
||||
.memory_file_system()
|
||||
@@ -322,23 +330,23 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct InMemorySystem {
|
||||
user_config_directory: Mutex<Option<SystemPathBuf>>,
|
||||
user_config_directory: Arc<Mutex<Option<SystemPathBuf>>>,
|
||||
memory_fs: MemoryFileSystem,
|
||||
}
|
||||
|
||||
impl InMemorySystem {
|
||||
pub fn new(cwd: SystemPathBuf) -> Self {
|
||||
Self {
|
||||
user_config_directory: Mutex::new(None),
|
||||
user_config_directory: Mutex::new(None).into(),
|
||||
memory_fs: MemoryFileSystem::with_current_directory(cwd),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_memory_fs(memory_fs: MemoryFileSystem) -> Self {
|
||||
Self {
|
||||
user_config_directory: Mutex::new(None),
|
||||
user_config_directory: Mutex::new(None).into(),
|
||||
memory_fs,
|
||||
}
|
||||
}
|
||||
@@ -440,10 +448,7 @@ impl System for InMemorySystem {
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn System> {
|
||||
Box::new(Self {
|
||||
user_config_directory: Mutex::new(self.user_config_directory.lock().unwrap().clone()),
|
||||
memory_fs: self.memory_fs.clone(),
|
||||
})
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,11 +457,15 @@ impl WritableSystem for InMemorySystem {
|
||||
self.memory_fs.create_new_file(path)
|
||||
}
|
||||
|
||||
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
|
||||
fn write_file_bytes(&self, path: &SystemPath, content: &[u8]) -> Result<()> {
|
||||
self.memory_fs.write_file(path, content)
|
||||
}
|
||||
|
||||
fn create_directory_all(&self, path: &SystemPath) -> Result<()> {
|
||||
self.memory_fs.create_directory_all(path)
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn WritableSystem> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
|
||||
use crate::fix::{FixResult, fix_file};
|
||||
use crate::noqa::add_noqa;
|
||||
use crate::package::PackageRoot;
|
||||
use crate::preview::is_py315_support_enabled;
|
||||
use crate::registry::Rule;
|
||||
#[cfg(any(feature = "test-rules", test))]
|
||||
use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule};
|
||||
@@ -33,7 +34,7 @@ use crate::settings::types::UnsafeFixes;
|
||||
use crate::settings::{LinterSettings, TargetVersion, flags};
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::suppression::Suppressions;
|
||||
use crate::{Locator, directives, fs};
|
||||
use crate::{Locator, directives, fs, warn_user_once};
|
||||
|
||||
pub(crate) mod float;
|
||||
|
||||
@@ -450,6 +451,14 @@ pub fn lint_only(
|
||||
) -> LinterResult {
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
|
||||
if matches!(target_version.linter_version(), PythonVersion::PY315)
|
||||
&& !is_py315_support_enabled(settings)
|
||||
{
|
||||
warn_user_once!(
|
||||
"Support for Python 3.15 is under development and may be unstable. Enable `preview` to remove this warning."
|
||||
);
|
||||
}
|
||||
|
||||
let parsed = source.into_parsed(source_kind, source_type, target_version.parser_version());
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
@@ -555,6 +564,14 @@ pub fn lint_fix<'a>(
|
||||
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
|
||||
if matches!(target_version.linter_version(), PythonVersion::PY315)
|
||||
&& !is_py315_support_enabled(settings)
|
||||
{
|
||||
warn_user_once!(
|
||||
"Support for Python 3.15 is under development and may be unstable. Enable `preview` to remove this warning."
|
||||
);
|
||||
}
|
||||
|
||||
// Continuously fix until the source code stabilizes.
|
||||
loop {
|
||||
// Parse once.
|
||||
|
||||
@@ -296,3 +296,8 @@ pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled(
|
||||
pub(crate) const fn is_range_suppressions_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/22419
|
||||
pub(crate) const fn is_py315_support_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -36,13 +36,16 @@ use crate::{Fix, FixAvailability, Violation};
|
||||
/// ```python
|
||||
/// import logging
|
||||
///
|
||||
/// logging.basicConfig(level=logging.INFO)
|
||||
/// logger = logging.getLogger(__name__)
|
||||
///
|
||||
///
|
||||
/// def sum_less_than_four(a, b):
|
||||
/// logger.debug("Calling sum_less_than_four")
|
||||
/// return a + b < 4
|
||||
///
|
||||
///
|
||||
/// if __name__ == "__main__":
|
||||
/// logging.basicConfig(level=logging.INFO)
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
|
||||
@@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::add_argument;
|
||||
use crate::{AlwaysFixableViolation, Applicability, Fix};
|
||||
use crate::{Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `subprocess.run` without an explicit `check` argument.
|
||||
@@ -39,9 +39,12 @@ use crate::{AlwaysFixableViolation, Applicability, Fix};
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe for function calls that contain
|
||||
/// `**kwargs`, as adding a `check` keyword argument to such a call may lead
|
||||
/// to a duplicate keyword argument error.
|
||||
///
|
||||
/// This rule's fix is marked as display-only because it's not clear whether the
|
||||
/// potential exception was meant to be ignored by setting `check=False` or if
|
||||
/// the author simply forgot to include `check=True`. The fix adds
|
||||
/// `check=False`, making the existing behavior explicit but possibly masking
|
||||
/// the original intention.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run)
|
||||
@@ -49,14 +52,18 @@ use crate::{AlwaysFixableViolation, Applicability, Fix};
|
||||
#[violation_metadata(stable_since = "v0.0.285")]
|
||||
pub(crate) struct SubprocessRunWithoutCheck;
|
||||
|
||||
impl AlwaysFixableViolation for SubprocessRunWithoutCheck {
|
||||
impl Violation for SubprocessRunWithoutCheck {
|
||||
// The fix is always set on the diagnostic, but display-only fixes aren't
|
||||
// considered "fixable" in the tests.
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`subprocess.run` without explicit `check` argument".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Add explicit `check=False`".to_string()
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Add explicit `check=False`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,20 +81,11 @@ pub(crate) fn subprocess_run_without_check(checker: &Checker, call: &ast::ExprCa
|
||||
if call.arguments.find_keyword("check").is_none() {
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(SubprocessRunWithoutCheck, call.func.range());
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
add_argument("check=False", &call.arguments, checker.tokens()),
|
||||
// If the function call contains `**kwargs`, mark the fix as unsafe.
|
||||
if call
|
||||
.arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.any(|keyword| keyword.arg.is_none())
|
||||
{
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
},
|
||||
));
|
||||
diagnostic.set_fix(Fix::display_only_edit(add_argument(
|
||||
"check=False",
|
||||
&call.arguments,
|
||||
checker.tokens(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ help: Add explicit `check=False`
|
||||
5 | subprocess.run("ls", shell=True)
|
||||
6 | subprocess.run(
|
||||
7 | ["ls"],
|
||||
note: This is a display-only fix and is likely to be incorrect
|
||||
|
||||
PLW1510 [*] `subprocess.run` without explicit `check` argument
|
||||
--> subprocess_run_without_check.py:5:1
|
||||
@@ -39,6 +40,7 @@ help: Add explicit `check=False`
|
||||
6 | subprocess.run(
|
||||
7 | ["ls"],
|
||||
8 | shell=False,
|
||||
note: This is a display-only fix and is likely to be incorrect
|
||||
|
||||
PLW1510 [*] `subprocess.run` without explicit `check` argument
|
||||
--> subprocess_run_without_check.py:6:1
|
||||
@@ -59,6 +61,7 @@ help: Add explicit `check=False`
|
||||
9 | )
|
||||
10 | subprocess.run(["ls"], **kwargs)
|
||||
11 |
|
||||
note: This is a display-only fix and is likely to be incorrect
|
||||
|
||||
PLW1510 [*] `subprocess.run` without explicit `check` argument
|
||||
--> subprocess_run_without_check.py:10:1
|
||||
@@ -79,4 +82,4 @@ help: Add explicit `check=False`
|
||||
11 |
|
||||
12 | # Non-errors.
|
||||
13 | subprocess.run("ls", check=True)
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
note: This is a display-only fix and is likely to be incorrect
|
||||
|
||||
@@ -7,7 +7,7 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
|
||||
--- Summary ---
|
||||
Removed: 15
|
||||
Added: 23
|
||||
Added: 20
|
||||
|
||||
--- Removed ---
|
||||
E741 Ambiguous variable name: `I`
|
||||
@@ -301,6 +301,7 @@ RUF100 [*] Unused suppression (non-enabled: `E501`)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
47 | I = 1
|
||||
48 | # ruff: enable[E501]
|
||||
| --------------------
|
||||
|
|
||||
help: Remove unused suppression
|
||||
43 | def f():
|
||||
@@ -308,26 +309,10 @@ help: Remove unused suppression
|
||||
45 | # logged to user
|
||||
- # ruff: disable[E501]
|
||||
46 | I = 1
|
||||
47 | # ruff: enable[E501]
|
||||
48 |
|
||||
|
||||
|
||||
RUF100 [*] Unused suppression (non-enabled: `E501`)
|
||||
--> suppressions.py:48:5
|
||||
|
|
||||
46 | # ruff: disable[E501]
|
||||
47 | I = 1
|
||||
48 | # ruff: enable[E501]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Remove unused suppression
|
||||
45 | # logged to user
|
||||
46 | # ruff: disable[E501]
|
||||
47 | I = 1
|
||||
- # ruff: enable[E501]
|
||||
47 |
|
||||
48 |
|
||||
49 |
|
||||
50 | def f():
|
||||
49 | def f():
|
||||
|
||||
|
||||
RUF100 [*] Unused `noqa` directive (unused: `E741`, `F841`)
|
||||
@@ -563,6 +548,9 @@ RUF102 [*] Invalid rule code in suppression: YF829
|
||||
| ^^^^^
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
97 | # ruff: enable[YF829]
|
||||
| -----
|
||||
|
|
||||
help: Remove the rule code
|
||||
90 |
|
||||
@@ -572,6 +560,10 @@ help: Remove the rule code
|
||||
93 | # ruff: disable[F841, RQW320]
|
||||
94 | value = 0
|
||||
95 | # ruff: enable[F841, RQW320]
|
||||
- # ruff: enable[YF829]
|
||||
96 |
|
||||
97 |
|
||||
98 | def f():
|
||||
|
||||
|
||||
RUF102 [*] Invalid rule code in suppression: RQW320
|
||||
@@ -583,6 +575,8 @@ RUF102 [*] Invalid rule code in suppression: RQW320
|
||||
| ^^^^^^
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
| ------
|
||||
97 | # ruff: enable[YF829]
|
||||
|
|
||||
help: Remove the rule code
|
||||
91 | def f():
|
||||
@@ -590,23 +584,6 @@ help: Remove the rule code
|
||||
93 | # ruff: disable[YF829]
|
||||
- # ruff: disable[F841, RQW320]
|
||||
94 + # ruff: disable[F841]
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
97 | # ruff: enable[YF829]
|
||||
|
||||
|
||||
RUF102 [*] Invalid rule code in suppression: RQW320
|
||||
--> suppressions.py:96:26
|
||||
|
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
| ^^^^^^
|
||||
97 | # ruff: enable[YF829]
|
||||
|
|
||||
help: Remove the rule code
|
||||
93 | # ruff: disable[YF829]
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
- # ruff: enable[F841, RQW320]
|
||||
96 + # ruff: enable[F841]
|
||||
@@ -615,24 +592,6 @@ help: Remove the rule code
|
||||
99 |
|
||||
|
||||
|
||||
RUF102 [*] Invalid rule code in suppression: YF829
|
||||
--> suppressions.py:97:20
|
||||
|
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
97 | # ruff: enable[YF829]
|
||||
| ^^^^^
|
||||
|
|
||||
help: Remove the rule code
|
||||
94 | # ruff: disable[F841, RQW320]
|
||||
95 | value = 0
|
||||
96 | # ruff: enable[F841, RQW320]
|
||||
- # ruff: enable[YF829]
|
||||
97 |
|
||||
98 |
|
||||
99 | def f():
|
||||
|
||||
|
||||
RUF103 [*] Invalid suppression comment: missing suppression codes like `[E501, ...]`
|
||||
--> suppressions.py:109:5
|
||||
|
|
||||
|
||||
@@ -36,6 +36,7 @@ pub enum PythonVersion {
|
||||
Py312,
|
||||
Py313,
|
||||
Py314,
|
||||
Py315,
|
||||
}
|
||||
|
||||
impl Default for PythonVersion {
|
||||
@@ -58,6 +59,7 @@ impl TryFrom<ast::PythonVersion> for PythonVersion {
|
||||
ast::PythonVersion::PY312 => Ok(Self::Py312),
|
||||
ast::PythonVersion::PY313 => Ok(Self::Py313),
|
||||
ast::PythonVersion::PY314 => Ok(Self::Py314),
|
||||
ast::PythonVersion::PY315 => Ok(Self::Py315),
|
||||
_ => Err(format!("unrecognized python version {value}")),
|
||||
}
|
||||
}
|
||||
@@ -88,6 +90,7 @@ impl PythonVersion {
|
||||
Self::Py312 => (3, 12),
|
||||
Self::Py313 => (3, 13),
|
||||
Self::Py314 => (3, 14),
|
||||
Self::Py315 => (3, 15),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ use ruff_python_trivia::Cursor;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
use crate::codes::Rule;
|
||||
use crate::fix::edits::delete_comment;
|
||||
@@ -24,6 +23,7 @@ use crate::rules::ruff::rules::{
|
||||
UnmatchedSuppressionComment, UnusedCodes, UnusedNOQA, UnusedNOQAKind, code_is_valid,
|
||||
};
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{Locator, Violation};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum SuppressionAction {
|
||||
@@ -85,11 +85,33 @@ pub(crate) struct Suppression {
|
||||
/// Range for which the suppression applies
|
||||
range: TextRange,
|
||||
|
||||
/// Any comments associated with the suppression
|
||||
comments: SmallVec<[SuppressionComment; 2]>,
|
||||
|
||||
/// Whether this suppression actually suppressed a diagnostic
|
||||
used: Cell<bool>,
|
||||
|
||||
comments: DisableEnableComments,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum DisableEnableComments {
|
||||
/// An implicitly closed disable comment without a matching enable comment.
|
||||
Disable(SuppressionComment),
|
||||
/// A matching pair of disable and enable comments.
|
||||
DisableEnable(SuppressionComment, SuppressionComment),
|
||||
}
|
||||
|
||||
impl DisableEnableComments {
|
||||
pub(crate) fn disable_comment(&self) -> &SuppressionComment {
|
||||
match self {
|
||||
DisableEnableComments::Disable(comment) => comment,
|
||||
DisableEnableComments::DisableEnable(disable, _) => disable,
|
||||
}
|
||||
}
|
||||
pub(crate) fn enable_comment(&self) -> Option<&SuppressionComment> {
|
||||
match self {
|
||||
DisableEnableComments::Disable(_) => None,
|
||||
DisableEnableComments::DisableEnable(_, enable) => Some(enable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -171,23 +193,16 @@ impl Suppressions {
|
||||
if !code_is_valid(&suppression.code, &context.settings().external) {
|
||||
// InvalidRuleCode
|
||||
if context.is_rule_enabled(Rule::InvalidRuleCode) {
|
||||
for comment in &suppression.comments {
|
||||
let (range, edit) = Suppressions::delete_code_or_comment(
|
||||
locator,
|
||||
suppression,
|
||||
comment,
|
||||
true,
|
||||
);
|
||||
context
|
||||
.report_diagnostic(
|
||||
InvalidRuleCode {
|
||||
rule_code: suppression.code.to_string(),
|
||||
kind: InvalidRuleCodeKind::Suppression,
|
||||
},
|
||||
range,
|
||||
)
|
||||
.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
Suppressions::report_suppression(
|
||||
context,
|
||||
locator,
|
||||
suppression,
|
||||
true,
|
||||
InvalidRuleCode {
|
||||
rule_code: suppression.code.to_string(),
|
||||
kind: InvalidRuleCodeKind::Suppression,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if !suppression.used.get() {
|
||||
// UnusedNOQA
|
||||
@@ -197,42 +212,37 @@ impl Suppressions {
|
||||
) else {
|
||||
continue; // "external" lint code, don't treat it as unused
|
||||
};
|
||||
for comment in &suppression.comments {
|
||||
let (range, edit) = Suppressions::delete_code_or_comment(
|
||||
locator,
|
||||
suppression,
|
||||
comment,
|
||||
false,
|
||||
);
|
||||
|
||||
let codes = if context.is_rule_enabled(rule) {
|
||||
UnusedCodes {
|
||||
unmatched: vec![suppression.code.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
UnusedCodes {
|
||||
disabled: vec![suppression.code.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let codes = if context.is_rule_enabled(rule) {
|
||||
UnusedCodes {
|
||||
unmatched: vec![suppression.code.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
UnusedCodes {
|
||||
disabled: vec![suppression.code.to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
context
|
||||
.report_diagnostic(
|
||||
UnusedNOQA {
|
||||
codes: Some(codes),
|
||||
kind: UnusedNOQAKind::Suppression,
|
||||
},
|
||||
range,
|
||||
)
|
||||
.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
Suppressions::report_suppression(
|
||||
context,
|
||||
locator,
|
||||
suppression,
|
||||
false,
|
||||
UnusedNOQA {
|
||||
codes: Some(codes),
|
||||
kind: UnusedNOQAKind::Suppression,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if suppression.comments.len() == 1 {
|
||||
} else if let DisableEnableComments::Disable(comment) = &suppression.comments {
|
||||
// UnmatchedSuppressionComment
|
||||
let range = suppression.comments[0].range;
|
||||
if unmatched_ranges.insert(range) {
|
||||
context.report_diagnostic_if_enabled(UnmatchedSuppressionComment {}, range);
|
||||
if unmatched_ranges.insert(comment.range) {
|
||||
context.report_diagnostic_if_enabled(
|
||||
UnmatchedSuppressionComment {},
|
||||
comment.range,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,6 +277,35 @@ impl Suppressions {
|
||||
}
|
||||
}
|
||||
|
||||
fn report_suppression<T: Violation>(
|
||||
context: &LintContext,
|
||||
locator: &Locator,
|
||||
suppression: &Suppression,
|
||||
highlight_only_code: bool,
|
||||
kind: T,
|
||||
) {
|
||||
let disable_comment = suppression.comments.disable_comment();
|
||||
let (range, edit) = Suppressions::delete_code_or_comment(
|
||||
locator,
|
||||
suppression,
|
||||
disable_comment,
|
||||
highlight_only_code,
|
||||
);
|
||||
let mut diagnostic = context.report_diagnostic(kind, range);
|
||||
if let Some(enable_comment) = suppression.comments.enable_comment() {
|
||||
let (enable_range, enable_range_edit) = Suppressions::delete_code_or_comment(
|
||||
locator,
|
||||
suppression,
|
||||
enable_comment,
|
||||
highlight_only_code,
|
||||
);
|
||||
diagnostic.secondary_annotation("", enable_range);
|
||||
diagnostic.set_fix(Fix::safe_edits(edit, [enable_range_edit]));
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(edit));
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_code_or_comment(
|
||||
locator: &Locator<'_>,
|
||||
suppression: &Suppression,
|
||||
@@ -424,7 +463,10 @@ impl<'a> SuppressionsBuilder<'a> {
|
||||
self.valid.push(Suppression {
|
||||
code: code.into(),
|
||||
range: combined_range,
|
||||
comments: smallvec![comment.comment.clone(), other.comment.clone()],
|
||||
comments: DisableEnableComments::DisableEnable(
|
||||
comment.comment.clone(),
|
||||
other.comment.clone(),
|
||||
),
|
||||
used: false.into(),
|
||||
});
|
||||
}
|
||||
@@ -441,7 +483,7 @@ impl<'a> SuppressionsBuilder<'a> {
|
||||
self.valid.push(Suppression {
|
||||
code: code.into(),
|
||||
range: implicit_range,
|
||||
comments: smallvec![comment.comment.clone()],
|
||||
comments: DisableEnableComments::Disable(comment.comment.clone()),
|
||||
used: false.into(),
|
||||
});
|
||||
}
|
||||
@@ -643,7 +685,7 @@ mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
use itertools::Itertools;
|
||||
use ruff_python_parser::{Mode, ParseOptions, parse};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use similar::DiffableStr;
|
||||
|
||||
use crate::{
|
||||
@@ -705,24 +747,22 @@ print('hello')
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo]\nprint('hello')\n# ruff: enable[foo]",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[foo]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[foo]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
@@ -751,30 +791,28 @@ def foo():
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[bar]\n print('hello')\n\n",
|
||||
code: "bar",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: None,
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo]\nprint('hello')\n\ndef foo():\n # ruff: disable[bar]\n print('hello')\n\n",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: None,
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
@@ -803,46 +841,42 @@ class Foo:
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[bar]\n print('hello')\n # ruff: enable[bar]",
|
||||
code: "bar",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[bar]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[bar]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo]\n def bar(self):\n # ruff: disable[bar]\n print('hello')\n # ruff: enable[bar]\n # ruff: enable[foo]",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[foo]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[foo]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
@@ -872,46 +906,42 @@ def foo():
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo]\n print('hello')\n # ruff: disable[bar]\n print('hello')\n # ruff: enable[foo]",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[foo]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[foo]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[bar]\n print('hello')\n # ruff: enable[foo]\n print('hello')\n # ruff: enable[bar]",
|
||||
code: "bar",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[bar]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[bar]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
@@ -936,50 +966,46 @@ print('hello')
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo, bar]\nprint('hello')\n# ruff: enable[foo, bar]",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo, bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[foo, bar]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo, bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[foo, bar]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo, bar]\nprint('hello')\n# ruff: enable[foo, bar]",
|
||||
code: "bar",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo, bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[foo, bar]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo, bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[foo, bar]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
@@ -1005,16 +1031,15 @@ print('world')
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo]\nprint('hello')\n# ruff: enable[bar]\nprint('world')\n",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: None,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
@@ -1051,32 +1076,30 @@ print('hello')
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo, bar]\nprint('hello')\n# ruff: enable[bar, foo]\n",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo, bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo, bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: None,
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo, bar]\nprint('hello')\n# ruff: enable[bar, foo]\n",
|
||||
code: "bar",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo, bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo, bar]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
"bar",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: None,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
@@ -1116,38 +1139,35 @@ print('hello')
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo] first\nprint('hello')\n# ruff: disable[foo] second\nprint('hello')\n# ruff: enable[foo]",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo] first",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "first",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[foo]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo] first",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "first",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[foo]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[foo] second\nprint('hello')\n# ruff: enable[foo]\n",
|
||||
code: "foo",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[foo] second",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "second",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[foo] second",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"foo",
|
||||
],
|
||||
reason: "second",
|
||||
},
|
||||
enable_comment: None,
|
||||
},
|
||||
],
|
||||
invalid: [],
|
||||
@@ -1189,100 +1209,92 @@ def bar():
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[delta] unmatched\n pass\n # ruff: enable[beta,gamma]\n# ruff: enable[alpha]\n\n# ruff: disable # parse error!\n",
|
||||
code: "delta",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[delta] unmatched",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"delta",
|
||||
],
|
||||
reason: "unmatched",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[delta] unmatched",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"delta",
|
||||
],
|
||||
reason: "unmatched",
|
||||
},
|
||||
enable_comment: None,
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[beta,gamma]\n if True:\n # ruff: disable[delta] unmatched\n pass\n # ruff: enable[beta,gamma]",
|
||||
code: "beta",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[beta,gamma]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"beta",
|
||||
"gamma",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[beta,gamma]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"beta",
|
||||
"gamma",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[beta,gamma]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"beta",
|
||||
"gamma",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[beta,gamma]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"beta",
|
||||
"gamma",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[beta,gamma]\n if True:\n # ruff: disable[delta] unmatched\n pass\n # ruff: enable[beta,gamma]",
|
||||
code: "gamma",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[beta,gamma]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"beta",
|
||||
"gamma",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[beta,gamma]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"beta",
|
||||
"gamma",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[beta,gamma]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"beta",
|
||||
"gamma",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[beta,gamma]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"beta",
|
||||
"gamma",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[zeta] unmatched\n pass\n# ruff: enable[zeta] underindented\n pass\n",
|
||||
code: "zeta",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[zeta] unmatched",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"zeta",
|
||||
],
|
||||
reason: "unmatched",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[zeta] unmatched",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"zeta",
|
||||
],
|
||||
reason: "unmatched",
|
||||
},
|
||||
enable_comment: None,
|
||||
},
|
||||
Suppression {
|
||||
covered_source: "# ruff: disable[alpha]\ndef foo():\n # ruff: disable[beta,gamma]\n if True:\n # ruff: disable[delta] unmatched\n pass\n # ruff: enable[beta,gamma]\n# ruff: enable[alpha]",
|
||||
code: "alpha",
|
||||
comments: [
|
||||
SuppressionComment {
|
||||
text: "# ruff: disable[alpha]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"alpha",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
SuppressionComment {
|
||||
text: "# ruff: enable[alpha]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"alpha",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
],
|
||||
disable_comment: SuppressionComment {
|
||||
text: "# ruff: disable[alpha]",
|
||||
action: Disable,
|
||||
codes: [
|
||||
"alpha",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
enable_comment: SuppressionComment {
|
||||
text: "# ruff: enable[alpha]",
|
||||
action: Enable,
|
||||
codes: [
|
||||
"alpha",
|
||||
],
|
||||
reason: "",
|
||||
},
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
@@ -1532,10 +1544,8 @@ def bar():
|
||||
#[test]
|
||||
fn comment_attributes() {
|
||||
let source = "# ruff: disable[foo, bar] hello world";
|
||||
let mut parser = SuppressionParser::new(
|
||||
source,
|
||||
TextRange::new(0.into(), TextSize::try_from(source.len()).unwrap()),
|
||||
);
|
||||
let mut parser =
|
||||
SuppressionParser::new(source, TextRange::new(0.into(), source.text_len()));
|
||||
let comment = parser.parse_comment().unwrap();
|
||||
assert_eq!(comment.action, SuppressionAction::Disable);
|
||||
assert_eq!(
|
||||
@@ -1554,12 +1564,12 @@ def bar():
|
||||
source: &'_ str,
|
||||
) -> Result<DebugSuppressionComment<'_>, ParseError> {
|
||||
let offset = TextSize::new(source.find('#').unwrap_or(0).try_into().unwrap());
|
||||
let mut parser = SuppressionParser::new(
|
||||
source,
|
||||
TextRange::new(offset, TextSize::try_from(source.len()).unwrap()),
|
||||
);
|
||||
let mut parser = SuppressionParser::new(source, TextRange::new(offset, source.text_len()));
|
||||
match parser.parse_comment() {
|
||||
Ok(comment) => Ok(DebugSuppressionComment { source, comment }),
|
||||
Ok(comment) => Ok(DebugSuppressionComment {
|
||||
source,
|
||||
comment: Some(comment),
|
||||
}),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
@@ -1639,16 +1649,18 @@ def bar():
|
||||
.field("covered_source", &&self.source[self.suppression.range])
|
||||
.field("code", &self.suppression.code)
|
||||
.field(
|
||||
"comments",
|
||||
&self
|
||||
.suppression
|
||||
.comments
|
||||
.iter()
|
||||
.map(|comment| DebugSuppressionComment {
|
||||
source: self.source,
|
||||
comment: comment.clone(),
|
||||
})
|
||||
.collect_vec(),
|
||||
"disable_comment",
|
||||
&DebugSuppressionComment {
|
||||
source: self.source,
|
||||
comment: Some(self.suppression.comments.disable_comment().clone()),
|
||||
},
|
||||
)
|
||||
.field(
|
||||
"enable_comment",
|
||||
&DebugSuppressionComment {
|
||||
source: self.source,
|
||||
comment: self.suppression.comments.enable_comment().cloned(),
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
@@ -1667,7 +1679,7 @@ def bar():
|
||||
"comment",
|
||||
&DebugSuppressionComment {
|
||||
source: self.source,
|
||||
comment: self.invalid.comment.clone(),
|
||||
comment: Some(self.invalid.comment.clone()),
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
@@ -1690,23 +1702,27 @@ def bar():
|
||||
|
||||
struct DebugSuppressionComment<'a> {
|
||||
source: &'a str,
|
||||
comment: SuppressionComment,
|
||||
comment: Option<SuppressionComment>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for DebugSuppressionComment<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("SuppressionComment")
|
||||
.field("text", &&self.source[self.comment.range])
|
||||
.field("action", &self.comment.action)
|
||||
.field(
|
||||
"codes",
|
||||
&DebugCodes {
|
||||
source: self.source,
|
||||
codes: &self.comment.codes,
|
||||
},
|
||||
)
|
||||
.field("reason", &&self.source[self.comment.reason])
|
||||
.finish()
|
||||
match &self.comment {
|
||||
Some(comment) => f
|
||||
.debug_struct("SuppressionComment")
|
||||
.field("text", &&self.source[comment.range])
|
||||
.field("action", &comment.action)
|
||||
.field(
|
||||
"codes",
|
||||
&DebugCodes {
|
||||
source: self.source,
|
||||
codes: &comment.codes,
|
||||
},
|
||||
)
|
||||
.field("reason", &&self.source[comment.reason])
|
||||
.finish(),
|
||||
None => f.debug_tuple("None").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ impl PythonVersion {
|
||||
major: 3,
|
||||
minor: 14,
|
||||
};
|
||||
pub const PY315: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 15,
|
||||
};
|
||||
|
||||
pub fn iter() -> impl Iterator<Item = PythonVersion> {
|
||||
[
|
||||
@@ -46,6 +50,7 @@ impl PythonVersion {
|
||||
PythonVersion::PY312,
|
||||
PythonVersion::PY313,
|
||||
PythonVersion::PY314,
|
||||
PythonVersion::PY315,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
@@ -61,7 +66,7 @@ impl PythonVersion {
|
||||
|
||||
/// The latest Python version supported in preview
|
||||
pub fn latest_preview() -> Self {
|
||||
let latest_preview = Self::PY314;
|
||||
let latest_preview = Self::PY315;
|
||||
debug_assert!(latest_preview >= Self::latest());
|
||||
latest_preview
|
||||
}
|
||||
|
||||
@@ -91,20 +91,22 @@ def example(session):
|
||||
.all()
|
||||
# fmt: on
|
||||
def off_and_on_without_data():
|
||||
"""All comments here are technically on the same prefix.
|
||||
|
||||
The comments between will be formatted. This is a known limitation.
|
||||
"""
|
||||
"""Test that comment-only fmt:off/on blocks preserve formatting."""
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
|
||||
|
||||
#hey, that won't work
|
||||
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
pass
|
||||
def on_and_off_broken():
|
||||
"""Another known limitation."""
|
||||
def on_and_off_with_comment_only_blocks():
|
||||
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
this=should.not_be.formatted()
|
||||
@@ -113,7 +115,16 @@ def on_and_off_broken():
|
||||
now . considers . multiple . fmt . directives . within . one . prefix
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
# ...but comments still get reformatted even though they should not be
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
def long_lines():
|
||||
if True:
|
||||
@@ -178,6 +189,50 @@ cfg.rule(
|
||||
# fmt: on
|
||||
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5
|
||||
)
|
||||
|
||||
# Test comment-only blocks at file level with various spacing patterns
|
||||
# fmt: off
|
||||
#nospace
|
||||
# twospaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#nospaceatall
|
||||
#extraspaces
|
||||
#evenmorespaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#SBATCH --job-name=test
|
||||
#SBATCH --output=test.out
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#first
|
||||
|
||||
#second
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#!@#$%^&*()
|
||||
#<=>+-*/
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#x=1+2
|
||||
#y = 3
|
||||
#z = 4
|
||||
# fmt: on
|
||||
|
||||
# fmt: off
|
||||
yield 'hello'
|
||||
# No formatting to the end of the file
|
||||
|
||||
@@ -112,29 +112,42 @@ def example(session):
|
||||
|
||||
|
||||
def off_and_on_without_data():
|
||||
"""All comments here are technically on the same prefix.
|
||||
|
||||
The comments between will be formatted. This is a known limitation.
|
||||
"""
|
||||
"""Test that comment-only fmt:off/on blocks preserve formatting."""
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
|
||||
# hey, that won't work
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
|
||||
def on_and_off_broken():
|
||||
"""Another known limitation."""
|
||||
def on_and_off_with_comment_only_blocks():
|
||||
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
this=should.not_be.formatted()
|
||||
and_=indeed . it is not formatted
|
||||
because . the . handling . inside . generate_ignored_nodes()
|
||||
now . considers . multiple . fmt . directives . within . one . prefix
|
||||
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
# ...but comments still get reformatted even though they should not be
|
||||
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -211,6 +224,50 @@ cfg.rule(
|
||||
# fmt: on
|
||||
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5,
|
||||
)
|
||||
|
||||
# Test comment-only blocks at file level with various spacing patterns
|
||||
# fmt: off
|
||||
#nospace
|
||||
# twospaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#nospaceatall
|
||||
#extraspaces
|
||||
#evenmorespaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#SBATCH --job-name=test
|
||||
#SBATCH --output=test.out
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#first
|
||||
|
||||
#second
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#!@#$%^&*()
|
||||
#<=>+-*/
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#x=1+2
|
||||
#y = 3
|
||||
#z = 4
|
||||
# fmt: on
|
||||
|
||||
# fmt: off
|
||||
yield 'hello'
|
||||
# No formatting to the end of the file
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
def foo(): return "mock" # fmt: skip
|
||||
if True: print("yay") # fmt: skip
|
||||
for i in range(10): print(i) # fmt: skip
|
||||
if True: print("this"); print("that") # fmt: skip
|
||||
while True: print("loop"); break # fmt: skip
|
||||
for x in [1, 2]: print(x); print("done") # fmt: skip
|
||||
def f(x: int): return x # fmt: skip
|
||||
|
||||
j = 1 # fmt: skip
|
||||
while j < 10: j += 1 # fmt: skip
|
||||
|
||||
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
|
||||
|
||||
v = (
|
||||
foo_dict # fmt: skip
|
||||
.setdefault("a", {})
|
||||
.setdefault("b", {})
|
||||
.setdefault("c", {})
|
||||
.setdefault("d", {})
|
||||
.setdefault("e", {})
|
||||
)
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
def foo(): return "mock" # fmt: skip
|
||||
if True: print("yay") # fmt: skip
|
||||
for i in range(10): print(i) # fmt: skip
|
||||
if True: print("this"); print("that") # fmt: skip
|
||||
while True: print("loop"); break # fmt: skip
|
||||
for x in [1, 2]: print(x); print("done") # fmt: skip
|
||||
def f(x: int): return x # fmt: skip
|
||||
|
||||
j = 1 # fmt: skip
|
||||
while j < 10: j += 1 # fmt: skip
|
||||
|
||||
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
|
||||
|
||||
v = (
|
||||
foo_dict # fmt: skip
|
||||
.setdefault("a", {})
|
||||
.setdefault("b", {})
|
||||
.setdefault("c", {})
|
||||
.setdefault("d", {})
|
||||
.setdefault("e", {})
|
||||
)
|
||||
|
||||
@@ -4,3 +4,84 @@ def foo():
|
||||
|
||||
# comment 1 # fmt: skip
|
||||
# comment 2
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3,
|
||||
4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3,
|
||||
4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3,
|
||||
4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3,
|
||||
4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
if False:
|
||||
# fmt: off # some other comment
|
||||
pass
|
||||
|
||||
@@ -4,3 +4,84 @@ def foo():
|
||||
|
||||
# comment 1 # fmt: skip
|
||||
# comment 2
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3,
|
||||
4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3,
|
||||
4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3,
|
||||
4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3,
|
||||
4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
if False:
|
||||
# fmt: off # some other comment
|
||||
pass
|
||||
|
||||
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.options.json
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.options.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
8
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.py
vendored
Normal file
8
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
with open("file.txt") as f: content = f.read() # fmt: skip
|
||||
|
||||
# Ideally, only the last line would be ignored
|
||||
# But ignoring only part of the asexpr_test causes a parse error
|
||||
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
|
||||
with open (
|
||||
"file.txt" ,
|
||||
) as f: content = f.read() # fmt: skip
|
||||
8
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.py.expect
vendored
Normal file
8
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip12.py.expect
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
with open("file.txt") as f: content = f.read() # fmt: skip
|
||||
|
||||
# Ideally, only the last line would be ignored
|
||||
# But ignoring only part of the asexpr_test causes a parse error
|
||||
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
|
||||
with open (
|
||||
"file.txt" ,
|
||||
) as f: content = f.read() # fmt: skip
|
||||
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.options.json
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.options.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
28
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.py
vendored
Normal file
28
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
t = (
|
||||
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
|
||||
t = (
|
||||
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{"foo": "bar",},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{"foo": "bar",},
|
||||
)
|
||||
32
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.py.expect
vendored
Normal file
32
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip13.py.expect
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
t = (
|
||||
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
|
||||
t = (
|
||||
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
a = "this is some code"
|
||||
b = 5 #fmt:skip
|
||||
b = 5 # fmt:skip
|
||||
c = 9 #fmt: skip
|
||||
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" #fmt:skip
|
||||
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
a = "this is some code"
|
||||
b = 5 # fmt:skip
|
||||
c = 9 # fmt: skip
|
||||
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
|
||||
b = 5 # fmt:skip
|
||||
c = 9 #fmt: skip
|
||||
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
|
||||
|
||||
19
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip_multiple_in_clause.py
vendored
Normal file
19
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip_multiple_in_clause.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Multiple fmt: skip in multi-part if-clause
|
||||
class ClassWithALongName:
|
||||
Constant1 = 1
|
||||
Constant2 = 2
|
||||
Constant3 = 3
|
||||
|
||||
|
||||
def test():
|
||||
if (
|
||||
"cond1" == "cond1"
|
||||
and "cond2" == "cond2"
|
||||
and 1 in (
|
||||
ClassWithALongName.Constant1,
|
||||
ClassWithALongName.Constant2,
|
||||
ClassWithALongName.Constant3, # fmt: skip
|
||||
) # fmt: skip
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,19 @@
|
||||
# Multiple fmt: skip in multi-part if-clause
|
||||
class ClassWithALongName:
|
||||
Constant1 = 1
|
||||
Constant2 = 2
|
||||
Constant3 = 3
|
||||
|
||||
|
||||
def test():
|
||||
if (
|
||||
"cond1" == "cond1"
|
||||
and "cond2" == "cond2"
|
||||
and 1 in (
|
||||
ClassWithALongName.Constant1,
|
||||
ClassWithALongName.Constant2,
|
||||
ClassWithALongName.Constant3, # fmt: skip
|
||||
) # fmt: skip
|
||||
):
|
||||
return True
|
||||
return False
|
||||
35
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip_multiple_strings.py
vendored
Normal file
35
crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip_multiple_strings.py
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Multiple fmt: skip on string literals
|
||||
a = (
|
||||
"this should " # fmt: skip
|
||||
"be fine"
|
||||
)
|
||||
|
||||
b = (
|
||||
"this is " # fmt: skip
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
c = (
|
||||
"and neither " # fmt: skip
|
||||
"is this " # fmt: skip
|
||||
"working"
|
||||
)
|
||||
|
||||
d = (
|
||||
"nor "
|
||||
"is this " # fmt: skip
|
||||
"working" # fmt: skip
|
||||
)
|
||||
|
||||
e = (
|
||||
"and this " # fmt: skip
|
||||
"is definitely "
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
# Dictionary entries with fmt: skip (covers issue with long lines)
|
||||
hotkeys = {
|
||||
"editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
"editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# Multiple fmt: skip on string literals
|
||||
a = (
|
||||
"this should " # fmt: skip
|
||||
"be fine"
|
||||
)
|
||||
|
||||
b = (
|
||||
"this is " # fmt: skip
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
c = (
|
||||
"and neither " # fmt: skip
|
||||
"is this " # fmt: skip
|
||||
"working"
|
||||
)
|
||||
|
||||
d = (
|
||||
"nor "
|
||||
"is this " # fmt: skip
|
||||
"working" # fmt: skip
|
||||
)
|
||||
|
||||
e = (
|
||||
"and this " # fmt: skip
|
||||
"is definitely "
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
# Dictionary entries with fmt: skip (covers issue with long lines)
|
||||
hotkeys = {
|
||||
"editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
"editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
}
|
||||
24
crates/ruff_python_formatter/resources/test/fixtures/black/cases/jupytext_markdown_fmt.py
vendored
Normal file
24
crates/ruff_python_formatter/resources/test/fixtures/black/cases/jupytext_markdown_fmt.py
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Test that Jupytext markdown comments are preserved before fmt:off/on blocks
|
||||
# %% [markdown]
|
||||
|
||||
# fmt: off
|
||||
# fmt: on
|
||||
|
||||
# Also test with other comments
|
||||
# Some comment
|
||||
# %% [markdown]
|
||||
# Another comment
|
||||
|
||||
# fmt: off
|
||||
x = 1
|
||||
# fmt: on
|
||||
|
||||
# Test multiple markdown comments
|
||||
# %% [markdown]
|
||||
# First markdown
|
||||
# %% [code]
|
||||
# Code cell
|
||||
|
||||
# fmt: off
|
||||
y = 2
|
||||
# fmt: on
|
||||
24
crates/ruff_python_formatter/resources/test/fixtures/black/cases/jupytext_markdown_fmt.py.expect
vendored
Normal file
24
crates/ruff_python_formatter/resources/test/fixtures/black/cases/jupytext_markdown_fmt.py.expect
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Test that Jupytext markdown comments are preserved before fmt:off/on blocks
|
||||
# %% [markdown]
|
||||
|
||||
# fmt: off
|
||||
# fmt: on
|
||||
|
||||
# Also test with other comments
|
||||
# Some comment
|
||||
# %% [markdown]
|
||||
# Another comment
|
||||
|
||||
# fmt: off
|
||||
x = 1
|
||||
# fmt: on
|
||||
|
||||
# Test multiple markdown comments
|
||||
# %% [markdown]
|
||||
# First markdown
|
||||
# %% [code]
|
||||
# Code cell
|
||||
|
||||
# fmt: off
|
||||
y = 2
|
||||
# fmt: on
|
||||
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.options.json
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.options.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"target_version": "3.14"}
|
||||
40
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.py
vendored
Normal file
40
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.py
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
x = t"foo"
|
||||
x = t'foo {{ {2 + 2}bar {{ baz'
|
||||
|
||||
x = t"foo {f'abc'} bar"
|
||||
|
||||
x = t"""foo {{ a
|
||||
foo {2 + 2}bar {{ baz
|
||||
|
||||
x = f"foo {{ {
|
||||
2 + 2 # comment
|
||||
}bar"
|
||||
|
||||
{{ baz
|
||||
|
||||
}} buzz
|
||||
|
||||
{print("abc" + "def"
|
||||
)}
|
||||
abc"""
|
||||
|
||||
t'{(abc:=10)}'
|
||||
|
||||
t'''This is a really long string, but just make sure that you reflow tstrings {
|
||||
2+2:d
|
||||
}'''
|
||||
t'This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}'
|
||||
|
||||
t"{ 2 + 2 = }"
|
||||
|
||||
t'{
|
||||
X
|
||||
!r
|
||||
}'
|
||||
|
||||
tr'\{{\}}'
|
||||
|
||||
t'''
|
||||
WITH {f'''
|
||||
{1}_cte AS ()'''}
|
||||
'''
|
||||
40
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.py.expect
vendored
Normal file
40
crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_750.py.expect
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
x = t"foo"
|
||||
x = t"foo {{ {2 + 2}bar {{ baz"
|
||||
|
||||
x = t"foo {f'abc'} bar"
|
||||
|
||||
x = t"""foo {{ a
|
||||
foo {2 + 2}bar {{ baz
|
||||
|
||||
x = f"foo {{ {
|
||||
2 + 2 # comment
|
||||
}bar"
|
||||
|
||||
{{ baz
|
||||
|
||||
}} buzz
|
||||
|
||||
{print("abc" + "def"
|
||||
)}
|
||||
abc"""
|
||||
|
||||
t"{(abc:=10)}"
|
||||
|
||||
t"""This is a really long string, but just make sure that you reflow tstrings {
|
||||
2+2:d
|
||||
}"""
|
||||
t"This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}"
|
||||
|
||||
t"{ 2 + 2 = }"
|
||||
|
||||
t"{
|
||||
X
|
||||
!r
|
||||
}"
|
||||
|
||||
rt"\{{\}}"
|
||||
|
||||
t"""
|
||||
WITH {f'''
|
||||
{1}_cte AS ()'''}
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
19
crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_fmtpass_imports.py
vendored
Normal file
19
crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_fmtpass_imports.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
@@ -0,0 +1,19 @@
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
@@ -156,24 +156,6 @@ Please use `--build-option` instead,
|
||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||
"""
|
||||
|
||||
this_will_become_one_line = (
|
||||
"a"
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
this_will_stay_on_three_lines = (
|
||||
"a" # comment
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
this_will_also_become_one_line = ( # comment
|
||||
"a"
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
assert some_var == expected_result, """
|
||||
test
|
||||
"""
|
||||
|
||||
@@ -198,16 +198,6 @@ Please use `--build-option` instead,
|
||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||
"""
|
||||
|
||||
this_will_become_one_line = "abc"
|
||||
|
||||
this_will_stay_on_three_lines = (
|
||||
"a" # comment
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
this_will_also_become_one_line = "abc" # comment
|
||||
|
||||
assert some_var == expected_result, """
|
||||
test
|
||||
"""
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
@@ -0,0 +1,10 @@
|
||||
def foo(
|
||||
a, #type:int
|
||||
b, #type: str
|
||||
c, # type: List[int]
|
||||
d, # type: Dict[int, str]
|
||||
e, # type: ignore
|
||||
f, # type : ignore
|
||||
g, # type : ignore
|
||||
):
|
||||
pass
|
||||
@@ -0,0 +1,10 @@
|
||||
def foo(
|
||||
a, # type: int
|
||||
b, # type: str
|
||||
c, # type: List[int]
|
||||
d, # type: Dict[int, str]
|
||||
e, # type: ignore
|
||||
f, # type : ignore
|
||||
g, # type : ignore
|
||||
):
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
{"preview": "enabled"}
|
||||
16
crates/ruff_python_formatter/resources/test/fixtures/black/cases/remove_parens_from_lhs.py
vendored
Normal file
16
crates/ruff_python_formatter/resources/test/fixtures/black/cases/remove_parens_from_lhs.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Remove unnecessary parentheses from LHS of assignments
|
||||
|
||||
|
||||
def a():
|
||||
return [1, 2, 3]
|
||||
|
||||
|
||||
# Single variable with unnecessary parentheses
|
||||
(b) = a()[0]
|
||||
|
||||
# Tuple unpacking with unnecessary parentheses
|
||||
(c, *_) = a()
|
||||
|
||||
# These should not be changed - parentheses are necessary
|
||||
(d,) = a() # single-element tuple
|
||||
e = (1 + 2) * 3 # RHS has precedence needs
|
||||
@@ -0,0 +1,16 @@
|
||||
# Remove unnecessary parentheses from LHS of assignments
|
||||
|
||||
|
||||
def a():
|
||||
return [1, 2, 3]
|
||||
|
||||
|
||||
# Single variable with unnecessary parentheses
|
||||
b = a()[0]
|
||||
|
||||
# Tuple unpacking with unnecessary parentheses
|
||||
c, *_ = a()
|
||||
|
||||
# These should not be changed - parentheses are necessary
|
||||
(d,) = a() # single-element tuple
|
||||
e = (1 + 2) * 3 # RHS has precedence needs
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtonoff.py
|
||||
---
|
||||
## Input
|
||||
|
||||
@@ -98,20 +97,22 @@ def example(session):
|
||||
.all()
|
||||
# fmt: on
|
||||
def off_and_on_without_data():
|
||||
"""All comments here are technically on the same prefix.
|
||||
|
||||
The comments between will be formatted. This is a known limitation.
|
||||
"""
|
||||
"""Test that comment-only fmt:off/on blocks preserve formatting."""
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
|
||||
|
||||
#hey, that won't work
|
||||
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
pass
|
||||
def on_and_off_broken():
|
||||
"""Another known limitation."""
|
||||
def on_and_off_with_comment_only_blocks():
|
||||
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
this=should.not_be.formatted()
|
||||
@@ -120,7 +121,16 @@ def on_and_off_broken():
|
||||
now . considers . multiple . fmt . directives . within . one . prefix
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
# ...but comments still get reformatted even though they should not be
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
def long_lines():
|
||||
if True:
|
||||
@@ -185,6 +195,50 @@ cfg.rule(
|
||||
# fmt: on
|
||||
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5
|
||||
)
|
||||
|
||||
# Test comment-only blocks at file level with various spacing patterns
|
||||
# fmt: off
|
||||
#nospace
|
||||
# twospaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#nospaceatall
|
||||
#extraspaces
|
||||
#evenmorespaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#SBATCH --job-name=test
|
||||
#SBATCH --output=test.out
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#first
|
||||
|
||||
#second
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#!@#$%^&*()
|
||||
#<=>+-*/
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#x=1+2
|
||||
#y = 3
|
||||
#z = 4
|
||||
# fmt: on
|
||||
|
||||
# fmt: off
|
||||
yield 'hello'
|
||||
# No formatting to the end of the file
|
||||
@@ -225,28 +279,16 @@ d={'a':1,
|
||||
# fmt: on
|
||||
goes + here,
|
||||
andhere,
|
||||
@@ -118,8 +119,10 @@
|
||||
"""
|
||||
# fmt: off
|
||||
|
||||
- # hey, that won't work
|
||||
|
||||
+ #hey, that won't work
|
||||
+
|
||||
+
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
@@ -134,7 +137,7 @@
|
||||
@@ -136,7 +137,7 @@
|
||||
and_=indeed . it is not formatted
|
||||
because . the . handling . inside . generate_ignored_nodes()
|
||||
now . considers . multiple . fmt . directives . within . one . prefix
|
||||
# fmt: on
|
||||
-
|
||||
+ # fmt: on
|
||||
# fmt: off
|
||||
- # ...but comments still get reformatted even though they should not be
|
||||
+ # ...but comments still get reformatted even though they should not be
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -174,14 +177,18 @@
|
||||
@@ -187,14 +188,18 @@
|
||||
$
|
||||
""",
|
||||
# fmt: off
|
||||
@@ -387,22 +429,24 @@ def example(session):
|
||||
|
||||
|
||||
def off_and_on_without_data():
|
||||
"""All comments here are technically on the same prefix.
|
||||
|
||||
The comments between will be formatted. This is a known limitation.
|
||||
"""
|
||||
"""Test that comment-only fmt:off/on blocks preserve formatting."""
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
|
||||
#should not be formatted
|
||||
|
||||
#hey, that won't work
|
||||
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
|
||||
def on_and_off_broken():
|
||||
"""Another known limitation."""
|
||||
def on_and_off_with_comment_only_blocks():
|
||||
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
this=should.not_be.formatted()
|
||||
@@ -411,7 +455,16 @@ def on_and_off_broken():
|
||||
now . considers . multiple . fmt . directives . within . one . prefix
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
# ...but comments still get reformatted even though they should not be
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -492,6 +545,50 @@ cfg.rule(
|
||||
# fmt: on
|
||||
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5,
|
||||
)
|
||||
|
||||
# Test comment-only blocks at file level with various spacing patterns
|
||||
# fmt: off
|
||||
#nospace
|
||||
# twospaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#nospaceatall
|
||||
#extraspaces
|
||||
#evenmorespaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#SBATCH --job-name=test
|
||||
#SBATCH --output=test.out
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#first
|
||||
|
||||
#second
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#!@#$%^&*()
|
||||
#<=>+-*/
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#x=1+2
|
||||
#y = 3
|
||||
#z = 4
|
||||
# fmt: on
|
||||
|
||||
# fmt: off
|
||||
yield 'hello'
|
||||
# No formatting to the end of the file
|
||||
@@ -617,29 +714,42 @@ def example(session):
|
||||
|
||||
|
||||
def off_and_on_without_data():
|
||||
"""All comments here are technically on the same prefix.
|
||||
|
||||
The comments between will be formatted. This is a known limitation.
|
||||
"""
|
||||
"""Test that comment-only fmt:off/on blocks preserve formatting."""
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
|
||||
# hey, that won't work
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
|
||||
def on_and_off_broken():
|
||||
"""Another known limitation."""
|
||||
def on_and_off_with_comment_only_blocks():
|
||||
"""Test that fmt:off/on works with multiple directives and comment-only blocks."""
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
this=should.not_be.formatted()
|
||||
and_=indeed . it is not formatted
|
||||
because . the . handling . inside . generate_ignored_nodes()
|
||||
now . considers . multiple . fmt . directives . within . one . prefix
|
||||
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
# ...but comments still get reformatted even though they should not be
|
||||
|
||||
#should not be formatted
|
||||
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
#should not be formatted
|
||||
#should not be formatted #also should not be formatted
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -716,6 +826,50 @@ cfg.rule(
|
||||
# fmt: on
|
||||
xxxxxxxxxx_xxxxxxxxxxx_xxxxxxx_xxxxxxxxx=5,
|
||||
)
|
||||
|
||||
# Test comment-only blocks at file level with various spacing patterns
|
||||
# fmt: off
|
||||
#nospace
|
||||
# twospaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#nospaceatall
|
||||
#extraspaces
|
||||
#evenmorespaces
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#SBATCH --job-name=test
|
||||
#SBATCH --output=test.out
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#first
|
||||
|
||||
#second
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#!@#$%^&*()
|
||||
#<=>+-*/
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
#x=1+2
|
||||
#y = 3
|
||||
#z = 4
|
||||
# fmt: on
|
||||
|
||||
# fmt: off
|
||||
yield 'hello'
|
||||
# No formatting to the end of the file
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmtskip10.py
|
||||
---
|
||||
## Input
|
||||
|
||||
@@ -8,11 +7,24 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/fmt
|
||||
def foo(): return "mock" # fmt: skip
|
||||
if True: print("yay") # fmt: skip
|
||||
for i in range(10): print(i) # fmt: skip
|
||||
if True: print("this"); print("that") # fmt: skip
|
||||
while True: print("loop"); break # fmt: skip
|
||||
for x in [1, 2]: print(x); print("done") # fmt: skip
|
||||
def f(x: int): return x # fmt: skip
|
||||
|
||||
j = 1 # fmt: skip
|
||||
while j < 10: j += 1 # fmt: skip
|
||||
|
||||
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
|
||||
|
||||
v = (
|
||||
foo_dict # fmt: skip
|
||||
.setdefault("a", {})
|
||||
.setdefault("b", {})
|
||||
.setdefault("c", {})
|
||||
.setdefault("d", {})
|
||||
.setdefault("e", {})
|
||||
)
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
@@ -20,19 +32,30 @@ b = [c for c in "A very long string that would normally generate some kind of co
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,8 +1,10 @@
|
||||
@@ -1,15 +1,20 @@
|
||||
def foo(): return "mock" # fmt: skip
|
||||
+
|
||||
+
|
||||
if True: print("yay") # fmt: skip
|
||||
for i in range(10): print(i) # fmt: skip
|
||||
if True: print("this"); print("that") # fmt: skip
|
||||
while True: print("loop"); break # fmt: skip
|
||||
for x in [1, 2]: print(x); print("done") # fmt: skip
|
||||
-def f(x: int): return x # fmt: skip
|
||||
|
||||
-j = 1 # fmt: skip
|
||||
+
|
||||
+def f(x: int): return x # fmt: skip
|
||||
+
|
||||
+
|
||||
+j = 1 # fmt: skip
|
||||
while j < 10: j += 1 # fmt: skip
|
||||
|
||||
-b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
|
||||
+b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
|
||||
|
||||
v = (
|
||||
foo_dict # fmt: skip
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -43,11 +66,27 @@ def foo(): return "mock" # fmt: skip
|
||||
|
||||
if True: print("yay") # fmt: skip
|
||||
for i in range(10): print(i) # fmt: skip
|
||||
if True: print("this"); print("that") # fmt: skip
|
||||
while True: print("loop"); break # fmt: skip
|
||||
for x in [1, 2]: print(x); print("done") # fmt: skip
|
||||
|
||||
|
||||
def f(x: int): return x # fmt: skip
|
||||
|
||||
|
||||
j = 1 # fmt: skip
|
||||
while j < 10: j += 1 # fmt: skip
|
||||
|
||||
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
|
||||
|
||||
v = (
|
||||
foo_dict # fmt: skip
|
||||
.setdefault("a", {})
|
||||
.setdefault("b", {})
|
||||
.setdefault("c", {})
|
||||
.setdefault("d", {})
|
||||
.setdefault("e", {})
|
||||
)
|
||||
```
|
||||
|
||||
## Black Output
|
||||
@@ -56,9 +95,22 @@ b = [c for c in "A very long string that would normally generate some kind of co
|
||||
def foo(): return "mock" # fmt: skip
|
||||
if True: print("yay") # fmt: skip
|
||||
for i in range(10): print(i) # fmt: skip
|
||||
if True: print("this"); print("that") # fmt: skip
|
||||
while True: print("loop"); break # fmt: skip
|
||||
for x in [1, 2]: print(x); print("done") # fmt: skip
|
||||
def f(x: int): return x # fmt: skip
|
||||
|
||||
j = 1 # fmt: skip
|
||||
while j < 10: j += 1 # fmt: skip
|
||||
|
||||
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
|
||||
|
||||
v = (
|
||||
foo_dict # fmt: skip
|
||||
.setdefault("a", {})
|
||||
.setdefault("b", {})
|
||||
.setdefault("c", {})
|
||||
.setdefault("d", {})
|
||||
.setdefault("e", {})
|
||||
)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
def foo():
|
||||
pass
|
||||
|
||||
|
||||
# comment 1 # fmt: skip
|
||||
# comment 2
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3,
|
||||
4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3,
|
||||
4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3,
|
||||
4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3,
|
||||
4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
if False:
|
||||
# fmt: off # some other comment
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -46,8 +46,7 @@
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
- (3,
|
||||
- 4),
|
||||
+ (3, 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
@@ -55,8 +54,7 @@
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
- (3,
|
||||
- 4),
|
||||
+ (3, 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
@@ -65,8 +63,7 @@
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
- (3,
|
||||
- 4),
|
||||
+ (3, 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
@@ -75,8 +72,7 @@
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
- (3,
|
||||
- 4),
|
||||
+ (3, 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
def foo():
|
||||
pass
|
||||
|
||||
|
||||
# comment 1 # fmt: skip
|
||||
# comment 2
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3, 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3, 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3, 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3, 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
if False:
|
||||
# fmt: off # some other comment
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
def foo():
|
||||
pass
|
||||
|
||||
|
||||
# comment 1 # fmt: skip
|
||||
# comment 2
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
# (3,
|
||||
# 4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3,
|
||||
4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# # fmt: off
|
||||
(3,
|
||||
4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3,
|
||||
4),
|
||||
# # fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
[
|
||||
(1, 2),
|
||||
# fmt: off
|
||||
(3,
|
||||
4),
|
||||
# fmt: on
|
||||
(5, 6),
|
||||
]
|
||||
|
||||
|
||||
if False:
|
||||
# fmt: off # some other comment
|
||||
pass
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
with open("file.txt") as f: content = f.read() # fmt: skip
|
||||
|
||||
# Ideally, only the last line would be ignored
|
||||
# But ignoring only part of the asexpr_test causes a parse error
|
||||
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
|
||||
with open (
|
||||
"file.txt" ,
|
||||
) as f: content = f.read() # fmt: skip
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,8 +1,8 @@
|
||||
-with open("file.txt") as f: content = f.read() # fmt: skip
|
||||
+with open("file.txt") as f: content = f.read() # fmt: skip
|
||||
|
||||
# Ideally, only the last line would be ignored
|
||||
# But ignoring only part of the asexpr_test causes a parse error
|
||||
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
|
||||
with open (
|
||||
"file.txt" ,
|
||||
-) as f: content = f.read() # fmt: skip
|
||||
+) as f: content = f.read() # fmt: skip
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
with open("file.txt") as f: content = f.read() # fmt: skip
|
||||
|
||||
# Ideally, only the last line would be ignored
|
||||
# But ignoring only part of the asexpr_test causes a parse error
|
||||
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
|
||||
with open (
|
||||
"file.txt" ,
|
||||
) as f: content = f.read() # fmt: skip
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
with open("file.txt") as f: content = f.read() # fmt: skip
|
||||
|
||||
# Ideally, only the last line would be ignored
|
||||
# But ignoring only part of the asexpr_test causes a parse error
|
||||
# Same with ignoring the asexpr_test without also ignoring the entire with_stmt
|
||||
with open (
|
||||
"file.txt" ,
|
||||
) as f: content = f.read() # fmt: skip
|
||||
```
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
t = (
|
||||
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
|
||||
t = (
|
||||
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{"foo": "bar",},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{"foo": "bar",},
|
||||
)
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,5 +1,9 @@
|
||||
t = (
|
||||
- {"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
+ {
|
||||
+ "foo": "very long string",
|
||||
+ "bar": "another very long string",
|
||||
+ "baz": "we should run out of space by now",
|
||||
+ }, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
@@ -14,8 +18,12 @@
|
||||
|
||||
|
||||
t = (
|
||||
- {"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{
|
||||
+ "foo": "very long string",
|
||||
+ "bar": "another very long string",
|
||||
+ "baz": "we should run out of space by now",
|
||||
+ }, # fmt: skip
|
||||
+ {
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
t = (
|
||||
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{"foo": "bar"},
|
||||
)
|
||||
|
||||
|
||||
t = (
|
||||
{"foo": "very long string", "bar": "another very long string", "baz": "we should run out of space by now"}, # fmt: skip
|
||||
{
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
|
||||
t = (
|
||||
{
|
||||
"foo": "very long string",
|
||||
"bar": "another very long string",
|
||||
"baz": "we should run out of space by now",
|
||||
}, # fmt: skip
|
||||
{
|
||||
"foo": "bar",
|
||||
},
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
a = "this is some code"
|
||||
b = 5 # fmt:skip
|
||||
c = 9 #fmt: skip
|
||||
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,4 +1,4 @@
|
||||
a = "this is some code"
|
||||
-b = 5 # fmt:skip
|
||||
-c = 9 #fmt: skip
|
||||
+b = 5 # fmt:skip
|
||||
+c = 9 # fmt: skip
|
||||
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
a = "this is some code"
|
||||
b = 5 # fmt:skip
|
||||
c = 9 # fmt: skip
|
||||
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
a = "this is some code"
|
||||
b = 5 # fmt:skip
|
||||
c = 9 #fmt: skip
|
||||
d = "thisisasuperlongstringthisisasuperlongstringthisisasuperlongstringthisisasuperlongstring" # fmt:skip
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# Multiple fmt: skip in multi-part if-clause
|
||||
class ClassWithALongName:
|
||||
Constant1 = 1
|
||||
Constant2 = 2
|
||||
Constant3 = 3
|
||||
|
||||
|
||||
def test():
|
||||
if (
|
||||
"cond1" == "cond1"
|
||||
and "cond2" == "cond2"
|
||||
and 1 in (
|
||||
ClassWithALongName.Constant1,
|
||||
ClassWithALongName.Constant2,
|
||||
ClassWithALongName.Constant3, # fmt: skip
|
||||
) # fmt: skip
|
||||
):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -9,11 +9,12 @@
|
||||
if (
|
||||
"cond1" == "cond1"
|
||||
and "cond2" == "cond2"
|
||||
- and 1 in (
|
||||
+ and 1
|
||||
+ in (
|
||||
ClassWithALongName.Constant1,
|
||||
ClassWithALongName.Constant2,
|
||||
- ClassWithALongName.Constant3, # fmt: skip
|
||||
- ) # fmt: skip
|
||||
+ ClassWithALongName.Constant3, # fmt: skip
|
||||
+ ) # fmt: skip
|
||||
):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# Multiple fmt: skip in multi-part if-clause
|
||||
class ClassWithALongName:
|
||||
Constant1 = 1
|
||||
Constant2 = 2
|
||||
Constant3 = 3
|
||||
|
||||
|
||||
def test():
|
||||
if (
|
||||
"cond1" == "cond1"
|
||||
and "cond2" == "cond2"
|
||||
and 1
|
||||
in (
|
||||
ClassWithALongName.Constant1,
|
||||
ClassWithALongName.Constant2,
|
||||
ClassWithALongName.Constant3, # fmt: skip
|
||||
) # fmt: skip
|
||||
):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# Multiple fmt: skip in multi-part if-clause
|
||||
class ClassWithALongName:
|
||||
Constant1 = 1
|
||||
Constant2 = 2
|
||||
Constant3 = 3
|
||||
|
||||
|
||||
def test():
|
||||
if (
|
||||
"cond1" == "cond1"
|
||||
and "cond2" == "cond2"
|
||||
and 1 in (
|
||||
ClassWithALongName.Constant1,
|
||||
ClassWithALongName.Constant2,
|
||||
ClassWithALongName.Constant3, # fmt: skip
|
||||
) # fmt: skip
|
||||
):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# Multiple fmt: skip on string literals
|
||||
a = (
|
||||
"this should " # fmt: skip
|
||||
"be fine"
|
||||
)
|
||||
|
||||
b = (
|
||||
"this is " # fmt: skip
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
c = (
|
||||
"and neither " # fmt: skip
|
||||
"is this " # fmt: skip
|
||||
"working"
|
||||
)
|
||||
|
||||
d = (
|
||||
"nor "
|
||||
"is this " # fmt: skip
|
||||
"working" # fmt: skip
|
||||
)
|
||||
|
||||
e = (
|
||||
"and this " # fmt: skip
|
||||
"is definitely "
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
# Dictionary entries with fmt: skip (covers issue with long lines)
|
||||
hotkeys = {
|
||||
"editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
"editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
}
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -29,7 +29,11 @@
|
||||
|
||||
# Dictionary entries with fmt: skip (covers issue with long lines)
|
||||
hotkeys = {
|
||||
- "editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
- "editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
- "editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
+ "editor:swap-line-down": [
|
||||
+ {"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}
|
||||
+ ], # fmt: skip
|
||||
+ "editor:swap-line-up": [
|
||||
+ {"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}
|
||||
+ ], # fmt: skip
|
||||
+ "editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
}
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# Multiple fmt: skip on string literals
|
||||
a = (
|
||||
"this should " # fmt: skip
|
||||
"be fine"
|
||||
)
|
||||
|
||||
b = (
|
||||
"this is " # fmt: skip
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
c = (
|
||||
"and neither " # fmt: skip
|
||||
"is this " # fmt: skip
|
||||
"working"
|
||||
)
|
||||
|
||||
d = (
|
||||
"nor "
|
||||
"is this " # fmt: skip
|
||||
"working" # fmt: skip
|
||||
)
|
||||
|
||||
e = (
|
||||
"and this " # fmt: skip
|
||||
"is definitely "
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
# Dictionary entries with fmt: skip (covers issue with long lines)
|
||||
hotkeys = {
|
||||
"editor:swap-line-down": [
|
||||
{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}
|
||||
], # fmt: skip
|
||||
"editor:swap-line-up": [
|
||||
{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}
|
||||
], # fmt: skip
|
||||
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
}
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# Multiple fmt: skip on string literals
|
||||
a = (
|
||||
"this should " # fmt: skip
|
||||
"be fine"
|
||||
)
|
||||
|
||||
b = (
|
||||
"this is " # fmt: skip
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
c = (
|
||||
"and neither " # fmt: skip
|
||||
"is this " # fmt: skip
|
||||
"working"
|
||||
)
|
||||
|
||||
d = (
|
||||
"nor "
|
||||
"is this " # fmt: skip
|
||||
"working" # fmt: skip
|
||||
)
|
||||
|
||||
e = (
|
||||
"and this " # fmt: skip
|
||||
"is definitely "
|
||||
"not working" # fmt: skip
|
||||
)
|
||||
|
||||
# Dictionary entries with fmt: skip (covers issue with long lines)
|
||||
hotkeys = {
|
||||
"editor:swap-line-down": [{"key": "ArrowDown", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
"editor:swap-line-up": [{"key": "ArrowUp", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
"editor:toggle-source": [{"key": "S", "modifiers": ["Alt", "Mod"]}], # fmt: skip
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
x = t"foo"
|
||||
x = t'foo {{ {2 + 2}bar {{ baz'
|
||||
|
||||
x = t"foo {f'abc'} bar"
|
||||
|
||||
x = t"""foo {{ a
|
||||
foo {2 + 2}bar {{ baz
|
||||
|
||||
x = f"foo {{ {
|
||||
2 + 2 # comment
|
||||
}bar"
|
||||
|
||||
{{ baz
|
||||
|
||||
}} buzz
|
||||
|
||||
{print("abc" + "def"
|
||||
)}
|
||||
abc"""
|
||||
|
||||
t'{(abc:=10)}'
|
||||
|
||||
t'''This is a really long string, but just make sure that you reflow tstrings {
|
||||
2+2:d
|
||||
}'''
|
||||
t'This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}'
|
||||
|
||||
t"{ 2 + 2 = }"
|
||||
|
||||
t'{
|
||||
X
|
||||
!r
|
||||
}'
|
||||
|
||||
tr'\{{\}}'
|
||||
|
||||
t'''
|
||||
WITH {f'''
|
||||
{1}_cte AS ()'''}
|
||||
'''
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -7,34 +7,32 @@
|
||||
foo {2 + 2}bar {{ baz
|
||||
|
||||
x = f"foo {{ {
|
||||
- 2 + 2 # comment
|
||||
- }bar"
|
||||
+ 2 + 2 # comment
|
||||
+}bar"
|
||||
|
||||
{{ baz
|
||||
|
||||
}} buzz
|
||||
|
||||
- {print("abc" + "def"
|
||||
-)}
|
||||
+ {print("abc" + "def")}
|
||||
abc"""
|
||||
|
||||
-t"{(abc:=10)}"
|
||||
+t"{(abc := 10)}"
|
||||
|
||||
t"""This is a really long string, but just make sure that you reflow tstrings {
|
||||
- 2+2:d
|
||||
+ 2 + 2:d
|
||||
}"""
|
||||
-t"This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}"
|
||||
+t"This is a really long string, but just make sure that you reflow tstrings correctly {2 + 2:d}"
|
||||
|
||||
t"{ 2 + 2 = }"
|
||||
|
||||
-t"{
|
||||
-X
|
||||
-!r
|
||||
-}"
|
||||
+t"{X!r}"
|
||||
|
||||
rt"\{{\}}"
|
||||
|
||||
t"""
|
||||
- WITH {f'''
|
||||
- {1}_cte AS ()'''}
|
||||
+ WITH {
|
||||
+ f'''
|
||||
+ {1}_cte AS ()'''
|
||||
+}
|
||||
"""
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
x = t"foo"
|
||||
x = t"foo {{ {2 + 2}bar {{ baz"
|
||||
|
||||
x = t"foo {f'abc'} bar"
|
||||
|
||||
x = t"""foo {{ a
|
||||
foo {2 + 2}bar {{ baz
|
||||
|
||||
x = f"foo {{ {
|
||||
2 + 2 # comment
|
||||
}bar"
|
||||
|
||||
{{ baz
|
||||
|
||||
}} buzz
|
||||
|
||||
{print("abc" + "def")}
|
||||
abc"""
|
||||
|
||||
t"{(abc := 10)}"
|
||||
|
||||
t"""This is a really long string, but just make sure that you reflow tstrings {
|
||||
2 + 2:d
|
||||
}"""
|
||||
t"This is a really long string, but just make sure that you reflow tstrings correctly {2 + 2:d}"
|
||||
|
||||
t"{ 2 + 2 = }"
|
||||
|
||||
t"{X!r}"
|
||||
|
||||
rt"\{{\}}"
|
||||
|
||||
t"""
|
||||
WITH {
|
||||
f'''
|
||||
{1}_cte AS ()'''
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
x = t"foo"
|
||||
x = t"foo {{ {2 + 2}bar {{ baz"
|
||||
|
||||
x = t"foo {f'abc'} bar"
|
||||
|
||||
x = t"""foo {{ a
|
||||
foo {2 + 2}bar {{ baz
|
||||
|
||||
x = f"foo {{ {
|
||||
2 + 2 # comment
|
||||
}bar"
|
||||
|
||||
{{ baz
|
||||
|
||||
}} buzz
|
||||
|
||||
{print("abc" + "def"
|
||||
)}
|
||||
abc"""
|
||||
|
||||
t"{(abc:=10)}"
|
||||
|
||||
t"""This is a really long string, but just make sure that you reflow tstrings {
|
||||
2+2:d
|
||||
}"""
|
||||
t"This is a really long string, but just make sure that you reflow tstrings correctly {2+2:d}"
|
||||
|
||||
t"{ 2 + 2 = }"
|
||||
|
||||
t"{
|
||||
X
|
||||
!r
|
||||
}"
|
||||
|
||||
rt"\{{\}}"
|
||||
|
||||
t"""
|
||||
WITH {f'''
|
||||
{1}_cte AS ()'''}
|
||||
"""
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -3,6 +3,7 @@
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
+
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/preview_multiline_strings.py
|
||||
---
|
||||
## Input
|
||||
|
||||
@@ -163,24 +162,6 @@ Please use `--build-option` instead,
|
||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||
"""
|
||||
|
||||
this_will_become_one_line = (
|
||||
"a"
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
this_will_stay_on_three_lines = (
|
||||
"a" # comment
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
this_will_also_become_one_line = ( # comment
|
||||
"a"
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
assert some_var == expected_result, """
|
||||
test
|
||||
"""
|
||||
@@ -421,18 +402,7 @@ a = b if """
|
||||
[
|
||||
"""cow
|
||||
moos""",
|
||||
@@ -206,7 +245,9 @@
|
||||
"c"
|
||||
)
|
||||
|
||||
-this_will_also_become_one_line = "abc" # comment
|
||||
+this_will_also_become_one_line = ( # comment
|
||||
+ "abc"
|
||||
+)
|
||||
|
||||
assert some_var == expected_result, """
|
||||
test
|
||||
@@ -224,10 +265,8 @@
|
||||
@@ -214,10 +253,8 @@
|
||||
"""Sxxxxxxx xxxxxxxx, xxxxxxx xx xxxxxxxxx
|
||||
xxxxxxxxxxxxx xxxxxxx xxxxxxxxx xxx-xxxxxxxxxx xxxxxx xx xxx-xxxxxx"""
|
||||
),
|
||||
@@ -445,7 +415,7 @@ a = b if """
|
||||
},
|
||||
}
|
||||
|
||||
@@ -246,14 +285,12 @@
|
||||
@@ -236,14 +273,12 @@
|
||||
a
|
||||
a"""
|
||||
),
|
||||
@@ -706,18 +676,6 @@ Please use `--build-option` instead,
|
||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||
"""
|
||||
|
||||
this_will_become_one_line = "abc"
|
||||
|
||||
this_will_stay_on_three_lines = (
|
||||
"a" # comment
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
this_will_also_become_one_line = ( # comment
|
||||
"abc"
|
||||
)
|
||||
|
||||
assert some_var == expected_result, """
|
||||
test
|
||||
"""
|
||||
@@ -1028,16 +986,6 @@ Please use `--build-option` instead,
|
||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||
"""
|
||||
|
||||
this_will_become_one_line = "abc"
|
||||
|
||||
this_will_stay_on_three_lines = (
|
||||
"a" # comment
|
||||
"b"
|
||||
"c"
|
||||
)
|
||||
|
||||
this_will_also_become_one_line = "abc" # comment
|
||||
|
||||
assert some_var == expected_result, """
|
||||
test
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
def foo(
|
||||
a, #type:int
|
||||
b, #type: str
|
||||
c, # type: List[int]
|
||||
d, # type: Dict[int, str]
|
||||
e, # type: ignore
|
||||
f, # type : ignore
|
||||
g, # type : ignore
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,9 +1,9 @@
|
||||
def foo(
|
||||
- a, # type: int
|
||||
+ a, # type:int
|
||||
b, # type: str
|
||||
c, # type: List[int]
|
||||
- d, # type: Dict[int, str]
|
||||
- e, # type: ignore
|
||||
+ d, # type: Dict[int, str]
|
||||
+ e, # type: ignore
|
||||
f, # type : ignore
|
||||
g, # type : ignore
|
||||
):
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
def foo(
|
||||
a, # type:int
|
||||
b, # type: str
|
||||
c, # type: List[int]
|
||||
d, # type: Dict[int, str]
|
||||
e, # type: ignore
|
||||
f, # type : ignore
|
||||
g, # type : ignore
|
||||
):
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
def foo(
|
||||
a, # type: int
|
||||
b, # type: str
|
||||
c, # type: List[int]
|
||||
d, # type: Dict[int, str]
|
||||
e, # type: ignore
|
||||
f, # type : ignore
|
||||
g, # type : ignore
|
||||
):
|
||||
pass
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# Remove unnecessary parentheses from LHS of assignments
|
||||
|
||||
|
||||
def a():
|
||||
return [1, 2, 3]
|
||||
|
||||
|
||||
# Single variable with unnecessary parentheses
|
||||
(b) = a()[0]
|
||||
|
||||
# Tuple unpacking with unnecessary parentheses
|
||||
(c, *_) = a()
|
||||
|
||||
# These should not be changed - parentheses are necessary
|
||||
(d,) = a() # single-element tuple
|
||||
e = (1 + 2) * 3 # RHS has precedence needs
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
|
||||
# Single variable with unnecessary parentheses
|
||||
-b = a()[0]
|
||||
+(b) = a()[0]
|
||||
|
||||
# Tuple unpacking with unnecessary parentheses
|
||||
-c, *_ = a()
|
||||
+(c, *_) = a()
|
||||
|
||||
# These should not be changed - parentheses are necessary
|
||||
(d,) = a() # single-element tuple
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# Remove unnecessary parentheses from LHS of assignments
|
||||
|
||||
|
||||
def a():
|
||||
return [1, 2, 3]
|
||||
|
||||
|
||||
# Single variable with unnecessary parentheses
|
||||
(b) = a()[0]
|
||||
|
||||
# Tuple unpacking with unnecessary parentheses
|
||||
(c, *_) = a()
|
||||
|
||||
# These should not be changed - parentheses are necessary
|
||||
(d,) = a() # single-element tuple
|
||||
e = (1 + 2) * 3 # RHS has precedence needs
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# Remove unnecessary parentheses from LHS of assignments
|
||||
|
||||
|
||||
def a():
|
||||
return [1, 2, 3]
|
||||
|
||||
|
||||
# Single variable with unnecessary parentheses
|
||||
b = a()[0]
|
||||
|
||||
# Tuple unpacking with unnecessary parentheses
|
||||
c, *_ = a()
|
||||
|
||||
# These should not be changed - parentheses are necessary
|
||||
(d,) = a() # single-element tuple
|
||||
e = (1 + 2) * 3 # RHS has precedence needs
|
||||
```
|
||||
3
crates/ty/docs/cli.md
generated
3
crates/ty/docs/cli.md
generated
@@ -37,7 +37,8 @@ ty check [OPTIONS] [PATH]...
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="ty-check--color"><a href="#ty-check--color"><code>--color</code></a> <i>when</i></dt><dd><p>Control when colored output is used</p>
|
||||
<dl class="cli-reference"><dt id="ty-check--add-ignore"><a href="#ty-check--add-ignore"><code>--add-ignore</code></a></dt><dd><p>Adds <code>ty: ignore</code> comments to suppress all rule diagnostics</p>
|
||||
</dd><dt id="ty-check--color"><a href="#ty-check--color"><code>--color</code></a> <i>when</i></dt><dd><p>Control when colored output is used</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>auto</code>: Display colors if the output goes to an interactive terminal</li>
|
||||
|
||||
229
crates/ty/docs/rules.md
generated
229
crates/ty/docs/rules.md
generated
@@ -8,7 +8,7 @@
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L538" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L540" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L137" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L139" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.7">0.0.7</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-top-callable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L155" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ def f(x: object):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L206" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ f(int) # error
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L232" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ a = 1
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L257" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L259" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ class C(A, B): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L283" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ class B(A): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L309" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L311" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ type B = A
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L353" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L355" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L331" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L333" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ false positives it can produce.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L374" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L376" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -373,7 +373,7 @@ class B(A, A): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L395" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L397" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -529,7 +529,7 @@ def test(): -> "Literal[5]":
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L621" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L623" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -559,7 +559,7 @@ class C(A, B): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L645" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L647" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -585,7 +585,7 @@ t[3] # IndexError: tuple index out of range
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L427" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L429" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -674,7 +674,7 @@ an atypical memory layout.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L699" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L701" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -701,7 +701,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L739" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L741" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -729,7 +729,7 @@ a: int = ''
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2042" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2044" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -763,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L761" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L763" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -799,7 +799,7 @@ asyncio.run(main())
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L791" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L793" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -823,7 +823,7 @@ class A(42): ... # error: [invalid-base]
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L842" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L844" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -850,7 +850,7 @@ with 1:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L863" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -879,7 +879,7 @@ a: str
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L886" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L888" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -923,7 +923,7 @@ except ZeroDivisionError:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1712" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1714" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -965,7 +965,7 @@ class D(A):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.35">0.0.1-alpha.35</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-frozen-dataclass-subclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2268" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2295" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1009,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L922" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L924" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1077,7 +1077,7 @@ a = 20 / 0 # type: ignore
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L666" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L668" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1116,7 +1116,7 @@ carol = Person(name="Carol", age=25) # typo!
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L953" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L955" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1151,7 +1151,7 @@ def f(t: TypeVar("U")): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1050" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1052" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1185,7 +1185,7 @@ class B(metaclass=f): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2170" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2197" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1292,7 +1292,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L573" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L575" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1346,7 +1346,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1026" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1028" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1376,7 +1376,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1077" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1079" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1426,7 +1426,7 @@ def foo(x: int) -> int: ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1176" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1178" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1452,7 +1452,7 @@ def f(a: int = ''): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1483,7 +1483,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L509" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L511" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1517,7 +1517,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1196" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1198" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1566,7 +1566,7 @@ def g():
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L720" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L722" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1591,7 +1591,7 @@ def func() -> int:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1239" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1241" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1681,13 +1681,59 @@ class C: ...
|
||||
- [Typing spec: The meaning of annotations](https://typing.python.org/en/latest/spec/annotations.html#the-meaning-of-annotations)
|
||||
- [Typing spec: String annotations](https://typing.python.org/en/latest/spec/annotations.html#string-annotations)
|
||||
|
||||
## `invalid-total-ordering`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.10">0.0.10</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-total-ordering" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2333" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for classes decorated with `@functools.total_ordering` that don't
|
||||
define any ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`).
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
The `@total_ordering` decorator requires the class to define at least one
|
||||
ordering method. If none is defined, Python raises a `ValueError` at runtime.
|
||||
|
||||
**Example**
|
||||
|
||||
|
||||
```python
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class MyClass: # Error: no ordering method defined
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
```
|
||||
|
||||
Use instead:
|
||||
|
||||
```python
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class MyClass:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __lt__(self, other: "MyClass") -> bool:
|
||||
return True
|
||||
```
|
||||
|
||||
## `invalid-type-alias-type`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1005" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1007" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1714,7 +1760,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1473" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1761,7 +1807,7 @@ Bar[int] # error: too few arguments
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1278" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1280" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1791,7 +1837,7 @@ TYPE_CHECKING = ''
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1302" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1304" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1821,7 +1867,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1354" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1356" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1855,7 +1901,7 @@ f(10) # Error
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1326" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1328" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1889,7 +1935,7 @@ class C:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1382" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1384" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1918,13 +1964,44 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
|
||||
[type variables]: https://docs.python.org/3/library/typing.html#typing.TypeVar
|
||||
|
||||
## `invalid-typed-dict-statement`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.9">0.0.9</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-typed-dict-statement" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2172" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Detects statements other than annotated declarations in `TypedDict` class bodies.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
`TypedDict` class bodies aren't allowed to contain any other types of statements. For
|
||||
example, method definitions and field values aren't allowed. None of these will be
|
||||
available on "instances of the `TypedDict`" at runtime (as `dict` is the runtime class of
|
||||
all "`TypedDict` instances").
|
||||
|
||||
**Example**
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
|
||||
class Foo(TypedDict):
|
||||
def bar(self): # error: [invalid-typed-dict-statement]
|
||||
pass
|
||||
```
|
||||
|
||||
## `missing-argument`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1411" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1413" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1949,7 +2026,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2143" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2145" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1982,7 +2059,7 @@ alice["age"] # KeyError
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1430" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1432" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2011,7 +2088,7 @@ func("string") # error: [no-matching-overload]
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1512" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1514" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2037,7 +2114,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-subscriptable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1455" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2061,7 +2138,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1685" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1687" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2094,7 +2171,7 @@ class B(A):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1565" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2121,7 +2198,7 @@ f(1, x=2) # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1896" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1898" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2148,7 +2225,7 @@ f(x=1) # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1584" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1586" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2176,7 +2253,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L180" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L182" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2208,7 +2285,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1606" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1608" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2245,7 +2322,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1636" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1638" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2309,7 +2386,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2070" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2072" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2336,7 +2413,7 @@ cast(int, f()) # Redundant
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2018" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2020" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2366,7 +2443,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1662" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1664" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2395,7 +2472,7 @@ class B(A): ... # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1830" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1832" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2429,7 +2506,7 @@ class F(NamedTuple):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1770" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1772" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2456,7 +2533,7 @@ f("foo") # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1748" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1750" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2484,7 +2561,7 @@ def _(x: int):
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1791" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1793" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2530,7 +2607,7 @@ class A:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1857" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1859" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2554,7 +2631,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1875" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1877" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2581,7 +2658,7 @@ f(x=1, y=2) # Error raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1917" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1919" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2609,7 +2686,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2091" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2093" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2667,7 +2744,7 @@ def g():
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1939" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1941" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2692,7 +2769,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1958" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1960" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2717,7 +2794,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2756,7 +2833,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1532" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1534" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2793,7 +2870,7 @@ b1 < b2 < b1 # exception raised here
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1977" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1979" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2852,7 +2929,7 @@ a = 20 / 2
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1120" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1122" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2915,7 +2992,7 @@ def foo(x: int | str) -> int | str:
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1999" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2001" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ pub(crate) struct CheckCommand {
|
||||
)]
|
||||
pub paths: Vec<SystemPathBuf>,
|
||||
|
||||
/// Adds `ty: ignore` comments to suppress all rule diagnostics.
|
||||
#[arg(long)]
|
||||
pub(crate) add_ignore: bool,
|
||||
|
||||
/// Run the command within the given project directory.
|
||||
///
|
||||
/// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory,
|
||||
|
||||
@@ -4,37 +4,36 @@ mod printer;
|
||||
mod python_version;
|
||||
mod version;
|
||||
|
||||
pub use args::Cli;
|
||||
use ty_project::metadata::settings::TerminalSettings;
|
||||
use ty_static::EnvVars;
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::process::{ExitCode, Termination};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::args::{CheckCommand, Command, TerminalColor};
|
||||
use crate::logging::{VerbosityLevel, setup_tracing};
|
||||
use crate::printer::Printer;
|
||||
use anyhow::{Context, anyhow};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use rayon::ThreadPoolBuilder;
|
||||
use ruff_db::cancellation::{CancellationToken, CancellationTokenSource};
|
||||
use ruff_db::cancellation::{Canceled, CancellationToken, CancellationTokenSource};
|
||||
use ruff_db::diagnostic::{
|
||||
Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity,
|
||||
};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::{STACK_SIZE, max_parallelism};
|
||||
use salsa::Database;
|
||||
use ty_project::metadata::options::ProjectOptionsOverrides;
|
||||
use ty_project::metadata::settings::TerminalSettings;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
use ty_project::{CollectReporter, Db, watch};
|
||||
use ty_project::{CollectReporter, Db, suppress_all_diagnostics, watch};
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
use ty_server::run_server;
|
||||
use ty_static::EnvVars;
|
||||
|
||||
use crate::args::{CheckCommand, Command, TerminalColor};
|
||||
use crate::logging::{VerbosityLevel, setup_tracing};
|
||||
use crate::printer::Printer;
|
||||
pub use args::Cli;
|
||||
|
||||
pub fn run() -> anyhow::Result<ExitStatus> {
|
||||
setup_rayon();
|
||||
@@ -112,6 +111,12 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let mode = if args.add_ignore {
|
||||
MainLoopMode::AddIgnore
|
||||
} else {
|
||||
MainLoopMode::Check
|
||||
};
|
||||
|
||||
let system = OsSystem::new(&cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
@@ -144,7 +149,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
}
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) =
|
||||
MainLoop::new(project_options_overrides, printer);
|
||||
MainLoop::new(mode, project_options_overrides, printer);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
@@ -215,6 +220,8 @@ impl Termination for ExitStatus {
|
||||
}
|
||||
|
||||
struct MainLoop {
|
||||
mode: MainLoopMode,
|
||||
|
||||
/// Sender that can be used to send messages to the main loop.
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
|
||||
@@ -237,6 +244,7 @@ struct MainLoop {
|
||||
|
||||
impl MainLoop {
|
||||
fn new(
|
||||
mode: MainLoopMode,
|
||||
project_options_overrides: ProjectOptionsOverrides,
|
||||
printer: Printer,
|
||||
) -> (Self, MainLoopCancellationToken) {
|
||||
@@ -247,6 +255,7 @@ impl MainLoop {
|
||||
|
||||
(
|
||||
Self {
|
||||
mode,
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
@@ -325,80 +334,78 @@ impl MainLoop {
|
||||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format.into())
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize())
|
||||
.with_cancellation_token(Some(self.cancellation_token.clone()))
|
||||
.show_fix_diff(true);
|
||||
|
||||
if check_revision == revision {
|
||||
if db.project().files(db).is_empty() {
|
||||
tracing::warn!("No python files found under the given path(s)");
|
||||
}
|
||||
|
||||
// TODO: We should have an official flag to silence workspace diagnostics.
|
||||
if std::env::var("TY_MEMORY_REPORT").as_deref() == Ok("mypy_primer") {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
let is_human_readable = terminal_settings.output_format.is_human_readable();
|
||||
|
||||
if result.is_empty() {
|
||||
if is_human_readable {
|
||||
writeln!(
|
||||
self.printer.stream_for_success_summary(),
|
||||
"{}",
|
||||
"All checks passed!".green().bold()
|
||||
)?;
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
} else {
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
let mut stdout = self.printer.stream_for_details().lock();
|
||||
let exit_status =
|
||||
exit_status_from_diagnostics(&result, terminal_settings);
|
||||
|
||||
// Only render diagnostics if they're going to be displayed, since doing
|
||||
// so is expensive.
|
||||
if stdout.is_enabled() {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
DisplayDiagnostics::new(db, &display_config, &result)
|
||||
)?;
|
||||
}
|
||||
|
||||
if !self.cancellation_token.is_cancelled() {
|
||||
if is_human_readable {
|
||||
writeln!(
|
||||
self.printer.stream_for_failure_summary(),
|
||||
"Found {} diagnostic{}",
|
||||
diagnostics_count,
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
}
|
||||
|
||||
if exit_status.is_internal_error() {
|
||||
tracing::warn!(
|
||||
"A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return Ok(exit_status);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if check_revision != revision {
|
||||
tracing::debug!(
|
||||
"Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if db.project().files(db).is_empty() {
|
||||
tracing::warn!("No python files found under the given path(s)");
|
||||
}
|
||||
|
||||
let result = match self.mode {
|
||||
MainLoopMode::Check => {
|
||||
// TODO: We should have an official flag to silence workspace diagnostics.
|
||||
if std::env::var("TY_MEMORY_REPORT").as_deref() == Ok("mypy_primer") {
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
self.write_diagnostics(db, &result)?;
|
||||
|
||||
if self.cancellation_token.is_cancelled() {
|
||||
Err(Canceled)
|
||||
} else {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
MainLoopMode::AddIgnore => {
|
||||
if let Ok(result) =
|
||||
suppress_all_diagnostics(db, result, &self.cancellation_token)
|
||||
{
|
||||
self.write_diagnostics(db, &result.diagnostics)?;
|
||||
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let is_human_readable =
|
||||
terminal_settings.output_format.is_human_readable();
|
||||
|
||||
if is_human_readable {
|
||||
writeln!(
|
||||
self.printer.stream_for_failure_summary(),
|
||||
"Added {} ignore comment{}",
|
||||
result.count,
|
||||
if result.count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(result.diagnostics)
|
||||
} else {
|
||||
Err(Canceled)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let exit_status = match result.as_deref() {
|
||||
Ok([]) => ExitStatus::Success,
|
||||
Ok(diagnostics) => {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
exit_status_from_diagnostics(diagnostics, terminal_settings)
|
||||
}
|
||||
Err(Canceled) => ExitStatus::Success,
|
||||
};
|
||||
|
||||
if exit_status.is_internal_error() {
|
||||
tracing::warn!(
|
||||
"A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."
|
||||
);
|
||||
}
|
||||
|
||||
if self.watcher.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(exit_status);
|
||||
}
|
||||
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
@@ -425,6 +432,65 @@ impl MainLoop {
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn write_diagnostics(
|
||||
&self,
|
||||
db: &ProjectDatabase,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> anyhow::Result<()> {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let is_human_readable = terminal_settings.output_format.is_human_readable();
|
||||
|
||||
match diagnostics {
|
||||
[] => {
|
||||
if is_human_readable {
|
||||
writeln!(
|
||||
self.printer.stream_for_success_summary(),
|
||||
"{}",
|
||||
"All checks passed!".green().bold()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
diagnostics => {
|
||||
let diagnostics_count = diagnostics.len();
|
||||
|
||||
let mut stdout = self.printer.stream_for_details().lock();
|
||||
|
||||
// Only render diagnostics if they're going to be displayed, since doing
|
||||
// so is expensive.
|
||||
if stdout.is_enabled() {
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format.into())
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize())
|
||||
.with_cancellation_token(Some(self.cancellation_token.clone()))
|
||||
.show_fix_diff(true);
|
||||
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
DisplayDiagnostics::new(db, &display_config, diagnostics)
|
||||
)?;
|
||||
}
|
||||
|
||||
if !self.cancellation_token.is_cancelled() && is_human_readable {
|
||||
writeln!(
|
||||
self.printer.stream_for_failure_summary(),
|
||||
"Found {} diagnostic{}",
|
||||
diagnostics_count,
|
||||
if diagnostics_count > 1 { "s" } else { "" }
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum MainLoopMode {
|
||||
Check,
|
||||
AddIgnore,
|
||||
}
|
||||
|
||||
fn exit_status_from_diagnostics(
|
||||
@@ -559,12 +625,7 @@ fn set_colored_override(color: Option<TerminalColor>) {
|
||||
fn setup_rayon() {
|
||||
ThreadPoolBuilder::default()
|
||||
.num_threads(max_parallelism().get())
|
||||
// Use a reasonably large stack size to avoid running into stack overflows too easily. The
|
||||
// size was chosen in such a way as to still be able to handle large expressions involving
|
||||
// binary operators (x + x + … + x) both during the AST walk in semantic index building as
|
||||
// well as during type checking. Using this stack size, we can handle handle expressions
|
||||
// that are several times larger than the corresponding limits in existing type checkers.
|
||||
.stack_size(16 * 1024 * 1024)
|
||||
.stack_size(STACK_SIZE)
|
||||
.build_global()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -160,6 +160,65 @@ fn configuration_include() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Files without extensions can be included by adding a literal glob to `include` that matches
|
||||
/// the path exactly. A literal glob is a glob without any meta characters.
|
||||
#[test]
|
||||
fn configuration_include_no_extension() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([(
|
||||
"src/main",
|
||||
r#"
|
||||
print(undefined_var) # error: unresolved-reference
|
||||
"#,
|
||||
)])?;
|
||||
|
||||
// By default, `src/main` is excluded because the file has no supported extension.
|
||||
case.write_file(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[src]
|
||||
include = ["src"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
WARN No python files found under the given path(s)
|
||||
");
|
||||
|
||||
// The file can be included by adding an exactly matching pattern
|
||||
case.write_file(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[src]
|
||||
include = ["src", "src/main"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> src/main:2:7
|
||||
|
|
||||
2 | print(undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test configuration file exclude functionality
|
||||
#[test]
|
||||
fn configuration_exclude() -> anyhow::Result<()> {
|
||||
|
||||
114
crates/ty/tests/cli/fixes.rs
Normal file
114
crates/ty/tests/cli/fixes.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
#[test]
|
||||
fn add_ignore() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_file(
|
||||
"different_violations.py",
|
||||
r#"
|
||||
import sys
|
||||
|
||||
x = 1 + a
|
||||
|
||||
if sys.does_not_exist:
|
||||
...
|
||||
|
||||
def test(a, b): ...
|
||||
|
||||
test(x = 10, b = 12)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--add-ignore"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
Added 4 ignore comments
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// There should be no diagnostics when running ty again
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
All checks passed!
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ignore_unfixable() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
("has_syntax_error.py", r"print(x # [unresolved-reference]"),
|
||||
(
|
||||
"different_violations.py",
|
||||
r#"
|
||||
import sys
|
||||
|
||||
x = 1 + a
|
||||
|
||||
reveal_type(x)
|
||||
|
||||
if sys.does_not_exist:
|
||||
...
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"repeated_violations.py",
|
||||
r#"
|
||||
x = (
|
||||
1 +
|
||||
a * b
|
||||
)
|
||||
|
||||
y = y # ty: ignore[unresolved-reference]
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--add-ignore").env("RUST_BACKTRACE", "1"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
info[revealed-type]: Revealed type
|
||||
--> different_violations.py:6:13
|
||||
|
|
||||
4 | x = 1 + a # ty:ignore[unresolved-reference]
|
||||
5 |
|
||||
6 | reveal_type(x) # ty:ignore[undefined-reveal]
|
||||
| ^ `Unknown`
|
||||
7 |
|
||||
8 | if sys.does_not_exist: # ty:ignore[unresolved-attribute]
|
||||
|
|
||||
|
||||
error[unresolved-reference]: Name `x` used when not defined
|
||||
--> has_syntax_error.py:1:7
|
||||
|
|
||||
1 | print(x # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[invalid-syntax]: unexpected EOF while parsing
|
||||
--> has_syntax_error.py:1:34
|
||||
|
|
||||
1 | print(x # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 3 diagnostics
|
||||
Added 5 ignore comments
|
||||
|
||||
----- stderr -----
|
||||
WARN Skipping file `<temp_dir>/has_syntax_error.py` with syntax errors
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,6 +2,7 @@ mod analysis_options;
|
||||
mod config_option;
|
||||
mod exit_code;
|
||||
mod file_selection;
|
||||
mod fixes;
|
||||
mod python_environment;
|
||||
mod rule_selection;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ auto-import-includes-modules,main.py,0,1
|
||||
auto-import-includes-modules,main.py,1,7
|
||||
auto-import-includes-modules,main.py,2,1
|
||||
auto-import-skips-current-module,main.py,0,1
|
||||
class-arg-completion,main.py,0,1
|
||||
fstring-completions,main.py,0,1
|
||||
higher-level-symbols-preferred,main.py,0,
|
||||
higher-level-symbols-preferred,main.py,1,1
|
||||
|
||||
|
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = false
|
||||
@@ -0,0 +1 @@
|
||||
class Foo(m<CURSOR: metaclass>)
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
8
crates/ty_completion_eval/truth/class-arg-completion/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/class-arg-completion/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
@@ -5,8 +5,8 @@ use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::find_node::covering_node;
|
||||
use ruff_text_size::TextRange;
|
||||
use ty_project::Db;
|
||||
use ty_python_semantic::create_suppression_fix;
|
||||
use ty_python_semantic::lint::LintId;
|
||||
use ty_python_semantic::suppress_single;
|
||||
use ty_python_semantic::types::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE};
|
||||
|
||||
/// A `QuickFix` Code Action
|
||||
@@ -42,7 +42,7 @@ pub fn code_actions(
|
||||
// Suggest just suppressing the lint (always a valid option, but never ideal)
|
||||
actions.push(QuickFix {
|
||||
title: format!("Ignore '{}' for this line", lint_id.name()),
|
||||
edits: create_suppression_fix(db, file, lint_id, diagnostic_range).into_edits(),
|
||||
edits: suppress_single(db, file, lint_id, diagnostic_range).into_edits(),
|
||||
preferred: false,
|
||||
});
|
||||
|
||||
@@ -437,6 +437,38 @@ mod tests {
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ignore_line_continuation_empty_lines() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"b = bbbbb \
|
||||
[ ccc # test
|
||||
|
||||
+ <START>ddd<END> \
|
||||
|
||||
] # test
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:4:11
|
||||
|
|
||||
2 | [ ccc # test
|
||||
3 |
|
||||
4 | + ddd \
|
||||
| ^^^
|
||||
5 |
|
||||
6 | ] # test
|
||||
|
|
||||
2 | [ ccc # test
|
||||
3 |
|
||||
4 | + ddd \
|
||||
-
|
||||
5 + # ty:ignore[unresolved-reference]
|
||||
6 | ] # test
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_reveal_type() {
|
||||
let test = CodeActionTest::with_source(
|
||||
|
||||
@@ -193,15 +193,16 @@ impl<'db> Completions<'db> {
|
||||
/// when the completion context determines that the given suggestion
|
||||
/// is never valid.
|
||||
fn add_skip_query(&mut self, mut completion: Completion<'db>) -> bool {
|
||||
// Tags completions with whether they are known to be usable in
|
||||
// a `raise` context.
|
||||
// Tags completions with context-specific if they are
|
||||
// known to be usable in a `raise` context and we have
|
||||
// determined a raisable type `raisable_ty`.
|
||||
//
|
||||
// It's possible that some completions are usable in a `raise`
|
||||
// but aren't marked here. That is, false negatives are
|
||||
// possible but false positives are not.
|
||||
if let Some(raisable_ty) = self.context.raisable_ty {
|
||||
if let Some(ty) = completion.ty {
|
||||
completion.is_definitively_raisable = ty.is_assignable_to(self.db, raisable_ty);
|
||||
completion.is_context_specific |= ty.is_assignable_to(self.db, raisable_ty);
|
||||
}
|
||||
}
|
||||
if self.context.exclude(self.db, &completion) {
|
||||
@@ -285,13 +286,13 @@ pub struct Completion<'db> {
|
||||
/// Whether this item only exists for type checking purposes and
|
||||
/// will be missing at runtime
|
||||
pub is_type_check_only: bool,
|
||||
/// Whether this item can definitively be used in a `raise` context.
|
||||
/// Whether this item can definitively be used in the current context.
|
||||
///
|
||||
/// Note that this may not always be computed. (i.e., Only computed
|
||||
/// when we are in a `raise` context.) And also note that if this
|
||||
/// is `true`, then it's definitively usable in `raise`, but if
|
||||
/// it's `false`, it _may_ still be usable in `raise`.
|
||||
pub is_definitively_raisable: bool,
|
||||
/// Some completions are computed based on contextual information.
|
||||
/// If that's the case, we know this is a very precise completion
|
||||
/// that should always be valid and can be preferred when
|
||||
/// ordering completions.
|
||||
pub is_context_specific: bool,
|
||||
/// The documentation associated with this item, if
|
||||
/// available.
|
||||
pub documentation: Option<Docstring>,
|
||||
@@ -315,7 +316,7 @@ impl<'db> Completion<'db> {
|
||||
import: None,
|
||||
builtin: semantic.builtin,
|
||||
is_type_check_only,
|
||||
is_definitively_raisable: false,
|
||||
is_context_specific: false,
|
||||
documentation,
|
||||
}
|
||||
}
|
||||
@@ -398,7 +399,7 @@ impl<'db> Completion<'db> {
|
||||
import: None,
|
||||
builtin: false,
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
is_context_specific: false,
|
||||
documentation: None,
|
||||
}
|
||||
}
|
||||
@@ -414,7 +415,7 @@ impl<'db> Completion<'db> {
|
||||
import: None,
|
||||
builtin: true,
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
is_context_specific: false,
|
||||
documentation: None,
|
||||
}
|
||||
}
|
||||
@@ -433,7 +434,7 @@ impl<'db> Completion<'db> {
|
||||
import: None,
|
||||
builtin: false,
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
is_context_specific: true,
|
||||
documentation,
|
||||
}
|
||||
}
|
||||
@@ -994,7 +995,7 @@ impl<'db> CollectionContext<'db> {
|
||||
#[allow(clippy::unused_self)]
|
||||
fn rank<'c>(&self, c: &'c Completion<'_>) -> Rank<'c> {
|
||||
Rank {
|
||||
definitively_usable: if c.is_definitively_raisable {
|
||||
definitively_usable: if c.is_context_specific {
|
||||
Sort::Higher
|
||||
} else {
|
||||
Sort::Even
|
||||
@@ -1183,7 +1184,6 @@ fn add_function_arg_completions<'db>(
|
||||
if p.is_positional_only || set_function_args.contains(&p.name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
completions.add(Completion::argument(
|
||||
&p.name,
|
||||
p.ty,
|
||||
@@ -1374,7 +1374,7 @@ fn add_unimported_completions<'db>(
|
||||
builtin: false,
|
||||
// TODO: `is_type_check_only` requires inferring the type of the symbol
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
is_context_specific: false,
|
||||
documentation: None,
|
||||
});
|
||||
}
|
||||
@@ -3088,9 +3088,9 @@ class Foo(<CURSOR>):
|
||||
);
|
||||
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
|
||||
metaclass=
|
||||
Bar
|
||||
Foo
|
||||
metaclass=
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3106,9 +3106,9 @@ class Bar: ...
|
||||
);
|
||||
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
|
||||
metaclass=
|
||||
Bar
|
||||
Foo
|
||||
metaclass=
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3124,9 +3124,9 @@ class Bar: ...
|
||||
);
|
||||
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
|
||||
metaclass=
|
||||
Bar
|
||||
Foo
|
||||
metaclass=
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3140,9 +3140,9 @@ class Foo(<CURSOR>",
|
||||
);
|
||||
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
|
||||
metaclass=
|
||||
Bar
|
||||
Foo
|
||||
metaclass=
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3804,8 +3804,8 @@ bar(o<CURSOR>
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@"
|
||||
foo
|
||||
okay=
|
||||
foo
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -3825,8 +3825,8 @@ bar(o<CURSOR>
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@"
|
||||
foo
|
||||
okay=
|
||||
foo
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -3940,10 +3940,10 @@ bar(o<CURSOR>
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_builtins().skip_auto_import().build().snapshot(),
|
||||
@"
|
||||
foo
|
||||
okay=
|
||||
okay_abc=
|
||||
okay_okay=
|
||||
foo
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -3961,9 +3961,9 @@ bar(<CURSOR>
|
||||
);
|
||||
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"
|
||||
okay=
|
||||
bar
|
||||
foo
|
||||
okay=
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -218,6 +218,7 @@ fn render_markdown(docstring: &str) -> String {
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
first_line = false;
|
||||
|
||||
// If we're in a literal block and we find a non-empty dedented line, end the block
|
||||
// TODO: we should remove all the trailing blank lines
|
||||
@@ -273,6 +274,22 @@ fn render_markdown(docstring: &str) -> String {
|
||||
block_indent = line_indent;
|
||||
in_any_code = true;
|
||||
in_markdown_with_fence = Some(fence.to_owned());
|
||||
// Render the line verbatim without its indent and move on.
|
||||
//
|
||||
// If there's any indent this is really just Bad Syntax but it "makes sense"
|
||||
// to someone writing docs like this:
|
||||
//
|
||||
// Returns:
|
||||
// Some details...
|
||||
// ```
|
||||
// some_example()
|
||||
// ```
|
||||
// etc etc...
|
||||
//
|
||||
// We "make this work" by stripping the indent on the fences but preserving the
|
||||
// full indent of the lines between the fences
|
||||
output.push_str(line);
|
||||
continue;
|
||||
}
|
||||
// If we're in a markdown code fence and this line seems to terminate it, end the block
|
||||
} else if let Some(fence) = &in_markdown_with_fence
|
||||
@@ -281,6 +298,9 @@ fn render_markdown(docstring: &str) -> String {
|
||||
in_any_code = false;
|
||||
block_indent = 0;
|
||||
in_markdown_with_fence = None;
|
||||
// Render the line without its indent and move on.
|
||||
output.push_str(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're not in a codeblock and we see something that signals a literal block, start one
|
||||
@@ -446,8 +466,6 @@ fn render_markdown(docstring: &str) -> String {
|
||||
// Print the line verbatim, it's in code
|
||||
output.push_str(line);
|
||||
}
|
||||
|
||||
first_line = false;
|
||||
}
|
||||
// Flush codeblock
|
||||
if in_any_code {
|
||||
@@ -1208,6 +1226,74 @@ mod tests {
|
||||
");
|
||||
}
|
||||
|
||||
// If an explicit markdown codefence is indented, eat the indent so it renders
|
||||
// "the way the user expects" (as written this is basically invalid markdown,
|
||||
// but it's nice if we handle it anyway because it makes visual sense).
|
||||
#[test]
|
||||
fn explicit_markdown_block_with_indent_tick() {
|
||||
let docstring = r#"
|
||||
My cool func...
|
||||
|
||||
Returns:
|
||||
Some details
|
||||
`````python
|
||||
x_y = thing_do();
|
||||
``` # this should't close the fence!
|
||||
a_b = other_thing();
|
||||
`````
|
||||
And so on.
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
My cool func...
|
||||
|
||||
Returns:
|
||||
Some details
|
||||
`````python
|
||||
x_y = thing_do();
|
||||
``` # this should't close the fence!
|
||||
a_b = other_thing();
|
||||
`````
|
||||
And so on.
|
||||
");
|
||||
}
|
||||
|
||||
// If an explicit markdown codefence is indented, eat the indent so it renders
|
||||
// "the way the user expects" (as written this is basically invalid markdown,
|
||||
// but it's nice if we handle it anyway because it makes visual sense).
|
||||
#[test]
|
||||
fn explicit_markdown_block_with_indent_tilde() {
|
||||
let docstring = r#"
|
||||
My cool func...
|
||||
|
||||
Returns:
|
||||
Some details
|
||||
~~~~~~python
|
||||
x_y = thing_do();
|
||||
~~~ # this should't close the fence!
|
||||
a_b = other_thing();
|
||||
~~~~~~
|
||||
And so on.
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
My cool func...
|
||||
|
||||
Returns:
|
||||
Some details
|
||||
~~~~~~python
|
||||
x_y = thing_do();
|
||||
~~~ # this should't close the fence!
|
||||
a_b = other_thing();
|
||||
~~~~~~
|
||||
And so on.
|
||||
");
|
||||
}
|
||||
|
||||
// What do we do when we hit the end of the docstring with an unclosed markdown block?
|
||||
#[test]
|
||||
fn explicit_markdown_block_with_unclosed_fence_tick() {
|
||||
@@ -1267,7 +1353,7 @@ mod tests {
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
My cool func:
|
||||
|
||||
``````we still think this is a codefence```
|
||||
``````we still think this is a codefence```
|
||||
x_y = thing_do();
|
||||
```````````` and are sloppy as heck with indentation and closing shrugggg
|
||||
");
|
||||
@@ -1290,7 +1376,7 @@ mod tests {
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
My cool func:
|
||||
|
||||
~~~~~~we still think this is a codefence~~~
|
||||
~~~~~~we still think this is a codefence~~~
|
||||
x_y = thing_do();
|
||||
~~~~~~~~~~~~~ and are sloppy as heck with indentation and closing shrugggg
|
||||
");
|
||||
|
||||
@@ -619,7 +619,7 @@ mod tests {
|
||||
list_snapshot(&db),
|
||||
@r#"
|
||||
[
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
|
||||
]
|
||||
"#,
|
||||
);
|
||||
@@ -662,7 +662,7 @@ mod tests {
|
||||
@r#"
|
||||
[
|
||||
Module::File("asyncio", "std-custom", "/typeshed/stdlib/asyncio/__init__.pyi", Package, None),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
|
||||
Module::File("random", "std-custom", "/typeshed/stdlib/random.pyi", Module, None),
|
||||
]
|
||||
"#,
|
||||
@@ -755,7 +755,7 @@ mod tests {
|
||||
[
|
||||
Module::File("asyncio", "std-custom", "/typeshed/stdlib/asyncio/__init__.pyi", Package, None),
|
||||
Module::File("collections", "std-custom", "/typeshed/stdlib/collections/__init__.pyi", Package, Some(Collections)),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
|
||||
]
|
||||
"#,
|
||||
);
|
||||
@@ -1091,7 +1091,7 @@ mod tests {
|
||||
list_snapshot(&db),
|
||||
@r#"
|
||||
[
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
|
||||
]
|
||||
"#,
|
||||
);
|
||||
@@ -1107,7 +1107,7 @@ mod tests {
|
||||
list_snapshot(&db),
|
||||
@r#"
|
||||
[
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
|
||||
]
|
||||
"#,
|
||||
);
|
||||
@@ -1129,7 +1129,7 @@ mod tests {
|
||||
list_snapshot(&db),
|
||||
@r#"
|
||||
[
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
|
||||
]
|
||||
"#,
|
||||
);
|
||||
@@ -1191,7 +1191,7 @@ mod tests {
|
||||
list_snapshot(&db),
|
||||
@r#"
|
||||
[
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, None),
|
||||
Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
|
||||
]
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -320,6 +320,7 @@ pub enum KnownModule {
|
||||
Abc,
|
||||
Contextlib,
|
||||
Dataclasses,
|
||||
Functools,
|
||||
Collections,
|
||||
Inspect,
|
||||
#[strum(serialize = "string.templatelib")]
|
||||
@@ -351,6 +352,7 @@ impl KnownModule {
|
||||
Self::Abc => "abc",
|
||||
Self::Contextlib => "contextlib",
|
||||
Self::Dataclasses => "dataclasses",
|
||||
Self::Functools => "functools",
|
||||
Self::Collections => "collections",
|
||||
Self::Inspect => "inspect",
|
||||
Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
|
||||
@@ -395,6 +397,10 @@ impl KnownModule {
|
||||
pub const fn is_importlib(self) -> bool {
|
||||
matches!(self, Self::ImportLib)
|
||||
}
|
||||
|
||||
pub const fn is_functools(self) -> bool {
|
||||
matches!(self, Self::Functools)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KnownModule {
|
||||
|
||||
@@ -325,6 +325,10 @@ impl ModulePath {
|
||||
relative_path: relative_path.with_extension("py"),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn into_search_path(self) -> SearchPath {
|
||||
self.search_path
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SystemPathBuf> for ModulePath {
|
||||
|
||||
@@ -32,11 +32,8 @@ specifies ty's implementation of Python's import resolution algorithm.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::iter::FusedIterator;
|
||||
use std::str::Split;
|
||||
|
||||
use compact_str::format_compact;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
@@ -1102,6 +1099,79 @@ fn desperately_resolve_name(
|
||||
resolve_name_impl(db, name, mode, search_paths.iter().flatten())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ResolvedModule {
|
||||
NamespacePackage,
|
||||
LegacyNamespacePackage(File),
|
||||
RegularPackage(File),
|
||||
Module(File),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ModuleResolutionCandidate {
|
||||
path: ModulePath,
|
||||
module: ResolvedModule,
|
||||
py_typed: PyTyped,
|
||||
}
|
||||
|
||||
impl ModuleResolutionCandidate {
|
||||
// Is this some kind of namespace package?
|
||||
fn is_any_namespace_package(&self) -> bool {
|
||||
match self.module {
|
||||
ResolvedModule::NamespacePackage => true,
|
||||
ResolvedModule::LegacyNamespacePackage(_) => true,
|
||||
ResolvedModule::RegularPackage(_) => false,
|
||||
ResolvedModule::Module(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
// This is the module we were actually interested in resolving, complete the resolution
|
||||
fn into_resolved_name(self) -> ResolvedName {
|
||||
match self.module {
|
||||
ResolvedModule::NamespacePackage => ResolvedName::NamespacePackage,
|
||||
// legacy namespace packages behave like regular packages when they're the target of the resolution
|
||||
ResolvedModule::LegacyNamespacePackage(file) => {
|
||||
ResolvedName::FileModule(ResolvedFileModule {
|
||||
kind: ModuleKind::Package,
|
||||
search_path: self.path.into_search_path(),
|
||||
file,
|
||||
})
|
||||
}
|
||||
ResolvedModule::RegularPackage(file) => ResolvedName::FileModule(ResolvedFileModule {
|
||||
kind: ModuleKind::Package,
|
||||
search_path: self.path.into_search_path(),
|
||||
file,
|
||||
}),
|
||||
ResolvedModule::Module(file) => ResolvedName::FileModule(ResolvedFileModule {
|
||||
kind: ModuleKind::Module,
|
||||
search_path: self.path.into_search_path(),
|
||||
file,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn missing_submodule_is_terminal(&self) -> bool {
|
||||
if matches!(self.py_typed, PyTyped::Partial) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only regular packages are truly terminal, as a later `foo/__init__.py`
|
||||
// can shadow `foo.py`. Both shadow namespace packages.
|
||||
matches!(self.module, ResolvedModule::RegularPackage(_))
|
||||
}
|
||||
|
||||
fn to_str<'a>(&self, db: &'a dyn Db) -> Cow<'a, str> {
|
||||
match self.module {
|
||||
ResolvedModule::NamespacePackage => {
|
||||
Cow::Owned(self.path.to_system_path().unwrap_or_default().to_string())
|
||||
}
|
||||
ResolvedModule::LegacyNamespacePackage(file) => Cow::Borrowed(file.path(db).as_str()),
|
||||
ResolvedModule::RegularPackage(file) => Cow::Borrowed(file.path(db).as_str()),
|
||||
ResolvedModule::Module(file) => Cow::Borrowed(file.path(db).as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_name_impl<'a>(
|
||||
db: &dyn Db,
|
||||
name: &ModuleName,
|
||||
@@ -1109,109 +1179,250 @@ fn resolve_name_impl<'a>(
|
||||
search_paths: impl Iterator<Item = &'a SearchPath>,
|
||||
) -> Option<ResolvedName> {
|
||||
let python_version = db.python_version();
|
||||
let resolver_state = ResolverContext::new(db, python_version, mode);
|
||||
let context = ResolverContext::new(db, python_version, mode);
|
||||
let is_non_shadowable = mode.is_non_shadowable(python_version.minor, name.as_str());
|
||||
let mut stub_name = None;
|
||||
|
||||
let name = RelaxedModuleName::new(name);
|
||||
let stub_name = name.to_stub_package();
|
||||
let mut is_namespace_package = false;
|
||||
let mut cur_candidates = search_paths
|
||||
.filter_map(|search_path| {
|
||||
// When a builtin module is imported, standard module resolution is bypassed:
|
||||
// the module name always resolves to the stdlib module,
|
||||
// even if there's a module of the same name in the first-party root
|
||||
// (which would normally result in the stdlib module being overridden).
|
||||
// TODO: offer a diagnostic if there is a first-party module of the same name
|
||||
if is_non_shadowable && !search_path.is_standard_library() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for search_path in search_paths {
|
||||
// When a builtin module is imported, standard module resolution is bypassed:
|
||||
// the module name always resolves to the stdlib module,
|
||||
// even if there's a module of the same name in the first-party root
|
||||
// (which would normally result in the stdlib module being overridden).
|
||||
// TODO: offer a diagnostic if there is a first-party module of the same name
|
||||
if is_non_shadowable && !search_path.is_standard_library() {
|
||||
continue;
|
||||
}
|
||||
Some(ModuleResolutionCandidate {
|
||||
path: search_path.to_module_path(),
|
||||
module: ResolvedModule::NamespacePackage,
|
||||
py_typed: PyTyped::Untyped,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut next_candidates = vec![];
|
||||
|
||||
if !search_path.is_standard_library() && resolver_state.mode.stubs_allowed() {
|
||||
match resolve_name_in_search_path(&resolver_state, &stub_name, search_path) {
|
||||
Ok((package_kind, _, ResolvedName::FileModule(module))) => {
|
||||
if package_kind.is_root() && module.kind.is_module() {
|
||||
// FIXME?: because we have to search every candidate on each step of this loop,
|
||||
// in theory we can search them all in parallel. However we need to join the parallelism
|
||||
// at the end of each iteration, and after the first iteration in 99% of cases we will have
|
||||
// reduced down to a single candidate, so maybe meh?
|
||||
let mut is_root = true;
|
||||
for component in name.components() {
|
||||
// Search for the next component in every search-path
|
||||
for mut candidate in cur_candidates.drain(..) {
|
||||
// On the first iteration, look for `mypackage-stubs` as well
|
||||
// Optimization: stdlib never has these `-stubs`
|
||||
if is_root
|
||||
&& context.mode.stubs_allowed()
|
||||
&& !candidate.path.search_path().is_standard_library()
|
||||
{
|
||||
let stub_name = stub_name.get_or_insert_with(|| format!("{component}-stubs"));
|
||||
let mut stub_candidate = candidate.clone();
|
||||
if resolve_name_in_search_path(&context, &mut stub_candidate, stub_name).is_ok() {
|
||||
// `mypackage-stubs.py(i)` is not a valid result
|
||||
if matches!(stub_candidate.module, ResolvedModule::Module(_)) {
|
||||
tracing::trace!(
|
||||
"Search path `{search_path}` contains a module \
|
||||
named `{stub_name}` but a standalone module isn't a valid stub."
|
||||
"Search path `{}` contains a module \
|
||||
named `{stub_name}` but a standalone module isn't a valid stub.",
|
||||
candidate.path.search_path()
|
||||
);
|
||||
} else {
|
||||
return Some(ResolvedName::FileModule(module));
|
||||
let shadows_all = stub_candidate.missing_submodule_is_terminal();
|
||||
next_candidates.push(stub_candidate);
|
||||
if shadows_all {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((_, _, ResolvedName::NamespacePackage)) => {
|
||||
is_namespace_package = true;
|
||||
}
|
||||
Err((PackageKind::Root, _)) => {
|
||||
tracing::trace!(
|
||||
"Search path `{search_path}` contains no stub package named `{stub_name}`."
|
||||
);
|
||||
}
|
||||
Err((PackageKind::Regular, PyTyped::Partial)) => {
|
||||
tracing::trace!(
|
||||
"Stub-package in `{search_path}` doesn't contain module: \
|
||||
`{name}` but it is a partial package, keep going."
|
||||
);
|
||||
// stub exists, but the module doesn't. But this is a partial package,
|
||||
// fall through to looking for a non-stub package
|
||||
}
|
||||
Err((PackageKind::Regular, _)) => {
|
||||
tracing::trace!(
|
||||
"Stub-package in `{search_path}` doesn't contain module: `{name}`"
|
||||
);
|
||||
// stub exists, but the module doesn't.
|
||||
return None;
|
||||
}
|
||||
Err((PackageKind::Namespace, _)) => {
|
||||
tracing::trace!(
|
||||
"Stub-package in `{search_path}` doesn't contain module: \
|
||||
`{name}` but it is a namespace package, keep going."
|
||||
);
|
||||
// stub exists, but the module doesn't. But this is a namespace package,
|
||||
// fall through to looking for a non-stub package
|
||||
}
|
||||
|
||||
if resolve_name_in_search_path(&context, &mut candidate, component).is_err() {
|
||||
if candidate.missing_submodule_is_terminal() {
|
||||
// Everything after this package should be shadowed out by this failure
|
||||
// But the previous results are still in play because they would have
|
||||
// shadowed this one out anyway.
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let shadows_all = candidate.missing_submodule_is_terminal();
|
||||
next_candidates.push(candidate);
|
||||
if shadows_all {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match resolve_name_in_search_path(&resolver_state, &name, search_path) {
|
||||
Ok((_, _, ResolvedName::FileModule(module))) => {
|
||||
return Some(ResolvedName::FileModule(module));
|
||||
// Now that we have several candidates, we need to reject candidates that are shadowed.
|
||||
// There are only two valid situations where we should proceed into the next iteration
|
||||
// with multiple candidates:
|
||||
//
|
||||
// * All the candidates are namespace packages
|
||||
// * `mypackage-stubs` is a candidate with `PyTyped::Partial`
|
||||
//
|
||||
// The existence of a single non-namespace package will shadow
|
||||
// all namespace packages *regardless of search-path order*.
|
||||
//
|
||||
// Similarly, the existence of a single regular package will shadow
|
||||
// all modules (mymod.py) *regardless of search-path order*.
|
||||
//
|
||||
// This is implemented with the `retain` that follows.
|
||||
//
|
||||
// We can't do this "delete all namespace packages" eagerly because we want a
|
||||
// `PyTyped::Partial` regular package to shadow namespace packages after it.
|
||||
// (FIXME: I guess we could just set a flag not to add them...)
|
||||
|
||||
// First record what kinds of things we found
|
||||
let mut found_regular_package = None;
|
||||
let mut found_module = None;
|
||||
let mut found_legacy_namespace_package = None;
|
||||
for candidate in &next_candidates {
|
||||
match (candidate.module, candidate.py_typed) {
|
||||
(ResolvedModule::LegacyNamespacePackage(file), _) => {
|
||||
found_legacy_namespace_package = Some(file);
|
||||
}
|
||||
(ResolvedModule::RegularPackage(file), PyTyped::Untyped | PyTyped::Full) => {
|
||||
found_regular_package = Some(file);
|
||||
}
|
||||
(ResolvedModule::Module(file), PyTyped::Untyped | PyTyped::Full) => {
|
||||
found_module = Some(file);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok((_, _, ResolvedName::NamespacePackage)) => {
|
||||
is_namespace_package = true;
|
||||
}
|
||||
|
||||
next_candidates.retain(|candidate| {
|
||||
if let Some(_legacy) = found_legacy_namespace_package && !matches!(candidate.module, ResolvedModule::LegacyNamespacePackage(_)) {
|
||||
// TODO: it would be nice to emit a warning about this but we just assume it's fine
|
||||
}
|
||||
|
||||
// Regular packages shadow anything that isn't a regular package independent of order
|
||||
if let Some(package) = found_regular_package && !matches!(candidate.module, ResolvedModule::RegularPackage(_)) {
|
||||
tracing::trace!("Discarding namespace package `{}` because a regular package of the same name was found: {}",
|
||||
candidate.to_str(db),
|
||||
package.path(db).as_str(),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Modules shadow namespace packages independent of order
|
||||
if let Some(module) = found_module && candidate.is_any_namespace_package() {
|
||||
tracing::trace!("Discarding namespace package `{}` because a module of the same name was found: {}",
|
||||
candidate.to_str(db),
|
||||
module.path(db).as_str(),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
if next_candidates.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Advance to the next level of candidates while reusing allocations
|
||||
// (we used `drain` so cur_candidates is empty)
|
||||
std::mem::swap(&mut cur_candidates, &mut next_candidates);
|
||||
is_root = false;
|
||||
}
|
||||
|
||||
// We now have a list of candidates that are all correct answers, and we just need to take the
|
||||
// Best one. Because of the filtering we've done in the loop, and sorting stub-packages to come
|
||||
// first, this is in fact just "the first one".
|
||||
cur_candidates
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(ModuleResolutionCandidate::into_resolved_name)
|
||||
}
|
||||
|
||||
/// Attempts to resolve a module name in a particular search path.
|
||||
///
|
||||
/// `search_path` should be the directory to start looking for the module.
|
||||
///
|
||||
/// `name` should be a complete non-empty module name, e.g, `foo` or
|
||||
/// `foo.bar.baz`.
|
||||
///
|
||||
/// Upon success, this returns the kind of the parent package (root, regular
|
||||
/// package or namespace package) along with the resolved details of the
|
||||
/// module: its kind (single-file module or package), the search path in
|
||||
/// which it was found (guaranteed to be equal to the one given) and the
|
||||
/// corresponding `File`.
|
||||
///
|
||||
/// Upon error, the kind of the parent package is returned.
|
||||
fn resolve_name_in_search_path(
|
||||
context: &ResolverContext,
|
||||
candidate: &mut ModuleResolutionCandidate,
|
||||
module_name: &str,
|
||||
) -> Result<(), ()> {
|
||||
if matches!(candidate.module, ResolvedModule::Module(_)) {
|
||||
tracing::trace!(
|
||||
"The non-package {} cannot have child",
|
||||
candidate.to_str(context.db)
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
let package_path = &mut candidate.path;
|
||||
package_path.push(module_name);
|
||||
|
||||
// Check for a regular package first (highest priority)
|
||||
package_path.push("__init__");
|
||||
if let Some(init) = resolve_file_module(package_path, context) {
|
||||
// Remove the `__init__` component for any potential next step
|
||||
package_path.pop();
|
||||
candidate.py_typed = package_path
|
||||
.py_typed(context)
|
||||
.inherit_parent(candidate.py_typed);
|
||||
if is_legacy_namespace_package(package_path, context, init) {
|
||||
candidate.module = ResolvedModule::LegacyNamespacePackage(init);
|
||||
} else {
|
||||
candidate.module = ResolvedModule::RegularPackage(init);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check for a file module next
|
||||
package_path.pop();
|
||||
|
||||
if let Some(file_module) = resolve_file_module(package_path, context) {
|
||||
candidate.module = ResolvedModule::Module(file_module);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Last resort, check if a folder with the given name exists. If so,
|
||||
// then this is a namespace package. We need to skip this check for
|
||||
// typeshed because the `resolve_file_module` can also return `None` if the
|
||||
// `__init__.py` exists but isn't available for the current Python version.
|
||||
// Let's assume that the `xml` module is only available on Python 3.11+ and
|
||||
// we're resolving for Python 3.10:
|
||||
//
|
||||
// * `resolve_file_module("xml/__init__.pyi")` returns `None` even though
|
||||
// the file exists but the module isn't available for the current Python
|
||||
// version.
|
||||
// * The check here would now return `true` because the `xml` directory
|
||||
// exists, resulting in a false positive for a namespace package.
|
||||
//
|
||||
// Since typeshed doesn't use any namespace packages today (May 2025),
|
||||
// simply skip this check which also helps performance. If typeshed
|
||||
// ever uses namespace packages, ensure that this check also takes the
|
||||
// `VERSIONS` file into consideration.
|
||||
if !package_path.search_path().is_standard_library() && package_path.is_directory(context) {
|
||||
if let Some(path) = package_path.to_system_path() {
|
||||
let system = context.db.system();
|
||||
if system.case_sensitivity().is_case_sensitive()
|
||||
|| system.path_exists_case_sensitive(
|
||||
&path,
|
||||
package_path.search_path().as_system_path().unwrap(),
|
||||
)
|
||||
{
|
||||
candidate.py_typed = package_path
|
||||
.py_typed(context)
|
||||
.inherit_parent(candidate.py_typed);
|
||||
candidate.module = ResolvedModule::NamespacePackage;
|
||||
return Ok(());
|
||||
}
|
||||
Err(kind) => match kind {
|
||||
(PackageKind::Root, _) => {
|
||||
tracing::trace!(
|
||||
"Search path `{search_path}` contains no package named `{name}`."
|
||||
);
|
||||
}
|
||||
(PackageKind::Regular, PyTyped::Partial) => {
|
||||
tracing::trace!(
|
||||
"Package in `{search_path}` doesn't contain module: \
|
||||
`{name}` but it is a partial package, keep going."
|
||||
);
|
||||
}
|
||||
(PackageKind::Regular, _) => {
|
||||
// For regular packages, don't search the next search path. All files of that
|
||||
// package must be in the same location
|
||||
tracing::trace!("Package in `{search_path}` doesn't contain module: `{name}`");
|
||||
return None;
|
||||
}
|
||||
(PackageKind::Namespace, _) => {
|
||||
tracing::trace!(
|
||||
"Package in `{search_path}` doesn't contain module: \
|
||||
`{name}` but it is a namespace package, keep going."
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if is_namespace_package {
|
||||
return Some(ResolvedName::NamespacePackage);
|
||||
}
|
||||
|
||||
None
|
||||
Err(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1234,101 +1445,6 @@ struct ResolvedFileModule {
|
||||
file: File,
|
||||
}
|
||||
|
||||
/// Attempts to resolve a module name in a particular search path.
|
||||
///
|
||||
/// `search_path` should be the directory to start looking for the module.
|
||||
///
|
||||
/// `name` should be a complete non-empty module name, e.g, `foo` or
|
||||
/// `foo.bar.baz`.
|
||||
///
|
||||
/// Upon success, this returns the kind of the parent package (root, regular
|
||||
/// package or namespace package) along with the resolved details of the
|
||||
/// module: its kind (single-file module or package), the search path in
|
||||
/// which it was found (guaranteed to be equal to the one given) and the
|
||||
/// corresponding `File`.
|
||||
///
|
||||
/// Upon error, the kind of the parent package is returned.
|
||||
fn resolve_name_in_search_path(
|
||||
context: &ResolverContext,
|
||||
name: &RelaxedModuleName,
|
||||
search_path: &SearchPath,
|
||||
) -> Result<(PackageKind, PyTyped, ResolvedName), (PackageKind, PyTyped)> {
|
||||
let mut components = name.components();
|
||||
let module_name = components.next_back().unwrap();
|
||||
|
||||
let resolved_package = resolve_package(search_path, components, context)?;
|
||||
|
||||
let mut package_path = resolved_package.path;
|
||||
|
||||
package_path.push(module_name);
|
||||
|
||||
// Check for a regular package first (highest priority)
|
||||
package_path.push("__init__");
|
||||
if let Some(regular_package) = resolve_file_module(&package_path, context) {
|
||||
return Ok((
|
||||
resolved_package.kind,
|
||||
resolved_package.typed,
|
||||
ResolvedName::FileModule(ResolvedFileModule {
|
||||
search_path: search_path.clone(),
|
||||
kind: ModuleKind::Package,
|
||||
file: regular_package,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
// Check for a file module next
|
||||
package_path.pop();
|
||||
|
||||
if let Some(file_module) = resolve_file_module(&package_path, context) {
|
||||
return Ok((
|
||||
resolved_package.kind,
|
||||
resolved_package.typed,
|
||||
ResolvedName::FileModule(ResolvedFileModule {
|
||||
file: file_module,
|
||||
kind: ModuleKind::Module,
|
||||
search_path: search_path.clone(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
// Last resort, check if a folder with the given name exists. If so,
|
||||
// then this is a namespace package. We need to skip this check for
|
||||
// typeshed because the `resolve_file_module` can also return `None` if the
|
||||
// `__init__.py` exists but isn't available for the current Python version.
|
||||
// Let's assume that the `xml` module is only available on Python 3.11+ and
|
||||
// we're resolving for Python 3.10:
|
||||
//
|
||||
// * `resolve_file_module("xml/__init__.pyi")` returns `None` even though
|
||||
// the file exists but the module isn't available for the current Python
|
||||
// version.
|
||||
// * The check here would now return `true` because the `xml` directory
|
||||
// exists, resulting in a false positive for a namespace package.
|
||||
//
|
||||
// Since typeshed doesn't use any namespace packages today (May 2025),
|
||||
// simply skip this check which also helps performance. If typeshed
|
||||
// ever uses namespace packages, ensure that this check also takes the
|
||||
// `VERSIONS` file into consideration.
|
||||
if !search_path.is_standard_library() && package_path.is_directory(context) {
|
||||
if let Some(path) = package_path.to_system_path() {
|
||||
let system = context.db.system();
|
||||
if system.case_sensitivity().is_case_sensitive()
|
||||
|| system.path_exists_case_sensitive(
|
||||
&path,
|
||||
package_path.search_path().as_system_path().unwrap(),
|
||||
)
|
||||
{
|
||||
return Ok((
|
||||
resolved_package.kind,
|
||||
resolved_package.typed,
|
||||
ResolvedName::NamespacePackage,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err((resolved_package.kind, resolved_package.typed))
|
||||
}
|
||||
|
||||
/// If `module` exists on disk with either a `.pyi` or `.py` extension,
|
||||
/// return the [`File`] corresponding to that path.
|
||||
///
|
||||
@@ -1366,90 +1482,6 @@ pub(super) fn resolve_file_module(
|
||||
Some(file)
|
||||
}
|
||||
|
||||
/// Attempt to resolve the parent package of a module.
|
||||
///
|
||||
/// `module_search_path` should be the directory to start looking for the
|
||||
/// parent package.
|
||||
///
|
||||
/// `components` should be the full module name of the parent package. This
|
||||
/// specifically should not include the basename of the module. So e.g.,
|
||||
/// for `foo.bar.baz`, `components` should be `[foo, bar]`. It follows that
|
||||
/// `components` may be empty (in which case, the parent package is the root).
|
||||
///
|
||||
/// Upon success, the path to the package and its "kind" (root, regular or
|
||||
/// namespace) is returned. Upon error, the kind of the package is still
|
||||
/// returned based on how many components were found and whether `__init__.py`
|
||||
/// is present.
|
||||
fn resolve_package<'a, 'db, I>(
|
||||
module_search_path: &SearchPath,
|
||||
components: I,
|
||||
resolver_state: &ResolverContext<'db>,
|
||||
) -> Result<ResolvedPackage, (PackageKind, PyTyped)>
|
||||
where
|
||||
I: Iterator<Item = &'a str>,
|
||||
{
|
||||
let mut package_path = module_search_path.to_module_path();
|
||||
|
||||
// `true` if inside a folder that is a namespace package (has no `__init__.py`).
|
||||
// Namespace packages are special because they can be spread across multiple search paths.
|
||||
// https://peps.python.org/pep-0420/
|
||||
let mut in_namespace_package = false;
|
||||
|
||||
// `true` if resolving a sub-package. For example, `true` when resolving `bar` of `foo.bar`.
|
||||
let mut in_sub_package = false;
|
||||
|
||||
let mut typed = package_path.py_typed(resolver_state);
|
||||
|
||||
// For `foo.bar.baz`, test that `foo` and `bar` both contain a `__init__.py`.
|
||||
for folder in components {
|
||||
package_path.push(folder);
|
||||
typed = package_path.py_typed(resolver_state).inherit_parent(typed);
|
||||
|
||||
let is_regular_package = package_path.is_regular_package(resolver_state);
|
||||
|
||||
if is_regular_package {
|
||||
// This is the only place where we need to consider the existence of legacy namespace
|
||||
// packages, as we are explicitly searching for the *parent* package of the module
|
||||
// we actually want. Here, such a package should be treated as a PEP-420 ("modern")
|
||||
// namespace package. In all other contexts it acts like a normal package and needs
|
||||
// no special handling.
|
||||
in_namespace_package = is_legacy_namespace_package(&package_path, resolver_state);
|
||||
} else if package_path.is_directory(resolver_state)
|
||||
// Pure modules hide namespace packages with the same name
|
||||
&& resolve_file_module(&package_path, resolver_state).is_none()
|
||||
{
|
||||
// A directory without an `__init__.py(i)` is a namespace package,
|
||||
// continue with the next folder.
|
||||
in_namespace_package = true;
|
||||
} else if in_namespace_package {
|
||||
// Package not found but it is part of a namespace package.
|
||||
return Err((PackageKind::Namespace, typed));
|
||||
} else if in_sub_package {
|
||||
// A regular sub package wasn't found.
|
||||
return Err((PackageKind::Regular, typed));
|
||||
} else {
|
||||
// We couldn't find `foo` for `foo.bar.baz`, search the next search path.
|
||||
return Err((PackageKind::Root, typed));
|
||||
}
|
||||
|
||||
in_sub_package = true;
|
||||
}
|
||||
|
||||
let kind = if in_namespace_package {
|
||||
PackageKind::Namespace
|
||||
} else if in_sub_package {
|
||||
PackageKind::Regular
|
||||
} else {
|
||||
PackageKind::Root
|
||||
};
|
||||
|
||||
Ok(ResolvedPackage {
|
||||
kind,
|
||||
path: package_path,
|
||||
typed,
|
||||
})
|
||||
}
|
||||
|
||||
/// Determines whether a package is a legacy namespace package.
|
||||
///
|
||||
/// Before PEP 420 introduced implicit namespace packages, the ecosystem developed
|
||||
@@ -1479,19 +1511,14 @@ where
|
||||
/// we will just get confused if you mess it up).
|
||||
fn is_legacy_namespace_package(
|
||||
package_path: &ModulePath,
|
||||
resolver_state: &ResolverContext,
|
||||
context: &ResolverContext,
|
||||
init: File,
|
||||
) -> bool {
|
||||
// Just an optimization, the stdlib and typeshed are never legacy namespace packages
|
||||
if package_path.search_path().is_standard_library() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut package_path = package_path.clone();
|
||||
package_path.push("__init__");
|
||||
let Some(init) = resolve_file_module(&package_path, resolver_state) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// This is all syntax-only analysis so it *could* be fooled but it's really unlikely.
|
||||
//
|
||||
// The benefit of being syntax-only is speed and avoiding circular dependencies
|
||||
@@ -1499,44 +1526,13 @@ fn is_legacy_namespace_package(
|
||||
//
|
||||
// The downside is if you write slightly different syntax we will fail to detect the idiom,
|
||||
// but hey, this is better than nothing!
|
||||
let parsed = ruff_db::parsed::parsed_module(resolver_state.db, init);
|
||||
let parsed = ruff_db::parsed::parsed_module(context.db, init);
|
||||
let mut visitor = LegacyNamespacePackageVisitor::default();
|
||||
visitor.visit_body(parsed.load(resolver_state.db).suite());
|
||||
visitor.visit_body(parsed.load(context.db).suite());
|
||||
|
||||
visitor.is_legacy_namespace_package
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ResolvedPackage {
|
||||
path: ModulePath,
|
||||
kind: PackageKind,
|
||||
typed: PyTyped,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum PackageKind {
|
||||
/// A root package or module. E.g. `foo` in `foo.bar.baz` or just `foo`.
|
||||
Root,
|
||||
|
||||
/// A regular sub-package where the parent contains an `__init__.py`.
|
||||
///
|
||||
/// For example, `bar` in `foo.bar` when the `foo` directory contains an `__init__.py`.
|
||||
Regular,
|
||||
|
||||
/// A sub-package in a namespace package. A namespace package is a package
|
||||
/// without an `__init__.py`.
|
||||
///
|
||||
/// For example, `bar` in `foo.bar` if the `foo` directory contains no
|
||||
/// `__init__.py`.
|
||||
Namespace,
|
||||
}
|
||||
|
||||
impl PackageKind {
|
||||
pub(crate) const fn is_root(self) -> bool {
|
||||
matches!(self, PackageKind::Root)
|
||||
}
|
||||
}
|
||||
|
||||
/// Info about the `py.typed` file for this package
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub(crate) enum PyTyped {
|
||||
@@ -1587,34 +1583,6 @@ impl<'db> ResolverContext<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`ModuleName`] but with relaxed semantics to allow `<package>-stubs.path`
|
||||
#[derive(Debug)]
|
||||
struct RelaxedModuleName(compact_str::CompactString);
|
||||
|
||||
impl RelaxedModuleName {
|
||||
fn new(name: &ModuleName) -> Self {
|
||||
Self(name.as_str().into())
|
||||
}
|
||||
|
||||
fn components(&self) -> Split<'_, char> {
|
||||
self.0.split('.')
|
||||
}
|
||||
|
||||
fn to_stub_package(&self) -> Self {
|
||||
if let Some((package, rest)) = self.0.split_once('.') {
|
||||
Self(format_compact!("{package}-stubs.{rest}"))
|
||||
} else {
|
||||
Self(format_compact!("{package}-stubs", package = self.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RelaxedModuleName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects if a module contains a statement of the form:
|
||||
/// ```python
|
||||
/// __path__ = pkgutil.extend_path(__path__, __name__)
|
||||
@@ -1926,14 +1894,12 @@ mod tests {
|
||||
asyncio: 3.8- # 'Regular' package on py38+
|
||||
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
|
||||
functools: 3.8- # Top-level single-file module
|
||||
xml: 3.8-3.8 # Namespace package on py38 only
|
||||
";
|
||||
|
||||
const STDLIB: &[FileSpec] = &[
|
||||
("asyncio/__init__.pyi", ""),
|
||||
("asyncio/tasks.pyi", ""),
|
||||
("functools.pyi", ""),
|
||||
("xml/etree.pyi", ""),
|
||||
];
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
@@ -1946,7 +1912,7 @@ mod tests {
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
let existing_modules = create_module_names(&["asyncio", "functools"]);
|
||||
for module_name in existing_modules {
|
||||
let resolved_module =
|
||||
resolve_module_confident(&db, &module_name).unwrap_or_else(|| {
|
||||
@@ -1970,16 +1936,12 @@ mod tests {
|
||||
asyncio: 3.8- # 'Regular' package on py38+
|
||||
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
|
||||
collections: 3.9- # 'Regular' package on py39+
|
||||
importlib: 3.9- # Namespace package on py39+
|
||||
xml: 3.8-3.8 # Namespace package on 3.8 only
|
||||
";
|
||||
|
||||
const STDLIB: &[FileSpec] = &[
|
||||
("collections/__init__.pyi", ""),
|
||||
("asyncio/__init__.pyi", ""),
|
||||
("asyncio/tasks.pyi", ""),
|
||||
("importlib/abc.pyi", ""),
|
||||
("xml/etree.pyi", ""),
|
||||
];
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
@@ -1992,13 +1954,7 @@ mod tests {
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&[
|
||||
"collections",
|
||||
"importlib",
|
||||
"importlib.abc",
|
||||
"xml",
|
||||
"asyncio.tasks",
|
||||
]);
|
||||
let nonexisting_modules = create_module_names(&["collections", "asyncio.tasks"]);
|
||||
|
||||
for module_name in nonexisting_modules {
|
||||
assert!(
|
||||
@@ -2015,7 +1971,6 @@ mod tests {
|
||||
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
|
||||
collections: 3.9- # 'Regular' package on py39+
|
||||
functools: 3.8- # Top-level single-file module
|
||||
importlib: 3.9- # Namespace package on py39+
|
||||
";
|
||||
|
||||
const STDLIB: &[FileSpec] = &[
|
||||
@@ -2023,7 +1978,6 @@ mod tests {
|
||||
("asyncio/tasks.pyi", ""),
|
||||
("collections/__init__.pyi", ""),
|
||||
("functools.pyi", ""),
|
||||
("importlib/abc.pyi", ""),
|
||||
];
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
@@ -2036,13 +1990,8 @@ mod tests {
|
||||
.with_python_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&[
|
||||
"asyncio",
|
||||
"functools",
|
||||
"importlib.abc",
|
||||
"collections",
|
||||
"asyncio.tasks",
|
||||
]);
|
||||
let existing_modules =
|
||||
create_module_names(&["asyncio", "functools", "collections", "asyncio.tasks"]);
|
||||
|
||||
for module_name in existing_modules {
|
||||
let resolved_module =
|
||||
@@ -2444,7 +2393,7 @@ mod tests {
|
||||
fn adding_file_to_search_path_with_lower_priority_does_not_invalidate_query() {
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
versions: "functools: 3.8-",
|
||||
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
|
||||
stdlib_files: &[("functools/__init__.pyi", "def update_wrapper(): ...")],
|
||||
};
|
||||
|
||||
let TestCase {
|
||||
@@ -2458,7 +2407,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let stdlib_functools_path = stdlib.join("functools.pyi");
|
||||
let stdlib_functools_path = stdlib.join("functools/__init__.pyi");
|
||||
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
|
||||
@@ -2470,7 +2419,7 @@ mod tests {
|
||||
// Adding a file to site-packages does not invalidate the query,
|
||||
// since site-packages takes lower priority in the module resolution
|
||||
db.clear_salsa_events();
|
||||
let site_packages_functools_path = site_packages.join("functools.py");
|
||||
let site_packages_functools_path = site_packages.join("functools/__init__.py");
|
||||
db.write_file(&site_packages_functools_path, "f: int")
|
||||
.unwrap();
|
||||
let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
|
||||
|
||||
@@ -14,6 +14,7 @@ license.workspace = true
|
||||
[dependencies]
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["cache", "serde"] }
|
||||
ruff_diagnostics = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_memory_usage = { workspace = true }
|
||||
ruff_options_metadata = { workspace = true }
|
||||
@@ -30,7 +31,7 @@ anyhow = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
crossbeam = { workspace = true }
|
||||
get-size2 = { workspace = true }
|
||||
get-size2 = { workspace = true, features = ["ordermap"] }
|
||||
globset = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
ordermap = { workspace = true, features = ["serde"] }
|
||||
@@ -48,8 +49,10 @@ toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true, features = ["redactions", "ron"] }
|
||||
ruff_db = { workspace = true, features = ["testing"] }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
|
||||
insta = { workspace = true, features = ["redactions", "ron"] }
|
||||
|
||||
[features]
|
||||
default = ["zstd"]
|
||||
|
||||
794
crates/ty_project/src/fixes.rs
Normal file
794
crates/ty_project/src/fixes.rs
Normal file
@@ -0,0 +1,794 @@
|
||||
use ruff_db::cancellation::{Canceled, CancellationToken};
|
||||
use ruff_db::diagnostic::{DisplayDiagnosticConfig, DisplayDiagnostics};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::SourceText;
|
||||
use ruff_db::system::{SystemPath, WritableSystem};
|
||||
use ruff_db::{
|
||||
diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span},
|
||||
files::File,
|
||||
source::source_text,
|
||||
};
|
||||
use ruff_diagnostics::{Fix, IsolationLevel, SourceMap};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use rustc_hash::FxHashSet;
|
||||
use salsa::Setter as _;
|
||||
use std::collections::BTreeMap;
|
||||
use thiserror::Error;
|
||||
use ty_python_semantic::{UNUSED_IGNORE_COMMENT, suppress_all};
|
||||
|
||||
use crate::Db;
|
||||
|
||||
pub struct SuppressAllResult {
|
||||
/// The non-lint diagnostics that can't be suppressed or the diagnostics of files
|
||||
/// that couldn't be suppressed (because ty failed to write the result back to disk,
|
||||
/// or the file contains syntax errors).
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
|
||||
/// The number of diagnostics that were suppressed.
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
/// Adds suppressions to all lint diagnostics and writes the changed files back to disk.
|
||||
///
|
||||
/// Returns how many diagnostics were suppressed along the remaining, non-suppressed diagnostics.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If the `db`'s system isn't [writable](WritableSystem).
|
||||
pub fn suppress_all_diagnostics(
|
||||
db: &mut dyn Db,
|
||||
mut diagnostics: Vec<Diagnostic>,
|
||||
cancellation_token: &CancellationToken,
|
||||
) -> Result<SuppressAllResult, Canceled> {
|
||||
let system = WritableSystem::dyn_clone(
|
||||
db.system()
|
||||
.as_writable()
|
||||
.expect("System should be writable"),
|
||||
);
|
||||
|
||||
let has_fixable = diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic
|
||||
.primary_span()
|
||||
.and_then(|span| span.range())
|
||||
.is_some()
|
||||
&& diagnostic.id().is_lint()
|
||||
&& diagnostic.id() != DiagnosticId::Lint(UNUSED_IGNORE_COMMENT.name())
|
||||
});
|
||||
|
||||
// Early return if there are no diagnostics that can be suppressed to avoid all the heavy work below.
|
||||
if !has_fixable {
|
||||
return Ok(SuppressAllResult {
|
||||
diagnostics,
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let mut by_file: BTreeMap<File, Vec<_>> = BTreeMap::new();
|
||||
|
||||
// Group the diagnostics by file, leave the file-agnostic diagnostics in `diagnostics`.
|
||||
for diagnostic in diagnostics.extract_if(.., |diagnostic| diagnostic.primary_span().is_some()) {
|
||||
let span = diagnostic
|
||||
.primary_span()
|
||||
.expect("should be set because `extract_if` only yields elements with a primary_span");
|
||||
|
||||
by_file
|
||||
.entry(span.expect_ty_file())
|
||||
.or_default()
|
||||
.push(diagnostic);
|
||||
}
|
||||
|
||||
let mut fixed_count = 0usize;
|
||||
let project = db.project();
|
||||
|
||||
// Try to suppress all lint-diagnostics in the given file.
|
||||
for (&file, file_diagnostics) in &mut by_file {
|
||||
if cancellation_token.is_cancelled() {
|
||||
return Err(Canceled);
|
||||
}
|
||||
|
||||
let Some(path) = file.path(db).as_system_path() else {
|
||||
tracing::debug!(
|
||||
"Skipping file `{}` with non-system path because vendored and system virtual file paths are read-only",
|
||||
file.path(db)
|
||||
);
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
let parsed = parsed_module(db, file);
|
||||
if parsed.load(db).has_syntax_errors() {
|
||||
tracing::warn!("Skipping file `{path}` with syntax errors",);
|
||||
continue;
|
||||
}
|
||||
|
||||
let fixable_diagnostics: Vec<_> = file_diagnostics
|
||||
.iter()
|
||||
.filter_map(|diagnostic| {
|
||||
let lint_id = diagnostic.id().as_lint()?;
|
||||
|
||||
// Don't suppress unused ignore comments.
|
||||
if lint_id == UNUSED_IGNORE_COMMENT.name() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// We can't suppress diagnostics without a corresponding file or range.
|
||||
let span = diagnostic.primary_span()?;
|
||||
let range = span.range()?;
|
||||
|
||||
Some((lint_id, range))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if fixable_diagnostics.is_empty() {
|
||||
tracing::debug!(
|
||||
"Skipping file `{path}` because it contains no suppressable diagnostics"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Suppressing {} diagnostics in `{path}`.",
|
||||
fixable_diagnostics.len()
|
||||
);
|
||||
|
||||
// Required to work around borrow checker issues.
|
||||
let path = path.to_path_buf();
|
||||
let fixes = suppress_all(db, file, &fixable_diagnostics);
|
||||
let source = source_text(db, file);
|
||||
|
||||
// TODO: Handle overlapping fixes when adding support for `--fix` by iterating until all fixes
|
||||
// were successfully applied. We don't need to do that for suppressions because suppression fixes
|
||||
// should never overlap (and, if they were, the worst outcome is that some suppressions are missing).
|
||||
let FixedCode {
|
||||
source: new_source,
|
||||
source_map,
|
||||
} = apply_fixes(&source, fixes).unwrap_or_else(|fixed| fixed);
|
||||
|
||||
let new_source = source.with_text(new_source, &source_map);
|
||||
|
||||
// Verify that the fix didn't introduce any syntax errors by overriding
|
||||
// the source text for `file`.
|
||||
let mut source_guard = WithUpdatedSourceGuard::new(db, file, &source, new_source.clone());
|
||||
let db = source_guard.db();
|
||||
let new_parsed = parsed_module(db, file);
|
||||
let new_parsed = new_parsed.load(db);
|
||||
|
||||
if new_parsed.has_syntax_errors() {
|
||||
let mut diag = Diagnostic::new(
|
||||
DiagnosticId::InternalError,
|
||||
Severity::Fatal,
|
||||
format_args!(
|
||||
"Adding suppressions introduced a syntax error. Reverting all changes."
|
||||
),
|
||||
);
|
||||
|
||||
let mut file_annotation = Annotation::primary(Span::from(file));
|
||||
file_annotation.hide_snippet(true);
|
||||
diag.annotate(file_annotation);
|
||||
|
||||
let parse_diagnostics: Vec<_> = new_parsed
|
||||
.errors()
|
||||
.iter()
|
||||
.map(|error| {
|
||||
Diagnostic::invalid_syntax(Span::from(file), &error.error, error.location)
|
||||
})
|
||||
.collect();
|
||||
|
||||
diag.add_bug_sub_diagnostics("%5BFix%20error%5D");
|
||||
|
||||
let file_db: &dyn ruff_db::Db = db;
|
||||
|
||||
diag.info(format_args!(
|
||||
"Introduced syntax errors:\n\n{}",
|
||||
DisplayDiagnostics::new(
|
||||
&file_db,
|
||||
&DisplayDiagnosticConfig::default(),
|
||||
&parse_diagnostics
|
||||
)
|
||||
));
|
||||
|
||||
file_diagnostics.push(diag);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write the changes back to disk.
|
||||
if let Err(err) = write_changes(db, &*system, file, &path, &new_source) {
|
||||
let mut diag = Diagnostic::new(
|
||||
DiagnosticId::Io,
|
||||
Severity::Error,
|
||||
format_args!("Failed to write fixes to file: {err}"),
|
||||
);
|
||||
|
||||
diag.annotate(Annotation::primary(Span::from(file)));
|
||||
diagnostics.push(diag);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we got here then we've been successful. Re-check to get the diagnostics with the
|
||||
// update source, update the fix count.
|
||||
|
||||
if fixable_diagnostics.len() == file_diagnostics.len() {
|
||||
file_diagnostics.clear();
|
||||
} else {
|
||||
// If there are any other file level diagnostics, call `check_file` to re-compute them
|
||||
// with updated ranges.
|
||||
let diagnostics = project.check_file(db, file);
|
||||
*file_diagnostics = diagnostics;
|
||||
}
|
||||
|
||||
fixed_count += fixable_diagnostics.len();
|
||||
// Don't restore the source text or we risk a panic when rendering the diagnostics
|
||||
// if reading any of the fixed files fails (for whatever reason).
|
||||
// The override will get removed on the next `File::sync_path` call.
|
||||
source_guard.defuse();
|
||||
}
|
||||
|
||||
// Stitch the remaining diagnostics back together.
|
||||
diagnostics.extend(by_file.into_values().flatten());
|
||||
diagnostics.sort_by(|left, right| {
|
||||
left.rendering_sort_key(db)
|
||||
.cmp(&right.rendering_sort_key(db))
|
||||
});
|
||||
|
||||
Ok(SuppressAllResult {
|
||||
diagnostics,
|
||||
count: fixed_count,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_changes(
|
||||
db: &dyn Db,
|
||||
system: &dyn WritableSystem,
|
||||
file: File,
|
||||
path: &SystemPath,
|
||||
new_source: &SourceText,
|
||||
) -> Result<(), WriteChangesError> {
|
||||
let metadata = system.path_metadata(path)?;
|
||||
|
||||
if metadata.revision() != file.revision(db) {
|
||||
return Err(WriteChangesError::FileWasModified);
|
||||
}
|
||||
|
||||
system.write_file_bytes(path, &new_source.to_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum WriteChangesError {
|
||||
#[error("failed to write changes to disk: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("the file has been modified")]
|
||||
FileWasModified,
|
||||
}
|
||||
|
||||
/// Apply a series of fixes to `File` and returns the updated source code along with the source map.
|
||||
///
|
||||
/// Returns an error if not all fixes were applied because some fixes are overlapping.
|
||||
fn apply_fixes(source: &str, mut fixes: Vec<Fix>) -> Result<FixedCode, FixedCode> {
|
||||
let mut output = String::with_capacity(source.len());
|
||||
let mut last_pos: Option<TextSize> = None;
|
||||
let mut has_overlapping_fixes = false;
|
||||
let mut isolated: FxHashSet<u32> = FxHashSet::default();
|
||||
|
||||
let mut source_map = SourceMap::default();
|
||||
|
||||
fixes.sort_unstable_by_key(Fix::min_start);
|
||||
|
||||
for fix in fixes {
|
||||
let mut edits = fix.edits().iter().peekable();
|
||||
|
||||
// If the fix contains at least one new edit, enforce isolation and positional requirements.
|
||||
if let Some(first) = edits.peek() {
|
||||
// If this fix requires isolation, and we've already applied another fix in the
|
||||
// same isolation group, skip it.
|
||||
if let IsolationLevel::Group(id) = fix.isolation() {
|
||||
if !isolated.insert(id) {
|
||||
has_overlapping_fixes = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos.is_some_and(|last_pos| last_pos >= first.start()) {
|
||||
has_overlapping_fixes = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut applied_edits = Vec::with_capacity(fix.edits().len());
|
||||
for edit in edits {
|
||||
// Add all contents from `last_pos` to `fix.location`.
|
||||
let slice = &source[TextRange::new(last_pos.unwrap_or_default(), edit.start())];
|
||||
output.push_str(slice);
|
||||
|
||||
// Add the start source marker for the patch.
|
||||
source_map.push_start_marker(edit, output.text_len());
|
||||
|
||||
// Add the patch itself.
|
||||
output.push_str(edit.content().unwrap_or_default());
|
||||
|
||||
// Add the end source marker for the added patch.
|
||||
source_map.push_end_marker(edit, output.text_len());
|
||||
|
||||
// Track that the edit was applied.
|
||||
last_pos = Some(edit.end());
|
||||
applied_edits.push(edit);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the remaining content.
|
||||
let slice = &source[last_pos.unwrap_or_default().to_usize()..];
|
||||
output.push_str(slice);
|
||||
|
||||
let fixed = FixedCode {
|
||||
source: output,
|
||||
source_map,
|
||||
};
|
||||
|
||||
if has_overlapping_fixes {
|
||||
Err(fixed)
|
||||
} else {
|
||||
Ok(fixed)
|
||||
}
|
||||
}
|
||||
|
||||
struct FixedCode {
|
||||
/// Source map that allows mapping positions in the fixed code back to positions in the original
|
||||
/// source code (useful for mapping fixed lines back to their original notebook cells).
|
||||
source_map: SourceMap,
|
||||
|
||||
/// The fixed source code
|
||||
source: String,
|
||||
}
|
||||
|
||||
/// Guard that sets [`File::set_source_text_override`] and guarantees to restore the original source
|
||||
/// text unless the guard is explicitly defused.
|
||||
struct WithUpdatedSourceGuard<'db> {
|
||||
db: &'db mut dyn Db,
|
||||
file: File,
|
||||
old_source: Option<SourceText>,
|
||||
}
|
||||
|
||||
impl<'db> WithUpdatedSourceGuard<'db> {
|
||||
fn new(
|
||||
db: &'db mut dyn Db,
|
||||
file: File,
|
||||
old_source: &SourceText,
|
||||
new_source: SourceText,
|
||||
) -> Self {
|
||||
file.set_source_text_override(db).to(Some(new_source));
|
||||
Self {
|
||||
db,
|
||||
file,
|
||||
old_source: Some(old_source.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn defuse(&mut self) {
|
||||
self.old_source = None;
|
||||
}
|
||||
|
||||
fn db(&mut self) -> &mut dyn Db {
|
||||
self.db
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WithUpdatedSourceGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(old_source) = self.old_source.take() {
|
||||
// We don't set `source_text_override` to `None` here because setting the value
|
||||
// invalidates the `source_text` query and there's the chance that reading the file's content
|
||||
// will fail this time (e.g. because the file was deleted), resulting in ty panicking
|
||||
// when trying to render any diagnostic for that file (because all offsets now point nowhere).
|
||||
// The override will be cleared by `File::sync_path`, the next time the revision changes.
|
||||
self.file
|
||||
.set_source_text_override(self.db)
|
||||
.to(Some(old_source));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::cancellation::CancellationTokenSource;
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, DisplayDiagnostics};
|
||||
use ruff_db::files::{File, system_path_to_file};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::name::Name;
|
||||
use rustc_hash::FxHashMap;
|
||||
use ty_python_semantic::UNUSED_IGNORE_COMMENT;
|
||||
use ty_python_semantic::lint::Level;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::metadata::options::Rules;
|
||||
use crate::metadata::value::RangedValue;
|
||||
use crate::{Db, ProjectMetadata, suppress_all_diagnostics};
|
||||
|
||||
#[test]
|
||||
fn simple_suppression() {
|
||||
assert_snapshot!(
|
||||
suppress_all_in(r#"
|
||||
a = b + 10"#
|
||||
),
|
||||
@r"
|
||||
Added 1 suppressions
|
||||
|
||||
## Fixed source
|
||||
|
||||
```py
|
||||
a = b + 10 # ty:ignore[unresolved-reference]
|
||||
```
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_suppressions_same_code() {
|
||||
assert_snapshot!(
|
||||
suppress_all_in(r#"
|
||||
a = b + 10 + c"#
|
||||
),
|
||||
@r"
|
||||
Added 2 suppressions
|
||||
|
||||
## Fixed source
|
||||
|
||||
```py
|
||||
a = b + 10 + c # ty:ignore[unresolved-reference]
|
||||
```
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_suppressions_different_codes() {
|
||||
assert_snapshot!(
|
||||
suppress_all_in(r#"
|
||||
import sys
|
||||
a = b + 10 + sys.veeersion"#
|
||||
),
|
||||
@r"
|
||||
Added 2 suppressions
|
||||
|
||||
## Fixed source
|
||||
|
||||
```py
|
||||
import sys
|
||||
a = b + 10 + sys.veeersion # ty:ignore[unresolved-attribute, unresolved-reference]
|
||||
```
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_fix_unused_ignore() {
|
||||
assert_snapshot!(
|
||||
suppress_all_in(r#"
|
||||
import sys
|
||||
a = 5 + 10 # ty: ignore[unresolved-reference]"#
|
||||
),
|
||||
@r"
|
||||
Added 0 suppressions
|
||||
|
||||
## Fixed source
|
||||
|
||||
```py
|
||||
import sys
|
||||
a = 5 + 10 # ty: ignore[unresolved-reference]
|
||||
```
|
||||
|
||||
## Diagnostics after applying fixes
|
||||
|
||||
warning[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||
--> test.py:2:13
|
||||
|
|
||||
1 | import sys
|
||||
2 | a = 5 + 10 # ty: ignore[unresolved-reference]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Remove the unused suppression comment
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_fix_files_containing_syntax_errors() {
|
||||
assert_snapshot!(
|
||||
suppress_all_in(r#"
|
||||
import sys
|
||||
a = x +
|
||||
"#
|
||||
),
|
||||
@r"
|
||||
Added 0 suppressions
|
||||
|
||||
## Fixed source
|
||||
|
||||
```py
|
||||
import sys
|
||||
a = x +
|
||||
```
|
||||
|
||||
## Diagnostics after applying fixes
|
||||
|
||||
error[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:2:5
|
||||
|
|
||||
1 | import sys
|
||||
2 | a = x +
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[invalid-syntax]: Expected an expression
|
||||
--> test.py:2:8
|
||||
|
|
||||
1 | import sys
|
||||
2 | a = x +
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arguments() {
|
||||
assert_snapshot!(
|
||||
suppress_all_in(r#"
|
||||
def test(a, b):
|
||||
pass
|
||||
|
||||
|
||||
test(
|
||||
a = 10,
|
||||
c = "unknown"
|
||||
)
|
||||
"#
|
||||
),
|
||||
@r#"
|
||||
Added 2 suppressions
|
||||
|
||||
## Fixed source
|
||||
|
||||
```py
|
||||
def test(a, b):
|
||||
pass
|
||||
|
||||
|
||||
test(
|
||||
a = 10,
|
||||
c = "unknown" # ty:ignore[unknown-argument]
|
||||
) # ty:ignore[missing-argument]
|
||||
```
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_type() {
|
||||
assert_snapshot!(
|
||||
suppress_all_in(r#"class A:
|
||||
def test(self, b: int) -> str:
|
||||
return "test"
|
||||
|
||||
|
||||
class B(A):
|
||||
def test(
|
||||
self,
|
||||
b: str
|
||||
) -> A.b:
|
||||
pass"#
|
||||
),
|
||||
@r#"
|
||||
Added 2 suppressions
|
||||
|
||||
## Fixed source
|
||||
|
||||
```py
|
||||
class A:
|
||||
def test(self, b: int) -> str:
|
||||
return "test"
|
||||
|
||||
|
||||
class B(A):
|
||||
def test(
|
||||
self,
|
||||
b: str
|
||||
) -> A.b: # ty:ignore[invalid-method-override, unresolved-attribute]
|
||||
pass
|
||||
```
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_ty_ignore() {
|
||||
assert_snapshot!(
|
||||
suppress_all_in(r#"class A:
|
||||
def test(self, b: int) -> str:
|
||||
return "test"
|
||||
|
||||
|
||||
class B(A):
|
||||
def test( # ty:ignore[unresolved-reference]
|
||||
self,
|
||||
b: str
|
||||
) -> A.b:
|
||||
pass"#
|
||||
),
|
||||
@r#"
|
||||
Added 2 suppressions
|
||||
|
||||
## Fixed source
|
||||
|
||||
```py
|
||||
class A:
|
||||
def test(self, b: int) -> str:
|
||||
return "test"
|
||||
|
||||
|
||||
class B(A):
|
||||
def test( # ty:ignore[unresolved-reference, invalid-method-override]
|
||||
self,
|
||||
b: str
|
||||
) -> A.b: # ty:ignore[unresolved-attribute]
|
||||
pass
|
||||
```
|
||||
|
||||
## Diagnostics after applying fixes
|
||||
|
||||
warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-reference'
|
||||
--> test.py:7:28
|
||||
|
|
||||
6 | class B(A):
|
||||
7 | def test( # ty:ignore[unresolved-reference, invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
8 | self,
|
||||
9 | b: str
|
||||
|
|
||||
help: Remove the unused suppression code
|
||||
"#);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn suppress_all_in(source: &str) -> String {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let mut metadata = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("."));
|
||||
metadata.options.rules = Some(Rules::from_iter([(
|
||||
RangedValue::cli(UNUSED_IGNORE_COMMENT.name.to_string()),
|
||||
RangedValue::cli(Level::Warn),
|
||||
)]));
|
||||
|
||||
let mut db = TestDb::new(metadata);
|
||||
db.init_program().unwrap();
|
||||
|
||||
db.write_file(
|
||||
"test.py",
|
||||
ruff_python_trivia::textwrap::dedent(source).trim(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "test.py").unwrap();
|
||||
|
||||
let parsed_before = parsed_module(&db, file);
|
||||
let had_syntax_errors = parsed_before.load(&db).has_syntax_errors();
|
||||
|
||||
let diagnostics = db.project().check_file(&db, file);
|
||||
let total_diagnostics = diagnostics.len();
|
||||
let cancellation_token_source = CancellationTokenSource::new();
|
||||
let fixes =
|
||||
suppress_all_diagnostics(&mut db, diagnostics, &cancellation_token_source.token())
|
||||
.expect("operation never gets cancelled");
|
||||
|
||||
assert_eq!(fixes.count, total_diagnostics - fixes.diagnostics.len());
|
||||
|
||||
File::sync_path(&mut db, SystemPath::new("test.py"));
|
||||
|
||||
let fixed = source_text(&db, file);
|
||||
|
||||
let parsed = parsed_module(&db, file);
|
||||
let parsed = parsed.load(&db);
|
||||
|
||||
let diagnostics_after_applying_fixes = db.project().check_file(&db, file);
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"Added {} suppressions\n\n## Fixed source\n\n```py\n{}\n```\n",
|
||||
fixes.count,
|
||||
fixed.as_str()
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !fixes.diagnostics.is_empty() {
|
||||
writeln!(
|
||||
output,
|
||||
"## Diagnostics after applying fixes\n\n{diagnostics}\n",
|
||||
diagnostics = DisplayDiagnostics::new(
|
||||
&db,
|
||||
&DisplayDiagnosticConfig::default(),
|
||||
&fixes.diagnostics
|
||||
)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert!(
|
||||
!parsed.has_syntax_errors() || had_syntax_errors,
|
||||
"Fixed introduced syntax errors\n\n{output}"
|
||||
);
|
||||
|
||||
let new_diagnostics =
|
||||
diff_diagnostics(&fixes.diagnostics, &diagnostics_after_applying_fixes);
|
||||
|
||||
if !new_diagnostics.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"## New diagnostics after re-checking file\n\n{diagnostics}\n",
|
||||
diagnostics = DisplayDiagnostics::new(
|
||||
&db,
|
||||
&DisplayDiagnosticConfig::default(),
|
||||
&new_diagnostics
|
||||
)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn diff_diagnostics<'a>(before: &'a [Diagnostic], after: &'a [Diagnostic]) -> Vec<Diagnostic> {
|
||||
let before = DiagnosticFingerprint::group_diagnostics(before);
|
||||
let after = DiagnosticFingerprint::group_diagnostics(after);
|
||||
|
||||
after
|
||||
.into_iter()
|
||||
.filter(|(key, _)| !before.contains_key(key))
|
||||
.map(|(_, diagnostic)| diagnostic.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
struct DiagnosticFingerprint(u64);
|
||||
|
||||
impl DiagnosticFingerprint {
|
||||
fn group_diagnostics(diagnostics: &[Diagnostic]) -> FxHashMap<Self, &Diagnostic> {
|
||||
let mut result = FxHashMap::default();
|
||||
|
||||
for diagnostic in diagnostics {
|
||||
Self::from_diagnostic(diagnostic, &mut result);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn from_diagnostic<'a>(
|
||||
diagnostic: &'a Diagnostic,
|
||||
seen: &mut FxHashMap<DiagnosticFingerprint, &'a Diagnostic>,
|
||||
) -> DiagnosticFingerprint {
|
||||
let mut disambiguator = 0u64;
|
||||
|
||||
loop {
|
||||
let mut h = DefaultHasher::default();
|
||||
disambiguator.hash(&mut h);
|
||||
|
||||
diagnostic.id().hash(&mut h);
|
||||
|
||||
let key = DiagnosticFingerprint(h.finish());
|
||||
match seen.entry(key) {
|
||||
Entry::Occupied(_) => {
|
||||
disambiguator += 1;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(diagnostic);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use ruff_db::system::SystemPath;
|
||||
|
||||
use crate::glob::include::MatchFile;
|
||||
pub(crate) use exclude::{ExcludeFilter, ExcludeFilterBuilder};
|
||||
pub(crate) use include::{IncludeFilter, IncludeFilterBuilder};
|
||||
pub(crate) use portable::{
|
||||
@@ -39,7 +40,9 @@ impl IncludeExcludeFilter {
|
||||
if self.exclude.match_directory(path, mode) {
|
||||
IncludeResult::Excluded
|
||||
} else if self.include.match_directory(path) {
|
||||
IncludeResult::Included
|
||||
IncludeResult::Included {
|
||||
literal_match: None,
|
||||
}
|
||||
} else {
|
||||
IncludeResult::NotIncluded
|
||||
}
|
||||
@@ -52,10 +55,16 @@ impl IncludeExcludeFilter {
|
||||
) -> IncludeResult {
|
||||
if self.exclude.match_file(path, mode) {
|
||||
IncludeResult::Excluded
|
||||
} else if self.include.match_file(path) {
|
||||
IncludeResult::Included
|
||||
} else {
|
||||
IncludeResult::NotIncluded
|
||||
match self.include.match_file(path) {
|
||||
MatchFile::Literal => IncludeResult::Included {
|
||||
literal_match: Some(true),
|
||||
},
|
||||
MatchFile::Pattern => IncludeResult::Included {
|
||||
literal_match: Some(false),
|
||||
},
|
||||
MatchFile::No => IncludeResult::NotIncluded,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +95,7 @@ pub(crate) enum IncludeResult {
|
||||
///
|
||||
/// For directories: This isn't a guarantee that any file in this directory gets included
|
||||
/// but we need to traverse it to make this decision.
|
||||
Included,
|
||||
Included { literal_match: Option<bool> },
|
||||
|
||||
/// The path matches an exclude pattern.
|
||||
Excluded,
|
||||
|
||||
@@ -33,7 +33,8 @@ const DFA_SIZE_LIMIT: usize = 1_000_000;
|
||||
pub(crate) struct IncludeFilter {
|
||||
#[get_size(ignore)]
|
||||
glob_set: GlobSet,
|
||||
original_patterns: Box<[String]>,
|
||||
original_patterns: Box<[Box<str>]>,
|
||||
literal_pattern_indices: Box<[usize]>,
|
||||
#[get_size(size_fn = dfa_memory_usage)]
|
||||
dfa: Option<dfa::dense::DFA<Vec<u32>>>,
|
||||
}
|
||||
@@ -45,10 +46,29 @@ fn dfa_memory_usage(dfa: &Option<dfa::dense::DFA<Vec<u32>>>) -> usize {
|
||||
|
||||
impl IncludeFilter {
|
||||
/// Whether the file matches any of the globs.
|
||||
pub(crate) fn match_file(&self, path: impl AsRef<SystemPath>) -> bool {
|
||||
pub(crate) fn match_file(&self, path: impl AsRef<SystemPath>) -> MatchFile {
|
||||
let path = path.as_ref();
|
||||
|
||||
self.glob_set.is_match(path)
|
||||
if self.literal_pattern_indices.is_empty() {
|
||||
return if self.glob_set.is_match(path) {
|
||||
MatchFile::Pattern
|
||||
} else {
|
||||
MatchFile::No
|
||||
};
|
||||
}
|
||||
|
||||
let matches = self.glob_set.matches(path);
|
||||
|
||||
if matches.is_empty() {
|
||||
MatchFile::No
|
||||
} else {
|
||||
for match_index in matches {
|
||||
if self.literal_pattern_indices.contains(&match_index) {
|
||||
return MatchFile::Literal;
|
||||
}
|
||||
}
|
||||
MatchFile::Pattern
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a directory or any of its children can be matched by any of the globs.
|
||||
@@ -120,18 +140,36 @@ impl PartialEq for IncludeFilter {
|
||||
|
||||
impl Eq for IncludeFilter {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum MatchFile {
|
||||
No,
|
||||
/// The file path matches the glob literally exactly. This is only the case for globs
|
||||
/// that don't use any wildcards.
|
||||
Literal,
|
||||
|
||||
/// The file path matches the glob pattern.
|
||||
Pattern,
|
||||
}
|
||||
|
||||
impl MatchFile {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct IncludeFilterBuilder {
|
||||
set: GlobSetBuilder,
|
||||
original_pattern: Vec<String>,
|
||||
set_len: usize,
|
||||
original_patterns: Vec<Box<str>>,
|
||||
regexes: Vec<String>,
|
||||
/// Indices of literal patterns (contain no meta characters).
|
||||
literal_pattern_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
impl IncludeFilterBuilder {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
literal_pattern_indices: Vec::new(),
|
||||
set: GlobSetBuilder::new(),
|
||||
original_pattern: Vec::new(),
|
||||
set_len: 0,
|
||||
original_patterns: Vec::new(),
|
||||
regexes: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -161,13 +199,16 @@ impl IncludeFilterBuilder {
|
||||
// No need to support Windows-style paths, so the backslash can be used a escape.
|
||||
.backslash_escape(true)
|
||||
.build()?;
|
||||
self.original_pattern.push(input.relative().to_string());
|
||||
|
||||
let is_literal_pattern = globset::escape(glob_pattern) == glob_pattern;
|
||||
|
||||
self.original_patterns.push(input.relative().into());
|
||||
|
||||
// `lib` is the same as `lib/**`
|
||||
// Add a glob that matches `lib` exactly, change the glob to `lib/**`.
|
||||
if glob_pattern.ends_with("**") {
|
||||
self.push_prefix_regex(&glob);
|
||||
self.set.add(glob);
|
||||
self.add_glob(glob);
|
||||
} else {
|
||||
let prefix_glob = GlobBuilder::new(&format!("{glob_pattern}/**"))
|
||||
.literal_separator(true)
|
||||
@@ -176,19 +217,28 @@ impl IncludeFilterBuilder {
|
||||
.build()?;
|
||||
|
||||
self.push_prefix_regex(&prefix_glob);
|
||||
self.set.add(prefix_glob);
|
||||
self.add_glob(prefix_glob);
|
||||
|
||||
// The reason we add the exact glob, e.g. `src` when the original pattern was `src/` is
|
||||
// so that `match_file` returns true when matching against a file. However, we don't
|
||||
// need to do this if this is a pattern that should only match a directory (specifically, its contents).
|
||||
if !only_directory {
|
||||
self.set.add(glob);
|
||||
if is_literal_pattern {
|
||||
self.literal_pattern_indices.push(self.set_len);
|
||||
}
|
||||
|
||||
self.add_glob(glob);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn add_glob(&mut self, glob: Glob) {
|
||||
self.set.add(glob);
|
||||
self.set_len += 1;
|
||||
}
|
||||
|
||||
fn push_prefix_regex(&mut self, glob: &Glob) {
|
||||
let main_separator = regex::escape(MAIN_SEPARATOR_STR);
|
||||
|
||||
@@ -239,7 +289,8 @@ impl IncludeFilterBuilder {
|
||||
Ok(IncludeFilter {
|
||||
glob_set,
|
||||
dfa,
|
||||
original_patterns: self.original_pattern.into(),
|
||||
literal_pattern_indices: self.literal_pattern_indices.into(),
|
||||
original_patterns: self.original_patterns.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -248,7 +299,7 @@ impl IncludeFilterBuilder {
|
||||
mod tests {
|
||||
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
|
||||
|
||||
use crate::glob::include::{IncludeFilter, IncludeFilterBuilder};
|
||||
use crate::glob::include::{IncludeFilter, IncludeFilterBuilder, MatchFile};
|
||||
use crate::glob::{PortableGlobKind, PortableGlobPattern};
|
||||
use ruff_db::system::{MemoryFileSystem, walk_directory::WalkState};
|
||||
|
||||
@@ -326,33 +377,33 @@ mod tests {
|
||||
"files/*.py",
|
||||
]);
|
||||
|
||||
assert!(filter.match_file("lib"));
|
||||
assert!(filter.match_file("lib/more/test"));
|
||||
assert_eq!(filter.match_file("lib"), MatchFile::Literal);
|
||||
assert_eq!(filter.match_file("lib/more/test"), MatchFile::Pattern);
|
||||
|
||||
// Unlike `directory`, `directory/` only includes a directory with the given name and its contents
|
||||
assert!(!filter.match_file("directory"));
|
||||
assert!(filter.match_file("directory/more/test"));
|
||||
assert_eq!(filter.match_file("directory"), MatchFile::No);
|
||||
assert_eq!(filter.match_file("directory/more/test"), MatchFile::Pattern);
|
||||
|
||||
// Unlike `src`, `src/*` only includes a directory with the given name.
|
||||
assert!(!filter.match_file("src"));
|
||||
assert!(filter.match_file("src/more/test"));
|
||||
assert_eq!(filter.match_file("src"), MatchFile::No);
|
||||
assert_eq!(filter.match_file("src/more/test"), MatchFile::Pattern);
|
||||
|
||||
// Unlike `tests`, `tests/**` only includes files under `tests`, but not a file named tests
|
||||
assert!(!filter.match_file("tests"));
|
||||
assert!(filter.match_file("tests/more/test"));
|
||||
assert_eq!(filter.match_file("tests"), MatchFile::No);
|
||||
assert_eq!(filter.match_file("tests/more/test"), MatchFile::Pattern);
|
||||
|
||||
// Unlike `match_directory`, prefixes should not be included.
|
||||
assert!(!filter.match_file("a"));
|
||||
assert!(!filter.match_file("a/test-b"));
|
||||
assert_eq!(filter.match_file("a"), MatchFile::No);
|
||||
assert_eq!(filter.match_file("a/test-b"), MatchFile::No);
|
||||
|
||||
assert!(!filter.match_file("a/test-b/x"));
|
||||
assert!(!filter.match_file("a/test"));
|
||||
assert_eq!(filter.match_file("a/test-b/x"), MatchFile::No);
|
||||
assert_eq!(filter.match_file("a/test"), MatchFile::No);
|
||||
|
||||
assert!(filter.match_file("files/a.py"));
|
||||
assert!(filter.match_file("files/a.py/bcd"));
|
||||
assert_eq!(filter.match_file("files/a.py"), MatchFile::Pattern);
|
||||
assert_eq!(filter.match_file("files/a.py/bcd"), MatchFile::Pattern);
|
||||
|
||||
assert!(!filter.match_file("not_included"));
|
||||
assert!(!filter.match_file("files/a.pi"));
|
||||
assert_eq!(filter.match_file("not_included"), MatchFile::No);
|
||||
assert_eq!(filter.match_file("files/a.pi"), MatchFile::No);
|
||||
}
|
||||
|
||||
/// Check that we skip directories that can never match.
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
|
||||
pub use db::tests::TestDb;
|
||||
pub use db::{ChangeResult, CheckMode, Db, ProjectDatabase, SalsaMemoryDump};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
pub use fixes::suppress_all_diagnostics;
|
||||
use metadata::settings::Settings;
|
||||
pub use metadata::{ProjectMetadata, ProjectMetadataError};
|
||||
use ruff_db::diagnostic::{
|
||||
@@ -33,6 +34,7 @@ use ty_python_semantic::types::check_types;
|
||||
|
||||
mod db;
|
||||
mod files;
|
||||
mod fixes;
|
||||
mod glob;
|
||||
pub mod metadata;
|
||||
mod walk;
|
||||
@@ -214,15 +216,19 @@ impl Project {
|
||||
/// This means, that this method is an over-approximation of `Self::files` and may return `true` for paths
|
||||
/// that won't be included when checking the project because they're ignored in a `.gitignore` file.
|
||||
pub fn is_file_included(self, db: &dyn Db, path: &SystemPath) -> bool {
|
||||
ProjectFilesFilter::from_project(db, self)
|
||||
.is_file_included(path, GlobFilterCheckMode::Adhoc)
|
||||
== IncludeResult::Included
|
||||
matches!(
|
||||
ProjectFilesFilter::from_project(db, self)
|
||||
.is_file_included(path, GlobFilterCheckMode::Adhoc),
|
||||
IncludeResult::Included { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_directory_included(self, db: &dyn Db, path: &SystemPath) -> bool {
|
||||
ProjectFilesFilter::from_project(db, self)
|
||||
.is_directory_included(path, GlobFilterCheckMode::Adhoc)
|
||||
== IncludeResult::Included
|
||||
matches!(
|
||||
ProjectFilesFilter::from_project(db, self)
|
||||
.is_directory_included(path, GlobFilterCheckMode::Adhoc),
|
||||
IncludeResult::Included { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {
|
||||
@@ -694,38 +700,7 @@ where
|
||||
Err(error) => {
|
||||
let message = error.to_diagnostic_message(Some(file.path(db)));
|
||||
let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message);
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"This indicates a bug in ty.",
|
||||
));
|
||||
|
||||
let report_message = "If you could open an issue at https://github.com/astral-sh/ty/issues/new?title=%5Bpanic%5D, we'd be very appreciative!";
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
report_message,
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format!(
|
||||
"Platform: {os} {arch}",
|
||||
os = std::env::consts::OS,
|
||||
arch = std::env::consts::ARCH
|
||||
),
|
||||
));
|
||||
if let Some(version) = ruff_db::program_version() {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format!("Version: {version}"),
|
||||
));
|
||||
}
|
||||
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format!(
|
||||
"Args: {args:?}",
|
||||
args = std::env::args().collect::<Vec<_>>()
|
||||
),
|
||||
));
|
||||
diagnostic.add_bug_sub_diagnostics("%5Bpanic%5D");
|
||||
|
||||
if let Some(backtrace) = error.backtrace {
|
||||
match backtrace.status() {
|
||||
|
||||
@@ -850,7 +850,6 @@ impl SrcOptions {
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case", transparent)]
|
||||
pub struct Rules {
|
||||
#[get_size(ignore)] // TODO: Add `GetSize` support for `OrderMap`.
|
||||
inner: OrderMap<RangedValue<String>, RangedValue<Level>, BuildHasherDefault<FxHasher>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ impl Override {
|
||||
matches!(
|
||||
self.files
|
||||
.is_file_included(path, GlobFilterCheckMode::Adhoc),
|
||||
IncludeResult::Included
|
||||
IncludeResult::Included { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,9 @@ impl<'a> ProjectFilesFilter<'a> {
|
||||
match self.match_included_paths(path, mode) {
|
||||
None => IncludeResult::NotIncluded,
|
||||
Some(CheckPathMatch::Partial) => self.src_filter.is_file_included(path, mode),
|
||||
Some(CheckPathMatch::Full) => IncludeResult::Included,
|
||||
Some(CheckPathMatch::Full) => IncludeResult::Included {
|
||||
literal_match: Some(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +95,9 @@ impl<'a> ProjectFilesFilter<'a> {
|
||||
Some(CheckPathMatch::Partial) => {
|
||||
self.src_filter.is_directory_maybe_included(path, mode)
|
||||
}
|
||||
Some(CheckPathMatch::Full) => IncludeResult::Included,
|
||||
Some(CheckPathMatch::Full) => IncludeResult::Included {
|
||||
literal_match: Some(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,60 +193,59 @@ impl<'a> ProjectFilesWalker<'a> {
|
||||
let directory_included = filter
|
||||
.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown);
|
||||
return match directory_included {
|
||||
IncludeResult::Included => WalkState::Continue,
|
||||
IncludeResult::Included { .. } => WalkState::Continue,
|
||||
IncludeResult::Excluded => {
|
||||
tracing::debug!(
|
||||
"Skipping directory '{path}' because it is excluded by a default or `src.exclude` pattern",
|
||||
path=entry.path()
|
||||
);
|
||||
WalkState::Skip
|
||||
},
|
||||
}
|
||||
IncludeResult::NotIncluded => {
|
||||
tracing::debug!(
|
||||
"Skipping directory `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI",
|
||||
path=entry.path()
|
||||
);
|
||||
WalkState::Skip
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Ignore any non python files to avoid creating too many entries in `Files`.
|
||||
// Unless the file is explicitly passed, we then always assume it's a python file.
|
||||
let source_type = entry.path().extension().and_then(PySourceType::try_from_extension).or_else(|| {
|
||||
if entry.depth() == 0 {
|
||||
Some(PySourceType::Python)
|
||||
} else {
|
||||
db.system().source_type(entry.path())
|
||||
}
|
||||
});
|
||||
|
||||
if source_type.is_none()
|
||||
{
|
||||
return WalkState::Continue;
|
||||
}
|
||||
|
||||
// For all files, except the ones that were explicitly passed to the walker (CLI),
|
||||
// check if they're included in the project.
|
||||
if entry.depth() > 0 || self.force_exclude {
|
||||
match filter
|
||||
.is_file_included(entry.path(), GlobFilterCheckMode::TopDown)
|
||||
{
|
||||
IncludeResult::Included => {},
|
||||
IncludeResult::Included { literal_match } => {
|
||||
// Ignore any non python files to avoid creating too many entries in `Files`.
|
||||
// Unless the file is explicitly passed on the CLI or a literal match in the `include`, we then always assume it's a file ty can analyze
|
||||
let source_type = if literal_match == Some(true) || entry.depth() == 0 {
|
||||
Some(PySourceType::Python)
|
||||
} else {
|
||||
entry.path().extension().and_then(PySourceType::try_from_extension).or_else(|| db.system().source_type(entry.path()))
|
||||
};
|
||||
|
||||
|
||||
if source_type.is_none()
|
||||
{
|
||||
return WalkState::Continue;
|
||||
}
|
||||
}
|
||||
IncludeResult::Excluded => {
|
||||
tracing::debug!(
|
||||
"Ignoring file `{path}` because it is excluded by a default or `src.exclude` pattern.",
|
||||
path=entry.path()
|
||||
);
|
||||
return WalkState::Continue;
|
||||
},
|
||||
}
|
||||
IncludeResult::NotIncluded => {
|
||||
tracing::debug!(
|
||||
"Ignoring file `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI.",
|
||||
path=entry.path()
|
||||
);
|
||||
return WalkState::Continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,56 @@ on top of that:
|
||||
Foo = NewType("Foo", 42)
|
||||
```
|
||||
|
||||
## `NewType`s in arithmetic and comparison expressions might or might not act as their base
|
||||
|
||||
These expressions are valid because `Foo` acts as its base type, `int`:
|
||||
|
||||
```py
|
||||
from typing import NewType
|
||||
|
||||
Foo = NewType("Foo", int)
|
||||
|
||||
reveal_type(Foo(42) + 1) # revealed: int
|
||||
reveal_type(1 + Foo(42)) # revealed: int
|
||||
reveal_type(Foo(42) + Foo(42)) # revealed: int
|
||||
reveal_type(Foo(42) == 42) # revealed: bool
|
||||
reveal_type(42 == Foo(42)) # revealed: bool
|
||||
reveal_type(Foo(42) == Foo(42)) # revealed: bool
|
||||
```
|
||||
|
||||
However, we can't always substitute `int` for `Foo` to evaluate expressions like these. In the
|
||||
following cases, only `Foo` itself is valid:
|
||||
|
||||
```py
|
||||
class Bar:
|
||||
def __add__(self, other: Foo) -> Foo:
|
||||
return other
|
||||
|
||||
def __radd__(self, other: Foo) -> Foo:
|
||||
return other
|
||||
|
||||
def __lt__(self, other: Foo) -> bool:
|
||||
return True
|
||||
|
||||
def __gt__(self, other: Foo) -> bool:
|
||||
return True
|
||||
|
||||
def __contains__(self, key: Foo) -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(Foo(42) + Bar()) # revealed: Foo
|
||||
reveal_type(Bar() + Foo(42)) # revealed: Foo
|
||||
reveal_type(Foo(42) < Bar()) # revealed: bool
|
||||
reveal_type(Bar() < Foo(42)) # revealed: bool
|
||||
reveal_type(Foo(42) in Bar()) # revealed: bool
|
||||
|
||||
42 + Bar() # error: [unsupported-operator]
|
||||
Bar() + 42 # error: [unsupported-operator]
|
||||
42 < Bar() # error: [unsupported-operator]
|
||||
Bar() < 42 # error: [unsupported-operator]
|
||||
42 in Bar() # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
## `float` and `complex` special cases
|
||||
|
||||
`float` and `complex` are subject to a special case in the typing spec, which we currently interpret
|
||||
@@ -178,6 +228,7 @@ and we accept the unions they expand into.
|
||||
|
||||
```py
|
||||
from typing import NewType
|
||||
from ty_extensions import static_assert, is_assignable_to
|
||||
|
||||
Foo = NewType("Foo", float)
|
||||
Foo(3.14)
|
||||
@@ -186,6 +237,15 @@ Foo("hello") # error: [invalid-argument-type] "Argument is incorrect: Expected
|
||||
|
||||
reveal_type(Foo(3.14).__class__) # revealed: type[int] | type[float]
|
||||
reveal_type(Foo(42).__class__) # revealed: type[int] | type[float]
|
||||
static_assert(is_assignable_to(Foo, float))
|
||||
static_assert(is_assignable_to(Foo, int | float))
|
||||
static_assert(is_assignable_to(Foo, int | float | None))
|
||||
# The assignments above require treating `Foo` as its underlying union type. Each of its members is
|
||||
# assignable to the union on the right, so `Foo` is assignable to the union, even though `Foo` as a
|
||||
# whole isn't assignable to any one member. However, as in the previous section, we need to be sure
|
||||
# that this treatment doesn't break cases like the assignment below, where `Foo` *is* assignable to
|
||||
# the union on the right, even though its members *aren't*.
|
||||
static_assert(is_assignable_to(Foo, Foo | None))
|
||||
|
||||
Bar = NewType("Bar", complex)
|
||||
Bar(1 + 2j)
|
||||
@@ -196,6 +256,11 @@ Bar("goodbye") # error: [invalid-argument-type]
|
||||
reveal_type(Bar(1 + 2j).__class__) # revealed: type[int] | type[float] | type[complex]
|
||||
reveal_type(Bar(3.14).__class__) # revealed: type[int] | type[float] | type[complex]
|
||||
reveal_type(Bar(42).__class__) # revealed: type[int] | type[float] | type[complex]
|
||||
static_assert(is_assignable_to(Bar, complex))
|
||||
static_assert(is_assignable_to(Bar, int | float | complex))
|
||||
static_assert(is_assignable_to(Bar, int | float | complex | None))
|
||||
# See the `Foo | None` case above.
|
||||
static_assert(is_assignable_to(Bar, Bar | None))
|
||||
```
|
||||
|
||||
We don't currently try to distinguish between an implicit union (e.g. `float`) and the equivalent
|
||||
@@ -223,6 +288,52 @@ def g(_: Callable[[int | float | complex], Bar]): ...
|
||||
g(Bar)
|
||||
```
|
||||
|
||||
The arithmetic and comparison test cases in the previous section used a `NewType` of `int`, but
|
||||
`NewType`s of `float` and `complex` are more complicated, because their base type is a union, and
|
||||
that union needs special handling in binary expressions. In these examples, we we need to lower
|
||||
`Foo` to `int | float` and then check each member of that union _individually_, as we would with an
|
||||
explicit `Union` on the left side:
|
||||
|
||||
```py
|
||||
reveal_type(Foo(3.14) < Foo(42)) # revealed: bool
|
||||
reveal_type(Foo(3.14) == Foo(42)) # revealed: bool
|
||||
reveal_type(Foo(3.14) + Foo(42)) # revealed: int | float
|
||||
reveal_type(Foo(3.14) / Foo(42)) # revealed: int | float
|
||||
```
|
||||
|
||||
But again as above, we can't _always_ lower `Foo` to `int | float`, because there are also binary
|
||||
expressions where only `Foo` itself is valid:
|
||||
|
||||
```py
|
||||
class Bing:
|
||||
def __add__(self, other: Foo) -> Foo:
|
||||
return other
|
||||
|
||||
def __radd__(self, other: Foo) -> Foo:
|
||||
return other
|
||||
|
||||
def __lt__(self, other: Foo) -> bool:
|
||||
return True
|
||||
|
||||
def __gt__(self, other: Foo) -> bool:
|
||||
return True
|
||||
|
||||
def __contains__(self, key: Foo) -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(Foo(3.14) + Bing()) # revealed: Foo
|
||||
reveal_type(Bing() + Foo(42)) # revealed: Foo
|
||||
reveal_type(Foo(3.14) < Bing()) # revealed: bool
|
||||
reveal_type(Bing() < Foo(42)) # revealed: bool
|
||||
reveal_type(Foo(3.14) in Bing()) # revealed: bool
|
||||
|
||||
3.14 + Bing() # error: [unsupported-operator]
|
||||
Bing() + 3.14 # error: [unsupported-operator]
|
||||
3.14 < Bing() # error: [unsupported-operator]
|
||||
Bing() < 3.14 # error: [unsupported-operator]
|
||||
3.14 in Bing() # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
## A `NewType` definition must be a simple variable assignment
|
||||
|
||||
```py
|
||||
|
||||
@@ -338,7 +338,111 @@ reveal_type(a is not c) # revealed: Literal[True]
|
||||
|
||||
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
|
||||
|
||||
// TODO
|
||||
### Unsupported Comparisons
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Comparisons between homogeneous tuples with incompatible element types should emit diagnostics for
|
||||
ordering operators (`<`, `<=`, `>`, `>=`), but not for equality operators (`==`, `!=`).
|
||||
|
||||
```py
|
||||
def f(
|
||||
a: tuple[int, ...],
|
||||
b: tuple[str, ...],
|
||||
c: tuple[str],
|
||||
):
|
||||
# Equality comparisons are always valid
|
||||
reveal_type(a == b) # revealed: bool
|
||||
reveal_type(a != b) # revealed: bool
|
||||
|
||||
# Ordering comparisons between incompatible types should emit errors
|
||||
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str, ...]`"
|
||||
a < b
|
||||
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str, ...]` and `tuple[int, ...]`"
|
||||
b < a
|
||||
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str]`"
|
||||
a < c
|
||||
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str]` and `tuple[int, ...]`"
|
||||
c < a
|
||||
```
|
||||
|
||||
When comparing fixed-length tuples with variable-length tuples, all element types that could
|
||||
potentially be compared must be compatible.
|
||||
|
||||
```py
|
||||
def _(
|
||||
var_int: tuple[int, ...],
|
||||
var_str: tuple[str, ...],
|
||||
fixed_int_str: tuple[int, str],
|
||||
):
|
||||
# Fixed `tuple[int, str]` vs. variable `tuple[int, ...]`:
|
||||
# Position 0: `int` vs. `int` are comparable.
|
||||
# Position 1 (if `var_int` has 2+ elements): `str` vs. `int` are not comparable.
|
||||
# error: [unsupported-operator]
|
||||
fixed_int_str < var_int
|
||||
|
||||
# Variable `tuple[int, ...]` vs. fixed `tuple[int, str]`:
|
||||
# Position 0: `int` vs. `int` are comparable.
|
||||
# Position 1 (if `var_int` has 2+ elements): `int` vs. `str` are not comparable.
|
||||
# error: [unsupported-operator]
|
||||
var_int < fixed_int_str
|
||||
|
||||
# Variable `tuple[str, ...]` vs. fixed `tuple[int, str]`:
|
||||
# Position 0: `str` vs. `int` are not comparable.
|
||||
# error: [unsupported-operator]
|
||||
var_str < fixed_int_str
|
||||
```
|
||||
|
||||
### Supported Comparisons
|
||||
|
||||
Comparisons between homogeneous tuples with compatible element types should work.
|
||||
|
||||
```py
|
||||
def _(a: tuple[int, ...], b: tuple[int, ...], c: tuple[bool, ...]):
|
||||
# Same element types - always valid
|
||||
reveal_type(a == b) # revealed: bool
|
||||
reveal_type(a != b) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
|
||||
# int and bool are compatible for comparison
|
||||
reveal_type(a < c) # revealed: bool
|
||||
reveal_type(c < a) # revealed: bool
|
||||
```
|
||||
|
||||
### Tuples with Prefixes and Suffixes
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Variable-length tuples with prefixes and suffixes are also checked.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
def _(
|
||||
prefix_int_var_str: tuple[int, *tuple[str, ...]],
|
||||
prefix_str_var_int: tuple[str, *tuple[int, ...]],
|
||||
):
|
||||
# Prefix `int` vs. prefix `str` are not comparable.
|
||||
# error: [unsupported-operator]
|
||||
prefix_int_var_str < prefix_str_var_int
|
||||
```
|
||||
|
||||
Tuples with compatible prefixes/suffixes are allowed.
|
||||
|
||||
```py
|
||||
def _(
|
||||
prefix_int_var_int: tuple[int, *tuple[int, ...]],
|
||||
prefix_int_var_bool: tuple[int, *tuple[bool, ...]],
|
||||
):
|
||||
# Prefix `int` vs. prefix `int`, variable `int` vs. variable `bool` are all comparable.
|
||||
reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: bool
|
||||
```
|
||||
|
||||
## Chained comparisons with elements that incorrectly implement `__bool__`
|
||||
|
||||
|
||||
@@ -583,7 +583,7 @@ from module import NotFrozenBase
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True)
|
||||
@total_ordering
|
||||
@total_ordering # error: [invalid-total-ordering]
|
||||
class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
|
||||
y: str
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user