Compare commits
3 Commits
0.12.5
...
brent/cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cabdd969ec | ||
|
|
2e5c8b9799 | ||
|
|
e1219bc27c |
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -143,12 +143,12 @@ jobs:
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
# NOTE: Do not exclude all Markdown files here, but rather use
|
||||
# specific exclude patterns like 'docs/**'), because tests for
|
||||
# 'ty' are written in Markdown.
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \
|
||||
':!**/*.md' \
|
||||
':crates/ty_python_semantic/resources/mdtest/**/*.md' \
|
||||
':!docs/**' \
|
||||
':!assets/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
|
||||
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,23 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
|
||||
- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
|
||||
- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
|
||||
- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
|
||||
- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
|
||||
|
||||
## 0.12.4
|
||||
|
||||
### Preview features
|
||||
|
||||
57
Cargo.lock
generated
57
Cargo.lock
generated
@@ -261,18 +261,6 @@ version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -1133,12 +1121,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -2566,12 +2548,6 @@ version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -2734,7 +2710,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2851,6 +2827,7 @@ dependencies = [
|
||||
"anstyle",
|
||||
"arc-swap",
|
||||
"camino",
|
||||
"countme",
|
||||
"dashmap",
|
||||
"dunce",
|
||||
"etcetera",
|
||||
@@ -2985,7 +2962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3317,7 +3294,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3791,12 +3768,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.20.0"
|
||||
@@ -4211,7 +4182,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_python_ast",
|
||||
@@ -4220,10 +4190,11 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"salsa",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
"ty_project",
|
||||
"ty_python_semantic",
|
||||
"ty_vendored",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4257,6 +4228,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.9.2",
|
||||
"tracing",
|
||||
"ty_ide",
|
||||
"ty_python_semantic",
|
||||
"ty_vendored",
|
||||
]
|
||||
@@ -4267,7 +4239,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitvec",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"compact_str",
|
||||
@@ -4304,7 +4275,6 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thin-vec",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"ty_python_semantic",
|
||||
@@ -4320,13 +4290,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"crossbeam",
|
||||
"dunce",
|
||||
"insta",
|
||||
"jod-thread",
|
||||
"libc",
|
||||
"lsp-server",
|
||||
"lsp-types",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
@@ -4337,7 +4304,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -5126,15 +5092,6 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
|
||||
@@ -57,9 +57,6 @@ assert_fs = { version = "1.1.0" }
|
||||
argfile = { version = "0.2.0" }
|
||||
bincode = { version = "2.0.0" }
|
||||
bitflags = { version = "2.5.0" }
|
||||
bitvec = { version = "1.0.1", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
bstr = { version = "1.9.1" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
camino = { version = "1.1.7" }
|
||||
@@ -166,7 +163,6 @@ strum_macros = { version = "0.27.0" }
|
||||
syn = { version = "2.0.55" }
|
||||
tempfile = { version = "3.9.0" }
|
||||
test-case = { version = "3.3.1" }
|
||||
thin-vec = { version = "0.2.14" }
|
||||
thiserror = { version = "2.0.0" }
|
||||
tikv-jemallocator = { version = "0.6.0" }
|
||||
toml = { version = "0.9.0" }
|
||||
|
||||
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.12.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.5/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.12.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.4/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.5
|
||||
rev: v0.12.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -18,14 +18,12 @@ use rustc_hash::FxHashMap;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use ruff_cache::{CacheKey, CacheKeyHasher};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Diagnostic, SerializableDiagnostics};
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_linter::message::create_lint_diagnostic;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::{VERSION, warn_user};
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::Settings;
|
||||
use ruff_workspace::resolver::Resolver;
|
||||
@@ -345,22 +343,7 @@ impl FileCache {
|
||||
let diagnostics = if lint.messages.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
|
||||
lint.messages
|
||||
.iter()
|
||||
.map(|msg| {
|
||||
create_lint_diagnostic(
|
||||
&msg.body,
|
||||
msg.suggestion.as_ref(),
|
||||
msg.range,
|
||||
msg.fix.clone(),
|
||||
msg.parent,
|
||||
file.clone(),
|
||||
msg.noqa_offset,
|
||||
msg.rule,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
lint.messages.to_diagnostics()
|
||||
};
|
||||
let notebook_indexes = if let Some(notebook_index) = lint.notebook_index.as_ref() {
|
||||
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
|
||||
@@ -415,7 +398,8 @@ pub(crate) struct LintCacheData {
|
||||
/// Imports made.
|
||||
// pub(super) imports: ImportMap,
|
||||
/// Diagnostic messages.
|
||||
pub(super) messages: Vec<CacheMessage>,
|
||||
#[bincode(with_serde)]
|
||||
pub(super) messages: SerializableDiagnostics,
|
||||
/// Source code of the file.
|
||||
///
|
||||
/// # Notes
|
||||
@@ -438,30 +422,7 @@ impl LintCacheData {
|
||||
String::new() // No messages, no need to keep the source!
|
||||
};
|
||||
|
||||
let messages = diagnostics
|
||||
.iter()
|
||||
// Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
|
||||
// this also serves to filter them out, but we shouldn't be caching files with syntax
|
||||
// errors anyway.
|
||||
.filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
|
||||
.map(|(rule, msg)| {
|
||||
// Make sure that all message use the same source file.
|
||||
assert_eq!(
|
||||
msg.expect_ruff_source_file(),
|
||||
diagnostics.first().unwrap().expect_ruff_source_file(),
|
||||
"message uses a different source file"
|
||||
);
|
||||
CacheMessage {
|
||||
rule,
|
||||
body: msg.body().to_string(),
|
||||
suggestion: msg.first_help_text().map(ToString::to_string),
|
||||
range: msg.expect_range(),
|
||||
parent: msg.parent(),
|
||||
fix: msg.fix().cloned(),
|
||||
noqa_offset: msg.noqa_offset(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let messages = SerializableDiagnostics::new(diagnostics);
|
||||
|
||||
Self {
|
||||
messages,
|
||||
|
||||
@@ -264,7 +264,6 @@ impl Printer {
|
||||
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
|
||||
.with_show_source(self.format == OutputFormat::Full)
|
||||
.with_unsafe_fixes(self.unsafe_fixes)
|
||||
.with_preview(preview)
|
||||
.emit(writer, &diagnostics.inner, &context)?;
|
||||
|
||||
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
|
||||
|
||||
@@ -25,6 +25,7 @@ ty_static = { workspace = true }
|
||||
anstyle = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
filetime = { workspace = true }
|
||||
@@ -58,11 +59,6 @@ tempfile = { workspace = true }
|
||||
cache = ["ruff_cache"]
|
||||
junit = ["dep:quick-junit"]
|
||||
os = ["ignore", "dep:etcetera"]
|
||||
serde = [
|
||||
"camino/serde1",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
"ruff_diagnostics/serde",
|
||||
]
|
||||
serde = ["camino/serde1", "dep:serde", "dep:serde_json", "ruff_diagnostics/serde"]
|
||||
# Exposes testing utilities.
|
||||
testing = ["tracing-subscriber"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{fmt::Formatter, path::Path, sync::Arc};
|
||||
|
||||
use ruff_diagnostics::{Applicability, Fix};
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_source_file::{LineColumn, SourceCode, SourceFile};
|
||||
|
||||
use ruff_annotate_snippets::Level as AnnotateLevel;
|
||||
@@ -9,6 +9,9 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
|
||||
use crate::{Db, files::File};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub use serde_diagnostics::SerializableDiagnostics;
|
||||
|
||||
mod render;
|
||||
mod stylesheet;
|
||||
|
||||
@@ -122,14 +125,7 @@ impl Diagnostic {
|
||||
/// directly. If callers want or need to avoid cloning the diagnostic
|
||||
/// message, then they can also pass a `DiagnosticMessage` directly.
|
||||
pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
|
||||
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message));
|
||||
}
|
||||
|
||||
/// Adds a "help" sub-diagnostic with the given message.
|
||||
///
|
||||
/// See the closely related [`Diagnostic::info`] method for more details.
|
||||
pub fn help<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
|
||||
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Help, message));
|
||||
self.sub(SubDiagnostic::new(Severity::Info, message));
|
||||
}
|
||||
|
||||
/// Adds a "sub" diagnostic to this diagnostic.
|
||||
@@ -384,15 +380,9 @@ impl Diagnostic {
|
||||
self.primary_message()
|
||||
}
|
||||
|
||||
/// Returns the message of the first sub-diagnostic with a `Help` severity.
|
||||
///
|
||||
/// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
|
||||
/// general this is not the guaranteed meaning of such a message.
|
||||
pub fn first_help_text(&self) -> Option<&str> {
|
||||
self.sub_diagnostics()
|
||||
.iter()
|
||||
.find(|sub| matches!(sub.inner.severity, SubDiagnosticSeverity::Help))
|
||||
.map(|sub| sub.inner.message.as_str())
|
||||
/// Returns the fix suggestion for the violation.
|
||||
pub fn suggestion(&self) -> Option<&str> {
|
||||
self.primary_annotation()?.get_message()
|
||||
}
|
||||
|
||||
/// Returns the URL for the rule documentation, if it exists.
|
||||
@@ -578,10 +568,7 @@ impl SubDiagnostic {
|
||||
/// Callers can pass anything that implements `std::fmt::Display`
|
||||
/// directly. If callers want or need to avoid cloning the diagnostic
|
||||
/// message, then they can also pass a `DiagnosticMessage` directly.
|
||||
pub fn new<'a>(
|
||||
severity: SubDiagnosticSeverity,
|
||||
message: impl IntoDiagnosticMessage + 'a,
|
||||
) -> SubDiagnostic {
|
||||
pub fn new<'a>(severity: Severity, message: impl IntoDiagnosticMessage + 'a) -> SubDiagnostic {
|
||||
let inner = Box::new(SubDiagnosticInner {
|
||||
severity,
|
||||
message: message.into_diagnostic_message(),
|
||||
@@ -659,7 +646,7 @@ impl SubDiagnostic {
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
struct SubDiagnosticInner {
|
||||
severity: SubDiagnosticSeverity,
|
||||
severity: Severity,
|
||||
message: DiagnosticMessage,
|
||||
annotations: Vec<Annotation>,
|
||||
}
|
||||
@@ -806,6 +793,7 @@ impl Annotation {
|
||||
/// These tags are used to provide additional information about the annotation.
|
||||
/// and are passed through to the language server protocol.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum DiagnosticTag {
|
||||
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
|
||||
Unnecessary,
|
||||
@@ -820,6 +808,7 @@ pub enum DiagnosticTag {
|
||||
///
|
||||
/// Rules use kebab case, e.g. `no-foo`.
|
||||
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct LintName(&'static str);
|
||||
|
||||
impl LintName {
|
||||
@@ -860,6 +849,7 @@ impl PartialEq<&str> for LintName {
|
||||
|
||||
/// Uniquely identifies the kind of a diagnostic.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum DiagnosticId {
|
||||
Panic,
|
||||
|
||||
@@ -1157,6 +1147,7 @@ impl From<crate::files::FileRange> for Span {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
@@ -1186,32 +1177,6 @@ impl Severity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`Severity`] but exclusively for sub-diagnostics.
|
||||
///
|
||||
/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
|
||||
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
|
||||
/// deleted and the two combined again.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
|
||||
pub enum SubDiagnosticSeverity {
|
||||
Help,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
impl SubDiagnosticSeverity {
|
||||
fn to_annotate(self) -> AnnotateLevel {
|
||||
match self {
|
||||
SubDiagnosticSeverity::Help => AnnotateLevel::Help,
|
||||
SubDiagnosticSeverity::Info => AnnotateLevel::Info,
|
||||
SubDiagnosticSeverity::Warning => AnnotateLevel::Warning,
|
||||
SubDiagnosticSeverity::Error => AnnotateLevel::Error,
|
||||
SubDiagnosticSeverity::Fatal => AnnotateLevel::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for rendering diagnostics.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DisplayDiagnosticConfig {
|
||||
@@ -1238,15 +1203,6 @@ pub struct DisplayDiagnosticConfig {
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
)]
|
||||
preview: bool,
|
||||
/// Whether to hide the real `Severity` of diagnostics.
|
||||
///
|
||||
/// This is intended for temporary use by Ruff, which only has a single `error` severity at the
|
||||
/// moment. We should be able to remove this option when Ruff gets more severities.
|
||||
hide_severity: bool,
|
||||
/// Whether to show the availability of a fix in a diagnostic.
|
||||
show_fix_status: bool,
|
||||
/// The lowest applicability that should be shown when reporting diagnostics.
|
||||
fix_applicability: Applicability,
|
||||
}
|
||||
|
||||
impl DisplayDiagnosticConfig {
|
||||
@@ -1275,35 +1231,6 @@ impl DisplayDiagnosticConfig {
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to hide a diagnostic's severity or not.
|
||||
pub fn hide_severity(self, yes: bool) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
hide_severity: yes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to show a fix's availability or not.
|
||||
pub fn show_fix_status(self, yes: bool) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
show_fix_status: yes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the lowest fix applicability that should be shown.
|
||||
///
|
||||
/// In other words, an applicability of `Safe` (the default) would suppress showing fixes or fix
|
||||
/// availability for unsafe or display-only fixes.
|
||||
///
|
||||
/// Note that this option is currently ignored when `hide_severity` is false.
|
||||
pub fn fix_applicability(self, applicability: Applicability) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
fix_applicability: applicability,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DisplayDiagnosticConfig {
|
||||
@@ -1313,9 +1240,6 @@ impl Default for DisplayDiagnosticConfig {
|
||||
color: false,
|
||||
context: 2,
|
||||
preview: false,
|
||||
hide_severity: false,
|
||||
show_fix_status: false,
|
||||
fix_applicability: Applicability::Safe,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1427,6 +1351,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
|
||||
/// a blanket trait implementation for `IntoDiagnosticMessage` for
|
||||
/// anything that implements `std::fmt::Display`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct DiagnosticMessage(Box<str>);
|
||||
|
||||
impl DiagnosticMessage {
|
||||
@@ -1490,7 +1415,11 @@ impl<T: std::fmt::Display> IntoDiagnosticMessage for T {
|
||||
///
|
||||
/// For Ruff rules this means the noqa code.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(transparent)
|
||||
)]
|
||||
pub struct SecondaryCode(String);
|
||||
|
||||
impl SecondaryCode {
|
||||
@@ -1535,3 +1464,205 @@ impl From<&SecondaryCode> for SecondaryCode {
|
||||
value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_diagnostics {
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_source_file::{SourceFile, SourceFileBuilder};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
Annotation, Diagnostic, DiagnosticId, DiagnosticInner, DiagnosticMessage, DiagnosticTag,
|
||||
LintName, SecondaryCode, Severity, Span, SubDiagnostic, SubDiagnosticInner, UnifiedFile,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SerializableDiagnostics {
|
||||
source_files: FxHashMap<String, String>,
|
||||
diagnostics: Vec<SerializableDiagnostic>,
|
||||
}
|
||||
|
||||
impl SerializableDiagnostics {
|
||||
pub fn new(diagnostics: &[Diagnostic]) -> Self {
|
||||
let mut source_files = FxHashMap::default();
|
||||
let mut serializable_diagnostics = Vec::with_capacity(diagnostics.len());
|
||||
for diagnostic in diagnostics {
|
||||
let mut subs = Vec::with_capacity(diagnostic.inner.subs.len());
|
||||
for sub in &diagnostic.inner.subs {
|
||||
subs.push(SerializableSubDiagnostic {
|
||||
severity: sub.inner.severity,
|
||||
message: sub.inner.message.clone(),
|
||||
annotations: serializable_annotations(
|
||||
&mut source_files,
|
||||
&sub.inner.annotations,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
serializable_diagnostics.push(SerializableDiagnostic {
|
||||
id: diagnostic.inner.id,
|
||||
severity: diagnostic.inner.severity,
|
||||
message: diagnostic.inner.message.clone(),
|
||||
annotations: serializable_annotations(
|
||||
&mut source_files,
|
||||
&diagnostic.inner.annotations,
|
||||
),
|
||||
subs,
|
||||
fix: diagnostic.inner.fix.clone(),
|
||||
parent: diagnostic.inner.parent,
|
||||
noqa_offset: diagnostic.inner.noqa_offset,
|
||||
secondary_code: diagnostic.inner.secondary_code.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
source_files,
|
||||
diagnostics: serializable_diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.diagnostics.is_empty()
|
||||
}
|
||||
|
||||
pub fn to_diagnostics(&self) -> Vec<Diagnostic> {
|
||||
let source_files: FxHashMap<&str, SourceFile> = self
|
||||
.source_files
|
||||
.iter()
|
||||
.map(|(name, contents)| {
|
||||
(
|
||||
name.as_str(),
|
||||
SourceFileBuilder::new(name.clone(), contents.clone()).finish(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
self.diagnostics
|
||||
.iter()
|
||||
.map(|diag| Diagnostic {
|
||||
inner: Arc::new(DiagnosticInner {
|
||||
id: diag.id,
|
||||
severity: diag.severity,
|
||||
message: diag.message.clone(),
|
||||
annotations: annotations(&source_files, &diag.annotations),
|
||||
subs: subdiagnostics(&source_files, &diag.subs),
|
||||
fix: diag.fix.clone(),
|
||||
parent: diag.parent,
|
||||
noqa_offset: diag.noqa_offset,
|
||||
secondary_code: diag.secondary_code.clone(),
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn serializable_annotations(
|
||||
source_files: &mut FxHashMap<String, String>,
|
||||
annotations: &[Annotation],
|
||||
) -> Vec<SerializableAnnotation> {
|
||||
let mut serializable_annotations = Vec::with_capacity(annotations.len());
|
||||
for annotation in annotations {
|
||||
let file = annotation.span.expect_ruff_file();
|
||||
source_files.insert(file.name().to_string(), file.source_text().to_string());
|
||||
serializable_annotations.push(SerializableAnnotation {
|
||||
span: SerializableSpan {
|
||||
name: file.name().to_string(),
|
||||
range: annotation.span.range,
|
||||
},
|
||||
message: annotation.message.clone(),
|
||||
is_primary: annotation.is_primary,
|
||||
tags: annotation.tags.clone(),
|
||||
});
|
||||
}
|
||||
serializable_annotations
|
||||
}
|
||||
|
||||
fn annotations(
|
||||
source_files: &FxHashMap<&str, SourceFile>,
|
||||
serializable_annotations: &[SerializableAnnotation],
|
||||
) -> Vec<Annotation> {
|
||||
serializable_annotations
|
||||
.iter()
|
||||
.map(|ann| {
|
||||
let span = Span {
|
||||
file: UnifiedFile::Ruff(
|
||||
source_files
|
||||
.get(ann.span.name.as_str())
|
||||
.expect("Expected source file in cache")
|
||||
.clone(),
|
||||
),
|
||||
range: ann.span.range,
|
||||
};
|
||||
Annotation {
|
||||
span,
|
||||
message: ann.message.clone(),
|
||||
is_primary: ann.is_primary,
|
||||
tags: ann.tags.clone(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn subdiagnostics(
|
||||
source_files: &FxHashMap<&str, SourceFile>,
|
||||
serializable_annotations: &[SerializableSubDiagnostic],
|
||||
) -> Vec<SubDiagnostic> {
|
||||
serializable_annotations
|
||||
.iter()
|
||||
.map(|sub| SubDiagnostic {
|
||||
inner: Box::new(SubDiagnosticInner {
|
||||
severity: sub.severity,
|
||||
message: sub.message.clone(),
|
||||
annotations: annotations(source_files, &sub.annotations),
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SerializableDiagnostic {
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
message: DiagnosticMessage,
|
||||
annotations: Vec<SerializableAnnotation>,
|
||||
subs: Vec<SerializableSubDiagnostic>,
|
||||
fix: Option<Fix>,
|
||||
parent: Option<TextSize>,
|
||||
noqa_offset: Option<TextSize>,
|
||||
secondary_code: Option<SecondaryCode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SerializableAnnotation {
|
||||
span: SerializableSpan,
|
||||
message: Option<DiagnosticMessage>,
|
||||
is_primary: bool,
|
||||
tags: Vec<DiagnosticTag>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SerializableSubDiagnostic {
|
||||
severity: Severity,
|
||||
message: DiagnosticMessage,
|
||||
annotations: Vec<SerializableAnnotation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
struct SerializableSpan {
|
||||
name: String,
|
||||
range: Option<TextRange>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for LintName {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?.into_boxed_str();
|
||||
Ok(LintName::of(Box::leak(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::diagnostic::stylesheet::DiagnosticStylesheet;
|
||||
use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled};
|
||||
use crate::{
|
||||
Db,
|
||||
files::File,
|
||||
@@ -18,17 +18,14 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig,
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
|
||||
SubDiagnostic, UnifiedFile,
|
||||
};
|
||||
|
||||
use azure::AzureRenderer;
|
||||
use concise::ConciseRenderer;
|
||||
use pylint::PylintRenderer;
|
||||
|
||||
mod azure;
|
||||
mod concise;
|
||||
mod full;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json;
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -107,7 +104,48 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.config.format {
|
||||
DiagnosticFormat::Concise => {
|
||||
ConciseRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
for diag in self.diagnostics {
|
||||
let (severity, severity_style) = match diag.severity() {
|
||||
Severity::Info => ("info", stylesheet.info),
|
||||
Severity::Warning => ("warning", stylesheet.warning),
|
||||
Severity::Error => ("error", stylesheet.error),
|
||||
Severity::Fatal => ("fatal", stylesheet.error),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{severity}[{id}]",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(span) = diag.primary_span() {
|
||||
write!(
|
||||
f,
|
||||
" {path}",
|
||||
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
write!(
|
||||
f,
|
||||
":{line}:{col}",
|
||||
line = fmt_styled(start.line, stylesheet.emphasis),
|
||||
col = fmt_styled(start.column, stylesheet.emphasis),
|
||||
)?;
|
||||
}
|
||||
write!(f, ":")?;
|
||||
}
|
||||
writeln!(f, " {message}", message = diag.concise_message())?;
|
||||
}
|
||||
}
|
||||
DiagnosticFormat::Full => {
|
||||
let stylesheet = if self.config.color {
|
||||
@@ -218,7 +256,7 @@ impl<'a> Resolved<'a> {
|
||||
/// both.)
|
||||
#[derive(Debug)]
|
||||
struct ResolvedDiagnostic<'a> {
|
||||
level: AnnotateLevel,
|
||||
severity: Severity,
|
||||
id: Option<String>,
|
||||
message: String,
|
||||
annotations: Vec<ResolvedAnnotation<'a>>,
|
||||
@@ -243,7 +281,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
let id = Some(diag.inner.id.to_string());
|
||||
let message = diag.inner.message.as_str().to_string();
|
||||
ResolvedDiagnostic {
|
||||
level: diag.inner.severity.to_annotate(),
|
||||
severity: diag.inner.severity,
|
||||
id,
|
||||
message,
|
||||
annotations,
|
||||
@@ -266,7 +304,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
})
|
||||
.collect();
|
||||
ResolvedDiagnostic {
|
||||
level: diag.inner.severity.to_annotate(),
|
||||
severity: diag.inner.severity,
|
||||
id: None,
|
||||
message: diag.inner.message.as_str().to_string(),
|
||||
annotations,
|
||||
@@ -333,7 +371,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
snippets_by_input
|
||||
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
|
||||
RenderableDiagnostic {
|
||||
level: self.level,
|
||||
severity: self.severity,
|
||||
id: self.id.as_deref(),
|
||||
message: &self.message,
|
||||
snippets_by_input,
|
||||
@@ -421,7 +459,7 @@ struct Renderable<'r> {
|
||||
#[derive(Debug)]
|
||||
struct RenderableDiagnostic<'r> {
|
||||
/// The severity of the diagnostic.
|
||||
level: AnnotateLevel,
|
||||
severity: Severity,
|
||||
/// The ID of the diagnostic. The ID can usually be used on the CLI or in a
|
||||
/// config file to change the severity of a lint.
|
||||
///
|
||||
@@ -440,6 +478,7 @@ struct RenderableDiagnostic<'r> {
|
||||
impl RenderableDiagnostic<'_> {
|
||||
/// Convert this to an "annotate" snippet.
|
||||
fn to_annotate(&self) -> AnnotateMessage<'_> {
|
||||
let level = self.severity.to_annotate();
|
||||
let snippets = self.snippets_by_input.iter().flat_map(|snippets| {
|
||||
let path = snippets.path;
|
||||
snippets
|
||||
@@ -447,7 +486,7 @@ impl RenderableDiagnostic<'_> {
|
||||
.iter()
|
||||
.map(|snippet| snippet.to_annotate(path))
|
||||
});
|
||||
let mut message = self.level.title(self.message);
|
||||
let mut message = level.title(self.message);
|
||||
if let Some(id) = self.id {
|
||||
message = message.id(id);
|
||||
}
|
||||
@@ -823,12 +862,9 @@ fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
|
||||
use crate::diagnostic::{
|
||||
Annotation, DiagnosticId, IntoDiagnosticMessage, SecondaryCode, Severity, Span,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use crate::diagnostic::{Annotation, DiagnosticId, SecondaryCode, Severity, Span};
|
||||
use crate::files::system_path_to_file;
|
||||
use crate::system::{DbWithWritableSystem, SystemPath};
|
||||
use crate::tests::TestDb;
|
||||
@@ -1512,7 +1548,7 @@ watermelon
|
||||
|
||||
let mut diag = env.err().primary("animals", "3", "3", "").build();
|
||||
diag.sub(
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
|
||||
env.sub_builder(Severity::Info, "this is a helpful note")
|
||||
.build(),
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
@@ -1541,15 +1577,15 @@ watermelon
|
||||
|
||||
let mut diag = env.err().primary("animals", "3", "3", "").build();
|
||||
diag.sub(
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
|
||||
env.sub_builder(Severity::Info, "this is a helpful note")
|
||||
.build(),
|
||||
);
|
||||
diag.sub(
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "another helpful note")
|
||||
env.sub_builder(Severity::Info, "another helpful note")
|
||||
.build(),
|
||||
);
|
||||
diag.sub(
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "and another helpful note")
|
||||
env.sub_builder(Severity::Info, "and another helpful note")
|
||||
.build(),
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
@@ -2271,27 +2307,6 @@ watermelon
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Hide diagnostic severity when rendering.
|
||||
pub(super) fn hide_severity(&mut self, yes: bool) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.hide_severity(yes);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Show fix availability when rendering.
|
||||
pub(super) fn show_fix_status(&mut self, yes: bool) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.show_fix_status(yes);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// The lowest fix applicability to show when rendering.
|
||||
pub(super) fn fix_applicability(&mut self, applicability: Applicability) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.fix_applicability(applicability);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Add a file with the given path and contents to this environment.
|
||||
pub(super) fn add(&mut self, path: &str, contents: &str) {
|
||||
let path = SystemPath::new(path);
|
||||
@@ -2355,7 +2370,7 @@ watermelon
|
||||
/// sub-diagnostic with "error" severity and canned values for
|
||||
/// its identifier and message.
|
||||
fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> {
|
||||
self.sub_builder(SubDiagnosticSeverity::Warning, "sub-diagnostic message")
|
||||
self.sub_builder(Severity::Warning, "sub-diagnostic message")
|
||||
}
|
||||
|
||||
/// Returns a builder for tersely constructing diagnostics.
|
||||
@@ -2376,11 +2391,7 @@ watermelon
|
||||
}
|
||||
|
||||
/// Returns a builder for tersely constructing sub-diagnostics.
|
||||
fn sub_builder(
|
||||
&mut self,
|
||||
severity: SubDiagnosticSeverity,
|
||||
message: &str,
|
||||
) -> SubDiagnosticBuilder<'_> {
|
||||
fn sub_builder(&mut self, severity: Severity, message: &str) -> SubDiagnosticBuilder<'_> {
|
||||
let subdiag = SubDiagnostic::new(severity, message);
|
||||
SubDiagnosticBuilder { env: self, subdiag }
|
||||
}
|
||||
@@ -2483,12 +2494,6 @@ watermelon
|
||||
self.diag.set_noqa_offset(noqa_offset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a "help" sub-diagnostic with the given message.
|
||||
fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
|
||||
self.diag.help(message);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper builder for tersely populating a `SubDiagnostic`.
|
||||
@@ -2595,8 +2600,7 @@ def fibonacci(n):
|
||||
|
||||
let diagnostics = vec![
|
||||
env.builder("unused-import", Severity::Error, "`os` imported but unused")
|
||||
.primary("fib.py", "1:7", "1:9", "")
|
||||
.help("Remove unused import: `os`")
|
||||
.primary("fib.py", "1:7", "1:9", "Remove unused import: `os`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(0),
|
||||
@@ -2609,8 +2613,12 @@ def fibonacci(n):
|
||||
Severity::Error,
|
||||
"Local variable `x` is assigned to but never used",
|
||||
)
|
||||
.primary("fib.py", "6:4", "6:5", "")
|
||||
.help("Remove assignment to unused variable `x`")
|
||||
.primary(
|
||||
"fib.py",
|
||||
"6:4",
|
||||
"6:5",
|
||||
"Remove assignment to unused variable `x`",
|
||||
)
|
||||
.secondary_code("F841")
|
||||
.fix(Fix::unsafe_edit(Edit::deletion(
|
||||
TextSize::from(94),
|
||||
@@ -2657,25 +2665,6 @@ if call(foo
|
||||
}
|
||||
|
||||
/// Create Ruff-style diagnostics for testing the various output formats for a notebook.
|
||||
///
|
||||
/// The concatenated cells look like this:
|
||||
///
|
||||
/// ```python
|
||||
/// # cell 1
|
||||
/// import os
|
||||
/// # cell 2
|
||||
/// import math
|
||||
///
|
||||
/// print('hello world')
|
||||
/// # cell 3
|
||||
/// def foo():
|
||||
/// print()
|
||||
/// x = 1
|
||||
/// ```
|
||||
///
|
||||
/// The first diagnostic is on the unused `os` import with location cell 1, row 2, column 8
|
||||
/// (`cell 1:2:8`). The second diagnostic is the unused `math` import at `cell 2:2:8`, and the
|
||||
/// third diagnostic is an unfixable unused variable at `cell 3:4:5`.
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
@@ -2731,8 +2720,7 @@ if call(foo
|
||||
|
||||
let diagnostics = vec![
|
||||
env.builder("unused-import", Severity::Error, "`os` imported but unused")
|
||||
.primary("notebook.ipynb", "2:7", "2:9", "")
|
||||
.help("Remove unused import: `os`")
|
||||
.primary("notebook.ipynb", "2:7", "2:9", "Remove unused import: `os`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(9),
|
||||
@@ -2745,8 +2733,12 @@ if call(foo
|
||||
Severity::Error,
|
||||
"`math` imported but unused",
|
||||
)
|
||||
.primary("notebook.ipynb", "4:7", "4:11", "")
|
||||
.help("Remove unused import: `math`")
|
||||
.primary(
|
||||
"notebook.ipynb",
|
||||
"4:7",
|
||||
"4:11",
|
||||
"Remove unused import: `math`",
|
||||
)
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(28),
|
||||
@@ -2759,8 +2751,12 @@ if call(foo
|
||||
Severity::Error,
|
||||
"Local variable `x` is assigned to but never used",
|
||||
)
|
||||
.primary("notebook.ipynb", "10:4", "10:5", "")
|
||||
.help("Remove assignment to unused variable `x`")
|
||||
.primary(
|
||||
"notebook.ipynb",
|
||||
"10:4",
|
||||
"10:5",
|
||||
"Remove assignment to unused variable `x`",
|
||||
)
|
||||
.secondary_code("F841")
|
||||
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(94),
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
use crate::diagnostic::{
|
||||
Diagnostic, DisplayDiagnosticConfig, Severity,
|
||||
stylesheet::{DiagnosticStylesheet, fmt_styled},
|
||||
};
|
||||
|
||||
use super::FileResolver;
|
||||
|
||||
pub(super) struct ConciseRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl<'a> ConciseRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig) -> Self {
|
||||
Self { resolver, config }
|
||||
}
|
||||
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
let sep = fmt_styled(":", stylesheet.separator);
|
||||
for diag in diagnostics {
|
||||
if let Some(span) = diag.primary_span() {
|
||||
write!(
|
||||
f,
|
||||
"{path}",
|
||||
path = fmt_styled(
|
||||
span.file().relative_path(self.resolver).to_string_lossy(),
|
||||
stylesheet.emphasis
|
||||
)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
if let Some(notebook_index) = self.resolver.notebook_index(span.file()) {
|
||||
write!(
|
||||
f,
|
||||
"{sep}cell {cell}{sep}{line}{sep}{col}",
|
||||
cell = notebook_index.cell(start.line).unwrap_or_default(),
|
||||
line = notebook_index.cell_row(start.line).unwrap_or_default(),
|
||||
col = start.column,
|
||||
)?;
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{sep}{line}{sep}{col}",
|
||||
line = start.line,
|
||||
col = start.column,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
write!(f, "{sep} ")?;
|
||||
}
|
||||
if self.config.hide_severity {
|
||||
if let Some(code) = diag.secondary_code() {
|
||||
write!(
|
||||
f,
|
||||
"{code} ",
|
||||
code = fmt_styled(code, stylesheet.secondary_code)
|
||||
)?;
|
||||
}
|
||||
if self.config.show_fix_status {
|
||||
if let Some(fix) = diag.fix() {
|
||||
// Do not display an indicator for inapplicable fixes
|
||||
if fix.applies(self.config.fix_applicability) {
|
||||
write!(f, "[{fix}] ", fix = fmt_styled("*", stylesheet.separator))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (severity, severity_style) = match diag.severity() {
|
||||
Severity::Info => ("info", stylesheet.info),
|
||||
Severity::Warning => ("warning", stylesheet.warning),
|
||||
Severity::Error => ("error", stylesheet.error),
|
||||
Severity::Fatal => ("fatal", stylesheet.error),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{severity}[{id}] ",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(f, "{message}", message = diag.concise_message())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_diagnostics::Applicability;
|
||||
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{
|
||||
TestEnvironment, create_diagnostics, create_notebook_diagnostics,
|
||||
create_syntax_error_diagnostics,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: error[unused-import] `os` imported but unused
|
||||
fib.py:6:5: error[unused-variable] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: error[undefined-name] Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes() {
|
||||
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: F401 [*] `os` imported but unused
|
||||
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes_preview() {
|
||||
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
env.preview(true);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: F401 [*] `os` imported but unused
|
||||
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes_syntax_errors() {
|
||||
let (mut env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
|
||||
syntax_errors.py:3:12: SyntaxError: Expected ')', found newline
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
syntax_errors.py:1:15: error[invalid-syntax] SyntaxError: Expected one or more symbol names after import
|
||||
syntax_errors.py:3:12: error[invalid-syntax] SyntaxError: Expected ')', found newline
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_output() {
|
||||
let (env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
notebook.ipynb:cell 1:2:8: error[unused-import] `os` imported but unused
|
||||
notebook.ipynb:cell 2:2:8: error[unused-import] `math` imported but unused
|
||||
notebook.ipynb:cell 3:4:5: error[unused-variable] Local variable `x` is assigned to but never used
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.format(DiagnosticFormat::Concise);
|
||||
|
||||
let diag = env.err().build();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@"error[test-diagnostic] main diagnostic message",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Full);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r#"
|
||||
error[unused-import]: `os` imported but unused
|
||||
--> fib.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
|
||||
error[unused-variable]: Local variable `x` is assigned to but never used
|
||||
--> fib.py:6:5
|
||||
|
|
||||
4 | def fibonacci(n):
|
||||
5 | """Compute the nth number in the Fibonacci sequence."""
|
||||
6 | x = 1
|
||||
| ^
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
help: Remove assignment to unused variable `x`
|
||||
|
||||
error[undefined-name]: Undefined name `a`
|
||||
--> undef.py:1:4
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
error[invalid-syntax]: SyntaxError: Expected one or more symbol names after import
|
||||
--> syntax_errors.py:1:15
|
||||
|
|
||||
1 | from os import
|
||||
| ^
|
||||
2 |
|
||||
3 | if call(foo
|
||||
|
|
||||
|
||||
error[invalid-syntax]: SyntaxError: Expected ')', found newline
|
||||
--> syntax_errors.py:3:12
|
||||
|
|
||||
1 | from os import
|
||||
2 |
|
||||
3 | if call(foo
|
||||
| ^
|
||||
4 | def bar():
|
||||
5 | pass
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ pub(super) fn diagnostic_to_json<'a>(
|
||||
|
||||
let fix = diagnostic.fix().map(|fix| JsonFix {
|
||||
applicability: fix.applicability(),
|
||||
message: diagnostic.first_help_text(),
|
||||
message: diagnostic.suggestion(),
|
||||
edits: ExpandedEdits {
|
||||
edits: fix.edits(),
|
||||
notebook_index,
|
||||
|
||||
@@ -41,8 +41,6 @@ pub struct DiagnosticStylesheet {
|
||||
pub(crate) line_no: Style,
|
||||
pub(crate) emphasis: Style,
|
||||
pub(crate) none: Style,
|
||||
pub(crate) separator: Style,
|
||||
pub(crate) secondary_code: Style,
|
||||
}
|
||||
|
||||
impl Default for DiagnosticStylesheet {
|
||||
@@ -64,8 +62,6 @@ impl DiagnosticStylesheet {
|
||||
line_no: bright_blue.effects(Effects::BOLD),
|
||||
emphasis: Style::new().effects(Effects::BOLD),
|
||||
none: Style::new(),
|
||||
separator: AnsiColor::Cyan.on_default(),
|
||||
secondary_code: AnsiColor::Red.on_default().effects(Effects::BOLD),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +75,6 @@ impl DiagnosticStylesheet {
|
||||
line_no: Style::new(),
|
||||
emphasis: Style::new(),
|
||||
none: Style::new(),
|
||||
separator: Style::new(),
|
||||
secondary_code: Style::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
use dashmap::mapref::entry::Entry;
|
||||
pub use file_root::{FileRoot, FileRootKind};
|
||||
pub use path::FilePath;
|
||||
@@ -311,6 +312,11 @@ pub struct File {
|
||||
/// the file has been deleted is to change the status to `Deleted`.
|
||||
#[default]
|
||||
status: FileStatus,
|
||||
|
||||
/// Counter that counts the number of created file instances and active file instances.
|
||||
/// Only enabled in debug builds.
|
||||
#[default]
|
||||
count: Count<File>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_source_file::LineIndex;
|
||||
@@ -36,7 +38,11 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
};
|
||||
|
||||
SourceText {
|
||||
inner: Arc::new(SourceTextInner { kind, read_error }),
|
||||
inner: Arc::new(SourceTextInner {
|
||||
kind,
|
||||
read_error,
|
||||
count: Count::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +125,8 @@ impl std::fmt::Debug for SourceText {
|
||||
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize)]
|
||||
struct SourceTextInner {
|
||||
#[get_size(ignore)]
|
||||
count: Count<SourceText>,
|
||||
kind: SourceTextKind,
|
||||
read_error: Option<SourceTextError>,
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ impl<'a> Resolver<'a> {
|
||||
match import {
|
||||
CollectedImport::Import(import) => {
|
||||
let module = resolve_module(self.db, &import)?;
|
||||
Some(module.file(self.db)?.path(self.db))
|
||||
Some(module.file()?.path(self.db))
|
||||
}
|
||||
CollectedImport::ImportFrom(import) => {
|
||||
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
|
||||
@@ -32,7 +32,7 @@ impl<'a> Resolver<'a> {
|
||||
resolve_module(self.db, &parent?)
|
||||
})?;
|
||||
|
||||
Some(module.file(self.db)?.path(self.db))
|
||||
Some(module.file()?.path(self.db))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -142,7 +142,3 @@ field47: typing.Optional[int] | typing.Optional[dict]
|
||||
# avoid reporting twice
|
||||
field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
field49: typing.Optional[complex | complex] | complex
|
||||
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
# Should throw duplicate union member but not fix
|
||||
isinstance(None, typing.Union[None, None])
|
||||
@@ -47,19 +47,3 @@ def _():
|
||||
from builtin import open
|
||||
|
||||
with open(p) as _: ... # No error
|
||||
|
||||
file = "file_1.py"
|
||||
|
||||
rename(file, "file_2.py")
|
||||
|
||||
rename(
|
||||
# commment 1
|
||||
file, # comment 2
|
||||
"file_2.py"
|
||||
,
|
||||
# comment 3
|
||||
)
|
||||
|
||||
rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
|
||||
rename(file, "file_2.py", src_dir_fd=1)
|
||||
@@ -84,25 +84,3 @@ class MyRequestHandler(BaseHTTPRequestHandler):
|
||||
def dont_GET(self):
|
||||
pass
|
||||
|
||||
|
||||
from http.server import CGIHTTPRequestHandler
|
||||
|
||||
|
||||
class MyCGIRequestHandler(CGIHTTPRequestHandler):
|
||||
def do_OPTIONS(self):
|
||||
pass
|
||||
|
||||
def dont_OPTIONS(self):
|
||||
pass
|
||||
|
||||
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
|
||||
|
||||
class MySimpleRequestHandler(SimpleHTTPRequestHandler):
|
||||
def do_OPTIONS(self):
|
||||
pass
|
||||
|
||||
def dont_OPTIONS(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -278,15 +278,3 @@ def f():
|
||||
for i in src:
|
||||
if lambda: 0:
|
||||
dst.append(i)
|
||||
|
||||
def f():
|
||||
i = "xyz"
|
||||
result = []
|
||||
for i in range(3):
|
||||
result.append(x for x in [i])
|
||||
|
||||
def f():
|
||||
i = "xyz"
|
||||
result = []
|
||||
for i in range(3):
|
||||
result.append((x for x in [i]))
|
||||
@@ -1,5 +0,0 @@
|
||||
#
|
||||
x = 0 \
|
||||
#
|
||||
+1
|
||||
print(x)
|
||||
@@ -1039,10 +1039,14 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
|
||||
}
|
||||
if checker.any_rule_enabled(&[
|
||||
Rule::OsChmod,
|
||||
Rule::OsMkdir,
|
||||
Rule::OsMakedirs,
|
||||
Rule::OsRename,
|
||||
Rule::OsReplace,
|
||||
Rule::OsStat,
|
||||
Rule::OsPathJoin,
|
||||
Rule::OsPathSamefile,
|
||||
Rule::OsPathSplitext,
|
||||
Rule::BuiltinOpen,
|
||||
Rule::PyPath,
|
||||
@@ -1108,18 +1112,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::OsGetcwd) {
|
||||
flake8_use_pathlib::rules::os_getcwd(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsChmod) {
|
||||
flake8_use_pathlib::rules::os_chmod(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsRename) {
|
||||
flake8_use_pathlib::rules::os_rename(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsReplace) {
|
||||
flake8_use_pathlib::rules::os_replace(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsPathSamefile) {
|
||||
flake8_use_pathlib::rules::os_path_samefile(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
|
||||
flake8_use_pathlib::rules::path_constructor_current_directory(
|
||||
checker, call, segments,
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(crate) fn check_tokens(
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::EmptyComment) {
|
||||
pylint::rules::empty_comments(context, comment_ranges, locator, indexer);
|
||||
pylint::rules::empty_comments(context, comment_ranges, locator);
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::AmbiguousUnicodeCharacterComment) {
|
||||
|
||||
@@ -920,11 +920,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
|
||||
// flake8-use-pathlib
|
||||
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
|
||||
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsChmod),
|
||||
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsChmod),
|
||||
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
|
||||
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
|
||||
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRename),
|
||||
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReplace),
|
||||
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename),
|
||||
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace),
|
||||
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
|
||||
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
|
||||
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
|
||||
@@ -940,7 +940,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin),
|
||||
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename),
|
||||
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
|
||||
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile),
|
||||
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile),
|
||||
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
|
||||
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),
|
||||
(Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath),
|
||||
|
||||
@@ -75,12 +75,11 @@ where
|
||||
);
|
||||
|
||||
let span = Span::from(file).with_range(range);
|
||||
let annotation = Annotation::primary(span);
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
let mut annotation = Annotation::primary(span);
|
||||
if let Some(suggestion) = suggestion {
|
||||
diagnostic.help(suggestion);
|
||||
annotation = annotation.message(suggestion);
|
||||
}
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
if let Some(fix) = fix {
|
||||
diagnostic.set_fix(fix);
|
||||
|
||||
@@ -6,12 +6,13 @@ use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use ruff_annotate_snippets::{Level, Renderer, Snippet};
|
||||
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode};
|
||||
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_source_file::{LineColumn, OneIndexed};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::fs::relativize_path;
|
||||
use crate::line_width::{IndentWidth, LineWidthBuilder};
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
@@ -20,6 +21,8 @@ use crate::settings::types::UnsafeFixes;
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
struct EmitterFlags: u8 {
|
||||
/// Whether to show the fix status of a diagnostic.
|
||||
const SHOW_FIX_STATUS = 1 << 0;
|
||||
/// Whether to show the diff of a fix, for diagnostics that have a fix.
|
||||
const SHOW_FIX_DIFF = 1 << 1;
|
||||
/// Whether to show the source code of a diagnostic.
|
||||
@@ -27,27 +30,17 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextEmitter {
|
||||
flags: EmitterFlags,
|
||||
config: DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl Default for TextEmitter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
flags: EmitterFlags::default(),
|
||||
config: DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Concise)
|
||||
.hide_severity(true)
|
||||
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()),
|
||||
}
|
||||
}
|
||||
unsafe_fixes: UnsafeFixes,
|
||||
}
|
||||
|
||||
impl TextEmitter {
|
||||
#[must_use]
|
||||
pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self {
|
||||
self.config = self.config.show_fix_status(show_fix_status);
|
||||
self.flags
|
||||
.set(EmitterFlags::SHOW_FIX_STATUS, show_fix_status);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -65,15 +58,7 @@ impl TextEmitter {
|
||||
|
||||
#[must_use]
|
||||
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
|
||||
self.config = self
|
||||
.config
|
||||
.fix_applicability(unsafe_fixes.required_applicability());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_preview(mut self, preview: bool) -> Self {
|
||||
self.config = self.config.preview(preview);
|
||||
self.unsafe_fixes = unsafe_fixes;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -86,10 +71,51 @@ impl Emitter for TextEmitter {
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in diagnostics {
|
||||
write!(writer, "{}", message.display(context, &self.config))?;
|
||||
|
||||
let filename = message.expect_ruff_filename();
|
||||
write!(
|
||||
writer,
|
||||
"{path}{sep}",
|
||||
path = relativize_path(&filename).bold(),
|
||||
sep = ":".cyan(),
|
||||
)?;
|
||||
|
||||
let start_location = message.expect_ruff_start_location();
|
||||
let notebook_index = context.notebook_index(&filename);
|
||||
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let diagnostic_location = if let Some(notebook_index) = notebook_index {
|
||||
write!(
|
||||
writer,
|
||||
"cell {cell}{sep}",
|
||||
cell = notebook_index
|
||||
.cell(start_location.line)
|
||||
.unwrap_or(OneIndexed::MIN),
|
||||
sep = ":".cyan(),
|
||||
)?;
|
||||
|
||||
LineColumn {
|
||||
line: notebook_index
|
||||
.cell_row(start_location.line)
|
||||
.unwrap_or(OneIndexed::MIN),
|
||||
column: start_location.column,
|
||||
}
|
||||
} else {
|
||||
start_location
|
||||
};
|
||||
|
||||
writeln!(
|
||||
writer,
|
||||
"{row}{sep}{col}{sep} {code_and_body}",
|
||||
row = diagnostic_location.line,
|
||||
col = diagnostic_location.column,
|
||||
sep = ":".cyan(),
|
||||
code_and_body = RuleCodeAndBody {
|
||||
message,
|
||||
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS),
|
||||
unsafe_fixes: self.unsafe_fixes,
|
||||
}
|
||||
)?;
|
||||
|
||||
if self.flags.intersects(EmitterFlags::SHOW_SOURCE) {
|
||||
// The `0..0` range is used to highlight file-level diagnostics.
|
||||
if message.expect_range() != TextRange::default() {
|
||||
@@ -160,7 +186,7 @@ pub(super) struct MessageCodeFrame<'a> {
|
||||
|
||||
impl Display for MessageCodeFrame<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let suggestion = self.message.first_help_text();
|
||||
let suggestion = self.message.suggestion();
|
||||
let footers = if let Some(suggestion) = suggestion {
|
||||
vec![Level::Help.title(suggestion)]
|
||||
} else {
|
||||
|
||||
@@ -134,26 +134,6 @@ pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) ->
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_chmod_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_rename_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_replace_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_path_samefile_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19245
|
||||
pub(crate) const fn is_fix_os_getcwd_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
|
||||
@@ -64,7 +64,6 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let mut union_type = UnionKind::TypingUnion;
|
||||
let mut optional_present = false;
|
||||
// Adds a member to `literal_exprs` if it is a `Literal` annotation
|
||||
let mut check_for_duplicate_members = |expr: &'a Expr, parent: &'a Expr| {
|
||||
if matches!(parent, Expr::BinOp(_)) {
|
||||
@@ -75,7 +74,6 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
&& is_optional_type(checker, expr)
|
||||
{
|
||||
// If the union member is an `Optional`, add a virtual `None` literal.
|
||||
optional_present = true;
|
||||
&VIRTUAL_NONE_LITERAL
|
||||
} else {
|
||||
expr
|
||||
@@ -89,7 +87,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
DuplicateUnionMember {
|
||||
duplicate_name: checker.generator().expr(virtual_expr),
|
||||
},
|
||||
// Use the real expression's range for diagnostics.
|
||||
// Use the real expression's range for diagnostics,
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
@@ -106,13 +104,6 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not reduce `Union[None, ... None]` to avoid introducing a `TypeError` unintentionally
|
||||
// e.g. `isinstance(None, Union[None, None])`, if reduced to `isinstance(None, None)`, causes
|
||||
// `TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union` to throw.
|
||||
if unique_nodes.iter().all(|expr| expr.is_none_literal_expr()) && !optional_present {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark [`Fix`] as unsafe when comments are in range.
|
||||
let applicability = if checker.comment_ranges().intersects(expr.range()) {
|
||||
Applicability::Unsafe
|
||||
|
||||
@@ -974,8 +974,6 @@ PYI016.py:143:61: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[typing.Optional[complex], complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -983,8 +981,6 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -994,15 +990,3 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: typing.Optional[complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:148:37: PYI016 Duplicate union member `None`
|
||||
|
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 | # Should throw duplicate union member but not fix
|
||||
148 | isinstance(None, typing.Union[None, None])
|
||||
| ^^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `None`
|
||||
|
||||
@@ -1162,8 +1162,6 @@ PYI016.py:143:61: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[None, complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:143:72: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1181,8 +1179,6 @@ PYI016.py:143:72: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[None, complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1190,8 +1186,6 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -1201,9 +1195,6 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: None | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1211,8 +1202,6 @@ PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -1222,15 +1211,3 @@ PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: None | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:148:37: PYI016 Duplicate union member `None`
|
||||
|
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 | # Should throw duplicate union member but not fix
|
||||
148 | isinstance(None, typing.Union[None, None])
|
||||
| ^^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `None`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::{Applicability, Edit, Fix, Violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||
use ruff_python_semantic::{SemanticModel, analyze::typing};
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_ast::{Expr, ExprCall};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
|
||||
@@ -72,85 +72,3 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
|
||||
match expr {
|
||||
Expr::Name(name) => Some(name),
|
||||
Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
|
||||
pub(crate) fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
if matches!(
|
||||
expr,
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(_),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(name) = get_name_expr(expr) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
typing::is_int(binding, semantic)
|
||||
}
|
||||
|
||||
pub(crate) fn check_os_pathlib_two_arg_calls(
|
||||
checker: &Checker,
|
||||
call: &ExprCall,
|
||||
attr: &str,
|
||||
path_arg: &str,
|
||||
second_arg: &str,
|
||||
fix_enabled: bool,
|
||||
violation: impl Violation,
|
||||
) {
|
||||
let range = call.range();
|
||||
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
|
||||
|
||||
let (Some(path_expr), Some(second_expr)) = (
|
||||
call.arguments.find_argument_value(path_arg, 0),
|
||||
call.arguments.find_argument_value(second_arg, 1),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let path_code = checker.locator().slice(path_expr.range());
|
||||
let second_code = checker.locator().slice(second_expr.range());
|
||||
|
||||
if fix_enabled {
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import("pathlib", "Path"),
|
||||
call.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
|
||||
let replacement = if is_pathlib_path_call(checker, path_expr) {
|
||||
format!("{path_code}.{attr}({second_code})")
|
||||
} else {
|
||||
format!("{binding}({path_code}).{attr}({second_code})")
|
||||
};
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
Ok(Fix::applicable_edits(
|
||||
Edit::range_replacement(replacement, range),
|
||||
[import_edit],
|
||||
applicability,
|
||||
))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
pub(crate) use glob_rule::*;
|
||||
pub(crate) use invalid_pathlib_with_suffix::*;
|
||||
pub(crate) use os_chmod::*;
|
||||
pub(crate) use os_getcwd::*;
|
||||
pub(crate) use os_path_abspath::*;
|
||||
pub(crate) use os_path_basename::*;
|
||||
@@ -15,11 +14,8 @@ pub(crate) use os_path_isabs::*;
|
||||
pub(crate) use os_path_isdir::*;
|
||||
pub(crate) use os_path_isfile::*;
|
||||
pub(crate) use os_path_islink::*;
|
||||
pub(crate) use os_path_samefile::*;
|
||||
pub(crate) use os_readlink::*;
|
||||
pub(crate) use os_remove::*;
|
||||
pub(crate) use os_rename::*;
|
||||
pub(crate) use os_replace::*;
|
||||
pub(crate) use os_rmdir::*;
|
||||
pub(crate) use os_sep_split::*;
|
||||
pub(crate) use os_unlink::*;
|
||||
@@ -28,7 +24,6 @@ pub(crate) use replaceable_by_pathlib::*;
|
||||
|
||||
mod glob_rule;
|
||||
mod invalid_pathlib_with_suffix;
|
||||
mod os_chmod;
|
||||
mod os_getcwd;
|
||||
mod os_path_abspath;
|
||||
mod os_path_basename;
|
||||
@@ -43,11 +38,8 @@ mod os_path_isabs;
|
||||
mod os_path_isdir;
|
||||
mod os_path_isfile;
|
||||
mod os_path_islink;
|
||||
mod os_path_samefile;
|
||||
mod os_readlink;
|
||||
mod os_remove;
|
||||
mod os_rename;
|
||||
mod os_replace;
|
||||
mod os_rmdir;
|
||||
mod os_sep_split;
|
||||
mod os_unlink;
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_chmod_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, is_file_descriptor, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.chmod`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`. When possible, using `Path` object
|
||||
/// methods such as `Path.chmod()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.chmod()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.chmod("file.py", 0o444)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("file.py").chmod(0o444)
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
|
||||
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsChmod;
|
||||
|
||||
impl Violation for OsChmod {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.chmod()` should be replaced by `Path.chmod()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path(...).chmod(...)`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH101
|
||||
pub(crate) fn os_chmod(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if segments != ["os", "chmod"] {
|
||||
return;
|
||||
}
|
||||
|
||||
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
|
||||
// ```
|
||||
if call
|
||||
.arguments
|
||||
.find_argument_value("path", 0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"chmod",
|
||||
"path",
|
||||
"mode",
|
||||
is_fix_os_chmod_enabled(checker.settings()),
|
||||
OsChmod,
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_path_samefile_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_two_arg_calls;
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.samefile`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os.path`. When possible, using `Path` object
|
||||
/// methods such as `Path.samefile()` can improve readability over the `os.path`
|
||||
/// module's counterparts (e.g., `os.path.samefile()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.path.samefile("f1.py", "f2.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("f1.py").samefile("f2.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
|
||||
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsPathSamefile;
|
||||
|
||||
impl Violation for OsPathSamefile {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.path.samefile()` should be replaced by `Path.samefile()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path(...).samefile()`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH121
|
||||
pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if segments != ["os", "path", "samefile"] {
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"samefile",
|
||||
"f1",
|
||||
"f2",
|
||||
is_fix_os_path_samefile_enabled(checker.settings()),
|
||||
OsPathSamefile,
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_rename_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.rename`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`. When possible, using `Path` object
|
||||
/// methods such as `Path.rename()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.rename()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.rename("old.py", "new.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("old.py").rename("new.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
|
||||
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsRename;
|
||||
|
||||
impl Violation for OsRename {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.rename()` should be replaced by `Path.rename()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path(...).rename(...)`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH104
|
||||
pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if segments != ["os", "rename"] {
|
||||
return;
|
||||
}
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
|
||||
// ```
|
||||
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"rename",
|
||||
"src",
|
||||
"dst",
|
||||
is_fix_os_rename_enabled(checker.settings()),
|
||||
OsRename,
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_replace_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.replace`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`. When possible, using `Path` object
|
||||
/// methods such as `Path.replace()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.replace()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.replace("old.py", "new.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("old.py").replace("new.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
|
||||
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsReplace;
|
||||
|
||||
impl Violation for OsReplace {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.replace()` should be replaced by `Path.replace()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Replace with `Path(...).replace(...)`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PTH105
|
||||
pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
if segments != ["os", "replace"] {
|
||||
return;
|
||||
}
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
|
||||
// ```
|
||||
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"replace",
|
||||
"src",
|
||||
"dst",
|
||||
is_fix_os_replace_enabled(checker.settings()),
|
||||
OsReplace,
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
is_file_descriptor, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::rules::flake8_use_pathlib::{
|
||||
rules::Glob,
|
||||
violations::{
|
||||
BuiltinOpen, Joiner, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSplitext, OsStat,
|
||||
OsSymlink, PyPath,
|
||||
},
|
||||
use crate::rules::flake8_use_pathlib::helpers::is_keyword_only_argument_non_default;
|
||||
use crate::rules::flake8_use_pathlib::rules::Glob;
|
||||
use crate::rules::flake8_use_pathlib::violations::{
|
||||
BuiltinOpen, Joiner, OsChmod, OsListdir, OsMakedirs, OsMkdir, OsPathJoin, OsPathSamefile,
|
||||
OsPathSplitext, OsRename, OsReplace, OsStat, OsSymlink, PyPath,
|
||||
};
|
||||
|
||||
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
@@ -20,6 +18,24 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
|
||||
let range = call.func.range();
|
||||
match qualified_name.segments() {
|
||||
// PTH101
|
||||
["os", "chmod"] => {
|
||||
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.chmod)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
|
||||
// ```
|
||||
if call
|
||||
.arguments
|
||||
.find_argument_value("path", 0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsChmod, range)
|
||||
}
|
||||
// PTH102
|
||||
["os", "makedirs"] => checker.report_diagnostic_if_enabled(OsMakedirs, range),
|
||||
// PTH103
|
||||
@@ -35,6 +51,38 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsMkdir, range)
|
||||
}
|
||||
// PTH104
|
||||
["os", "rename"] => {
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.rename)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
|
||||
// ```
|
||||
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsRename, range)
|
||||
}
|
||||
// PTH105
|
||||
["os", "replace"] => {
|
||||
// `src_dir_fd` and `dst_dir_fd` are not supported by pathlib, so check if they are
|
||||
// set to non-default values.
|
||||
// Signature as of Python 3.13 (https://docs.python.org/3/library/os.html#os.replace)
|
||||
// ```text
|
||||
// 0 1 2 3
|
||||
// os.replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
|
||||
// ```
|
||||
if is_keyword_only_argument_non_default(&call.arguments, "src_dir_fd")
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dst_dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
checker.report_diagnostic_if_enabled(OsReplace, range)
|
||||
}
|
||||
// PTH116
|
||||
["os", "stat"] => {
|
||||
// `dir_fd` is not supported by pathlib, so check if it's set to non-default values.
|
||||
@@ -76,6 +124,8 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
},
|
||||
range,
|
||||
),
|
||||
// PTH121
|
||||
["os", "path", "samefile"] => checker.report_diagnostic_if_enabled(OsPathSamefile, range),
|
||||
// PTH122
|
||||
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
|
||||
// PTH211
|
||||
@@ -184,6 +234,37 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
|
||||
fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
if matches!(
|
||||
expr,
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(_),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(name) = get_name_expr(expr) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
typing::is_int(binding, semantic)
|
||||
}
|
||||
|
||||
fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
|
||||
match expr {
|
||||
Expr::Name(name) => Some(name),
|
||||
Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if argument `name` is set to a non-default `None` value.
|
||||
fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usize) -> bool {
|
||||
arguments
|
||||
|
||||
@@ -20,7 +20,6 @@ full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
9 | aaa = os.mkdir(p)
|
||||
10 | os.makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -51,7 +50,6 @@ full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
12 | os.replace(p)
|
||||
13 | os.rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -62,7 +60,6 @@ full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
13 | os.rmdir(p)
|
||||
14 | os.remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
full_name.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -256,7 +253,6 @@ full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
|
||||
31 | os.path.splitext(p)
|
||||
32 | with open(p) as fp:
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -20,7 +20,6 @@ import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
9 | aaa = foo.mkdir(p)
|
||||
10 | foo.makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -51,7 +50,6 @@ import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
12 | foo.replace(p)
|
||||
13 | foo.rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -62,7 +60,6 @@ import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
13 | foo.rmdir(p)
|
||||
14 | foo.remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_as.py:13:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -255,7 +252,6 @@ import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
|
||||
| ^^^^^^^^^^^^^^ PTH121
|
||||
31 | foo_p.splitext(p)
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -20,7 +20,6 @@ import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
11 | aaa = mkdir(p)
|
||||
12 | makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -51,7 +50,6 @@ import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
14 | replace(p)
|
||||
15 | rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -62,7 +60,6 @@ import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()
|
||||
15 | rmdir(p)
|
||||
16 | remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_from.py:15:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -256,7 +253,6 @@ import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.sam
|
||||
33 | splitext(p)
|
||||
34 | with open(p) as fp:
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
@@ -293,36 +289,3 @@ import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()`
|
||||
43 | with open(p) as _: ... # Error
|
||||
| ^^^^ PTH123
|
||||
|
|
||||
|
||||
import_from.py:53:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
51 | file = "file_1.py"
|
||||
52 |
|
||||
53 | rename(file, "file_2.py")
|
||||
| ^^^^^^ PTH104
|
||||
54 |
|
||||
55 | rename(
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from.py:55:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
53 | rename(file, "file_2.py")
|
||||
54 |
|
||||
55 | rename(
|
||||
| ^^^^^^ PTH104
|
||||
56 | # commment 1
|
||||
57 | file, # comment 2
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from.py:63:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
61 | )
|
||||
62 |
|
||||
63 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
| ^^^^^^ PTH104
|
||||
64 |
|
||||
65 | rename(file, "file_2.py", src_dir_fd=1)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
@@ -20,7 +20,6 @@ import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
16 | aaa = xmkdir(p)
|
||||
17 | xmakedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -51,7 +50,6 @@ import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename(
|
||||
19 | xreplace(p)
|
||||
20 | xrmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -62,7 +60,6 @@ import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replac
|
||||
20 | xrmdir(p)
|
||||
21 | xremove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_from_as.py:20:1: PTH106 `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -255,7 +252,6 @@ import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.
|
||||
| ^^^^^^^^^ PTH121
|
||||
38 | xsplitext(p)
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -34,7 +34,6 @@ full_name.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
9 | aaa = os.mkdir(p)
|
||||
10 | os.makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
full_name.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -65,7 +64,6 @@ full_name.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
12 | os.replace(p)
|
||||
13 | os.rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -76,7 +74,6 @@ full_name.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
13 | os.rmdir(p)
|
||||
14 | os.remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
full_name.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -474,7 +471,6 @@ full_name.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
|
||||
31 | os.path.splitext(p)
|
||||
32 | with open(p) as fp:
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
full_name.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -34,7 +34,6 @@ import_as.py:8:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
9 | aaa = foo.mkdir(p)
|
||||
10 | foo.makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_as.py:9:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -65,7 +64,6 @@ import_as.py:11:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
12 | foo.replace(p)
|
||||
13 | foo.rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -76,7 +74,6 @@ import_as.py:12:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
13 | foo.rmdir(p)
|
||||
14 | foo.remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_as.py:13:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -472,7 +469,6 @@ import_as.py:30:1: PTH121 `os.path.samefile()` should be replaced by `Path.samef
|
||||
| ^^^^^^^^^^^^^^ PTH121
|
||||
31 | foo_p.splitext(p)
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_as.py:31:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -35,7 +35,6 @@ import_from.py:10:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
11 | aaa = mkdir(p)
|
||||
12 | makedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_from.py:11:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -66,7 +65,6 @@ import_from.py:13:1: PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
14 | replace(p)
|
||||
15 | rmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -77,7 +75,6 @@ import_from.py:14:1: PTH105 `os.replace()` should be replaced by `Path.replace()
|
||||
15 | rmdir(p)
|
||||
16 | remove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_from.py:15:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -487,7 +484,6 @@ import_from.py:32:1: PTH121 `os.path.samefile()` should be replaced by `Path.sam
|
||||
33 | splitext(p)
|
||||
34 | with open(p) as fp:
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_from.py:33:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
@@ -524,95 +520,3 @@ import_from.py:43:10: PTH123 `open()` should be replaced by `Path.open()`
|
||||
43 | with open(p) as _: ... # Error
|
||||
| ^^^^ PTH123
|
||||
|
|
||||
|
||||
import_from.py:53:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
51 | file = "file_1.py"
|
||||
52 |
|
||||
53 | rename(file, "file_2.py")
|
||||
| ^^^^^^ PTH104
|
||||
54 |
|
||||
55 | rename(
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | from os import remove, unlink, getcwd, readlink, stat
|
||||
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
|
||||
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
|
||||
5 |+import pathlib
|
||||
5 6 |
|
||||
6 7 | p = "/foo"
|
||||
7 8 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
50 51 |
|
||||
51 52 | file = "file_1.py"
|
||||
52 53 |
|
||||
53 |-rename(file, "file_2.py")
|
||||
54 |+pathlib.Path(file).rename("file_2.py")
|
||||
54 55 |
|
||||
55 56 | rename(
|
||||
56 57 | # commment 1
|
||||
|
||||
import_from.py:55:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
53 | rename(file, "file_2.py")
|
||||
54 |
|
||||
55 | rename(
|
||||
| ^^^^^^ PTH104
|
||||
56 | # commment 1
|
||||
57 | file, # comment 2
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | from os import remove, unlink, getcwd, readlink, stat
|
||||
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
|
||||
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
|
||||
5 |+import pathlib
|
||||
5 6 |
|
||||
6 7 | p = "/foo"
|
||||
7 8 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
52 53 |
|
||||
53 54 | rename(file, "file_2.py")
|
||||
54 55 |
|
||||
55 |-rename(
|
||||
56 |- # commment 1
|
||||
57 |- file, # comment 2
|
||||
58 |- "file_2.py"
|
||||
59 |- ,
|
||||
60 |- # comment 3
|
||||
61 |-)
|
||||
56 |+pathlib.Path(file).rename("file_2.py")
|
||||
62 57 |
|
||||
63 58 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
64 59 |
|
||||
|
||||
import_from.py:63:1: PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
|
||||
|
|
||||
61 | )
|
||||
62 |
|
||||
63 | rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
| ^^^^^^ PTH104
|
||||
64 |
|
||||
65 | rename(file, "file_2.py", src_dir_fd=1)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | from os import remove, unlink, getcwd, readlink, stat
|
||||
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
|
||||
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext
|
||||
5 |+import pathlib
|
||||
5 6 |
|
||||
6 7 | p = "/foo"
|
||||
7 8 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
60 61 | # comment 3
|
||||
61 62 | )
|
||||
62 63 |
|
||||
63 |-rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
64 |+pathlib.Path(file).rename("file_2.py")
|
||||
64 65 |
|
||||
65 66 | rename(file, "file_2.py", src_dir_fd=1)
|
||||
|
||||
@@ -35,7 +35,6 @@ import_from_as.py:15:6: PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
16 | aaa = xmkdir(p)
|
||||
17 | xmakedirs(p)
|
||||
|
|
||||
= help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
import_from_as.py:16:7: PTH102 `os.mkdir()` should be replaced by `Path.mkdir()`
|
||||
|
|
||||
@@ -66,7 +65,6 @@ import_from_as.py:18:1: PTH104 `os.rename()` should be replaced by `Path.rename(
|
||||
19 | xreplace(p)
|
||||
20 | xrmdir(p)
|
||||
|
|
||||
= help: Replace with `Path(...).rename(...)`
|
||||
|
||||
import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
|
|
||||
@@ -77,7 +75,6 @@ import_from_as.py:19:1: PTH105 `os.replace()` should be replaced by `Path.replac
|
||||
20 | xrmdir(p)
|
||||
21 | xremove(p)
|
||||
|
|
||||
= help: Replace with `Path(...).replace(...)`
|
||||
|
||||
import_from_as.py:20:1: PTH106 [*] `os.rmdir()` should be replaced by `Path.rmdir()`
|
||||
|
|
||||
@@ -485,7 +482,6 @@ import_from_as.py:37:1: PTH121 `os.path.samefile()` should be replaced by `Path.
|
||||
| ^^^^^^^^^ PTH121
|
||||
38 | xsplitext(p)
|
||||
|
|
||||
= help: Replace with `Path(...).samefile()`
|
||||
|
||||
import_from_as.py:38:1: PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
|
||||
|
|
||||
|
||||
@@ -2,6 +2,51 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
|
||||
use crate::Violation;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.chmod`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`. When possible, using `Path` object
|
||||
/// methods such as `Path.chmod()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.chmod()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.chmod("file.py", 0o444)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("file.py").chmod(0o444)
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
|
||||
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsChmod;
|
||||
|
||||
impl Violation for OsChmod {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.chmod()` should be replaced by `Path.chmod()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.makedirs`.
|
||||
///
|
||||
@@ -92,6 +137,99 @@ impl Violation for OsMkdir {
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.rename`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`. When possible, using `Path` object
|
||||
/// methods such as `Path.rename()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.rename()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.rename("old.py", "new.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("old.py").rename("new.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
|
||||
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsRename;
|
||||
|
||||
impl Violation for OsRename {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.rename()` should be replaced by `Path.rename()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.replace`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os`. When possible, using `Path` object
|
||||
/// methods such as `Path.replace()` can improve readability over the `os`
|
||||
/// module's counterparts (e.g., `os.replace()`).
|
||||
///
|
||||
/// Note that `os` functions may be preferable if performance is a concern,
|
||||
/// e.g., in hot loops.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.replace("old.py", "new.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("old.py").replace("new.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
|
||||
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsReplace;
|
||||
|
||||
impl Violation for OsReplace {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.replace()` should be replaced by `Path.replace()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.stat`.
|
||||
///
|
||||
@@ -209,6 +347,51 @@ pub(crate) enum Joiner {
|
||||
Joinpath,
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.samefile`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `pathlib` offers a high-level API for path manipulation, as compared to
|
||||
/// the lower-level API offered by `os.path`. When possible, using `Path` object
|
||||
/// methods such as `Path.samefile()` can improve readability over the `os.path`
|
||||
/// module's counterparts (e.g., `os.path.samefile()`).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import os
|
||||
///
|
||||
/// os.path.samefile("f1.py", "f2.py")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// Path("f1.py").samefile("f2.py")
|
||||
/// ```
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
/// especially on older versions of Python.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
|
||||
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct OsPathSamefile;
|
||||
|
||||
impl Violation for OsPathSamefile {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`os.path.samefile()` should be replaced by `Path.samefile()`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.splitext`.
|
||||
///
|
||||
|
||||
@@ -100,7 +100,7 @@ pub(crate) fn invalid_function_name(
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore the do_* methods of the http.server.BaseHTTPRequestHandler class and its subclasses
|
||||
// Ignore the do_* methods of the http.server.BaseHTTPRequestHandler class
|
||||
if name.starts_with("do_")
|
||||
&& parent_class.is_some_and(|class| {
|
||||
any_base_class(class, semantic, &mut |superclass| {
|
||||
@@ -108,13 +108,7 @@ pub(crate) fn invalid_function_name(
|
||||
qualified.is_some_and(|name| {
|
||||
matches!(
|
||||
name.segments(),
|
||||
[
|
||||
"http",
|
||||
"server",
|
||||
"BaseHTTPRequestHandler"
|
||||
| "CGIHTTPRequestHandler"
|
||||
| "SimpleHTTPRequestHandler"
|
||||
]
|
||||
["http", "server", "BaseHTTPRequestHandler"]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,21 +55,3 @@ N802.py:84:9: N802 Function name `dont_GET` should be lowercase
|
||||
| ^^^^^^^^ N802
|
||||
85 | pass
|
||||
|
|
||||
|
||||
N802.py:95:9: N802 Function name `dont_OPTIONS` should be lowercase
|
||||
|
|
||||
93 | pass
|
||||
94 |
|
||||
95 | def dont_OPTIONS(self):
|
||||
| ^^^^^^^^^^^^ N802
|
||||
96 | pass
|
||||
|
|
||||
|
||||
N802.py:106:9: N802 Function name `dont_OPTIONS` should be lowercase
|
||||
|
|
||||
104 | pass
|
||||
105 |
|
||||
106 | def dont_OPTIONS(self):
|
||||
| ^^^^^^^^^^^^ N802
|
||||
107 | pass
|
||||
|
|
||||
|
||||
@@ -406,14 +406,7 @@ fn convert_to_list_extend(
|
||||
};
|
||||
let target_str = locator.slice(for_stmt.target.range());
|
||||
let elt_str = locator.slice(to_append);
|
||||
let generator_str = if to_append
|
||||
.as_generator_expr()
|
||||
.is_some_and(|generator| !generator.parenthesized)
|
||||
{
|
||||
format!("({elt_str}) {for_type} {target_str} in {for_iter_str}{if_str}")
|
||||
} else {
|
||||
format!("{elt_str} {for_type} {target_str} in {for_iter_str}{if_str}")
|
||||
};
|
||||
let generator_str = format!("{elt_str} {for_type} {target_str} in {for_iter_str}{if_str}");
|
||||
|
||||
let variable_name = locator.slice(binding);
|
||||
let for_loop_inline_comments = comment_strings_in_range(
|
||||
|
||||
@@ -241,27 +241,5 @@ PERF401.py:280:13: PERF401 Use `list.extend` to create a transformed list
|
||||
279 | if lambda: 0:
|
||||
280 | dst.append(i)
|
||||
| ^^^^^^^^^^^^^ PERF401
|
||||
281 |
|
||||
282 | def f():
|
||||
|
|
||||
= help: Replace for loop with list.extend
|
||||
|
||||
PERF401.py:286:9: PERF401 Use a list comprehension to create a transformed list
|
||||
|
|
||||
284 | result = []
|
||||
285 | for i in range(3):
|
||||
286 | result.append(x for x in [i])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
287 |
|
||||
288 | def f():
|
||||
|
|
||||
= help: Replace for loop with list comprehension
|
||||
|
||||
PERF401.py:292:9: PERF401 Use a list comprehension to create a transformed list
|
||||
|
|
||||
290 | result = []
|
||||
291 | for i in range(3):
|
||||
292 | result.append((x for x in [i]))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
|
|
||||
= help: Replace for loop with list comprehension
|
||||
|
||||
@@ -566,8 +566,6 @@ PERF401.py:280:13: PERF401 [*] Use `list.extend` to create a transformed list
|
||||
279 | if lambda: 0:
|
||||
280 | dst.append(i)
|
||||
| ^^^^^^^^^^^^^ PERF401
|
||||
281 |
|
||||
282 | def f():
|
||||
|
|
||||
= help: Replace for loop with list.extend
|
||||
|
||||
@@ -579,47 +577,3 @@ PERF401.py:280:13: PERF401 [*] Use `list.extend` to create a transformed list
|
||||
279 |- if lambda: 0:
|
||||
280 |- dst.append(i)
|
||||
278 |+ dst.extend(i for i in src if (lambda: 0))
|
||||
281 279 |
|
||||
282 280 | def f():
|
||||
283 281 | i = "xyz"
|
||||
|
||||
PERF401.py:286:9: PERF401 [*] Use a list comprehension to create a transformed list
|
||||
|
|
||||
284 | result = []
|
||||
285 | for i in range(3):
|
||||
286 | result.append(x for x in [i])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
287 |
|
||||
288 | def f():
|
||||
|
|
||||
= help: Replace for loop with list comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
281 281 |
|
||||
282 282 | def f():
|
||||
283 283 | i = "xyz"
|
||||
284 |- result = []
|
||||
285 |- for i in range(3):
|
||||
286 |- result.append(x for x in [i])
|
||||
284 |+ result = [(x for x in [i]) for i in range(3)]
|
||||
287 285 |
|
||||
288 286 | def f():
|
||||
289 287 | i = "xyz"
|
||||
|
||||
PERF401.py:292:9: PERF401 [*] Use a list comprehension to create a transformed list
|
||||
|
|
||||
290 | result = []
|
||||
291 | for i in range(3):
|
||||
292 | result.append((x for x in [i]))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
|
||||
|
|
||||
= help: Replace for loop with list comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
287 287 |
|
||||
288 288 | def f():
|
||||
289 289 | i = "xyz"
|
||||
290 |- result = []
|
||||
291 |- for i in range(3):
|
||||
292 |- result.append((x for x in [i]))
|
||||
290 |+ result = [(x for x in [i]) for i in range(3)]
|
||||
|
||||
@@ -48,7 +48,6 @@ mod tests {
|
||||
#[test_case(Rule::ComparisonWithItself, Path::new("comparison_with_itself.py"))]
|
||||
#[test_case(Rule::EqWithoutHash, Path::new("eq_without_hash.py"))]
|
||||
#[test_case(Rule::EmptyComment, Path::new("empty_comment.py"))]
|
||||
#[test_case(Rule::EmptyComment, Path::new("empty_comment_line_continuation.py"))]
|
||||
#[test_case(Rule::ManualFromImport, Path::new("import_aliasing.py"))]
|
||||
#[test_case(Rule::IfStmtMinMax, Path::new("if_stmt_min_max.py"))]
|
||||
#[test_case(Rule::SingleStringSlots, Path::new("single_string_slots.py"))]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_trivia::{CommentRanges, is_python_whitespace};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
@@ -50,7 +49,6 @@ pub(crate) fn empty_comments(
|
||||
context: &LintContext,
|
||||
comment_ranges: &CommentRanges,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
) {
|
||||
let block_comments = comment_ranges.block_comments(locator.contents());
|
||||
|
||||
@@ -61,12 +59,12 @@ pub(crate) fn empty_comments(
|
||||
}
|
||||
|
||||
// If the line contains an empty comment, add a diagnostic.
|
||||
empty_comment(context, range, locator, indexer);
|
||||
empty_comment(context, range, locator);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [`Diagnostic`] if the comment at the given [`TextRange`] is empty.
|
||||
fn empty_comment(context: &LintContext, range: TextRange, locator: &Locator, indexer: &Indexer) {
|
||||
fn empty_comment(context: &LintContext, range: TextRange, locator: &Locator) {
|
||||
// Check: is the comment empty?
|
||||
if !locator
|
||||
.slice(range)
|
||||
@@ -97,20 +95,12 @@ fn empty_comment(context: &LintContext, range: TextRange, locator: &Locator, ind
|
||||
}
|
||||
});
|
||||
|
||||
// If there is no character preceding the comment, this comment must be on its own physical line.
|
||||
// If there is a line preceding the empty comment's line, check if it ends in a line continuation character. (`\`)
|
||||
let is_on_same_logical_line = indexer
|
||||
.preceded_by_continuations(first_hash_col, locator.contents())
|
||||
.is_some();
|
||||
|
||||
if let Some(mut diagnostic) = context
|
||||
.report_diagnostic_if_enabled(EmptyComment, TextRange::new(first_hash_col, line.end()))
|
||||
{
|
||||
diagnostic.set_fix(Fix::safe_edit(
|
||||
if let Some(deletion_start_col) = deletion_start_col {
|
||||
Edit::deletion(line.start() + deletion_start_col, line.end())
|
||||
} else if is_on_same_logical_line {
|
||||
Edit::deletion(first_hash_col, line.end())
|
||||
} else {
|
||||
Edit::range_deletion(locator.full_line_range(first_hash_col))
|
||||
},
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
empty_comment_line_continuation.py:1:1: PLR2044 [*] Line with empty comment
|
||||
|
|
||||
1 | #
|
||||
| ^ PLR2044
|
||||
2 | x = 0 \
|
||||
3 | #
|
||||
|
|
||||
= help: Delete the empty comment
|
||||
|
||||
ℹ Safe fix
|
||||
1 |-#
|
||||
2 1 | x = 0 \
|
||||
3 2 | #
|
||||
4 3 | +1
|
||||
|
||||
empty_comment_line_continuation.py:3:1: PLR2044 [*] Line with empty comment
|
||||
|
|
||||
1 | #
|
||||
2 | x = 0 \
|
||||
3 | #
|
||||
| ^ PLR2044
|
||||
4 | +1
|
||||
5 | print(x)
|
||||
|
|
||||
= help: Delete the empty comment
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | #
|
||||
2 2 | x = 0 \
|
||||
3 |-#
|
||||
3 |+
|
||||
4 4 | +1
|
||||
5 5 | print(x)
|
||||
@@ -272,7 +272,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
|
||||
}
|
||||
|
||||
assert!(
|
||||
!(fixable && diagnostic.first_help_text().is_none()),
|
||||
!(fixable && diagnostic.suggestion().is_none()),
|
||||
"Diagnostic emitted by {rule:?} is fixable but \
|
||||
`Violation::fix_title` returns `None`"
|
||||
);
|
||||
|
||||
@@ -235,7 +235,12 @@ impl TraversalSignal {
|
||||
}
|
||||
|
||||
pub fn walk_annotation<'a, V: SourceOrderVisitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
|
||||
visitor.visit_expr(expr);
|
||||
let node = AnyNodeRef::from(expr);
|
||||
if visitor.enter_node(node).is_traverse() {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
|
||||
visitor.leave_node(node);
|
||||
}
|
||||
|
||||
pub fn walk_decorator<'a, V>(visitor: &mut V, decorator: &'a Decorator)
|
||||
|
||||
@@ -163,7 +163,7 @@ fn stem(path: &str) -> &str {
|
||||
}
|
||||
|
||||
/// Infer the [`Visibility`] of a module from its path.
|
||||
pub(crate) fn module_visibility(module: Module) -> Visibility {
|
||||
pub(crate) fn module_visibility(module: &Module) -> Visibility {
|
||||
match &module.source {
|
||||
ModuleSource::Path(path) => {
|
||||
if path.iter().any(|m| is_private_module(m)) {
|
||||
|
||||
@@ -223,7 +223,7 @@ impl<'a> Definitions<'a> {
|
||||
// visibility.
|
||||
let visibility = {
|
||||
match &definition {
|
||||
Definition::Module(module) => module_visibility(*module),
|
||||
Definition::Module(module) => module_visibility(module),
|
||||
Definition::Member(member) => match member.kind {
|
||||
MemberKind::Class(class) => {
|
||||
let parent = &definitions[member.parent];
|
||||
|
||||
@@ -238,7 +238,7 @@ fn to_lsp_diagnostic(
|
||||
let name = diagnostic.name();
|
||||
let body = diagnostic.body().to_string();
|
||||
let fix = diagnostic.fix();
|
||||
let suggestion = diagnostic.first_help_text();
|
||||
let suggestion = diagnostic.suggestion();
|
||||
let code = diagnostic.secondary_code();
|
||||
|
||||
let fix = fix.and_then(|fix| fix.applies(Applicability::Unsafe).then_some(fix));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.5"
|
||||
version = "0.12.4"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -234,7 +234,7 @@ impl Workspace {
|
||||
start_location: source_code.line_column(msg.expect_range().start()).into(),
|
||||
end_location: source_code.line_column(msg.expect_range().end()).into(),
|
||||
fix: msg.fix().map(|fix| ExpandedFix {
|
||||
message: msg.first_help_text().map(ToString::to_string),
|
||||
message: msg.suggestion().map(ToString::to_string),
|
||||
edits: fix
|
||||
.edits()
|
||||
.iter()
|
||||
|
||||
8
crates/ty/docs/rules.md
generated
8
crates/ty/docs/rules.md
generated
@@ -16,7 +16,7 @@ Checks for byte-strings in type annotation positions.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Static analysis tools like ty can't analyze type annotations that use byte-string notation.
|
||||
Static analysis tools like ty can't analyse type annotations that use byte-string notation.
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -257,7 +257,7 @@ Checks for f-strings in type annotation positions.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Static analysis tools like ty can't analyze type annotations that use f-string notation.
|
||||
Static analysis tools like ty can't analyse type annotations that use f-string notation.
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -286,7 +286,7 @@ Checks for implicit concatenated strings in type annotation positions.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Static analysis tools like ty can't analyze type annotations that use implicit concatenated strings.
|
||||
Static analysis tools like ty can't analyse type annotations that use implicit concatenated strings.
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -1276,7 +1276,7 @@ Checks for raw-strings in type annotation positions.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Static analysis tools like ty can't analyze type annotations that use raw-string notation.
|
||||
Static analysis tools like ty can't analyse type annotations that use raw-string notation.
|
||||
|
||||
**Examples**
|
||||
|
||||
|
||||
@@ -676,7 +676,7 @@ fn invalid_include_pattern_concise_output() -> anyhow::Result<()> {
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
ty failed
|
||||
Cause: ty.toml:4:5: error[invalid-glob] Invalid include pattern: Too many stars at position 5
|
||||
Cause: error[invalid-glob] ty.toml:4:5: Invalid include pattern: Too many stars at position 5
|
||||
");
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -592,8 +592,8 @@ fn concise_diagnostics() -> anyhow::Result<()> {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
test.py:2:7: warning[unresolved-reference] Name `x` used when not defined
|
||||
test.py:3:7: error[non-subscriptable] Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
warning[unresolved-reference] test.py:2:7: Name `x` used when not defined
|
||||
error[non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
@@ -627,7 +627,7 @@ fn concise_revealed_type() -> anyhow::Result<()> {
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
test.py:5:13: info[revealed-type] Revealed type: `Literal["hello"]`
|
||||
info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]`
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -230,21 +230,6 @@ impl TestCase {
|
||||
fn system_file(&self, path: impl AsRef<SystemPath>) -> Result<File, FileError> {
|
||||
system_path_to_file(self.db(), path.as_ref())
|
||||
}
|
||||
|
||||
fn module<'c>(&'c self, name: &str) -> Module<'c> {
|
||||
resolve_module(self.db(), &ModuleName::new(name).unwrap()).expect("module to be present")
|
||||
}
|
||||
|
||||
fn sorted_submodule_names(&self, parent_module_name: &str) -> Vec<String> {
|
||||
let mut names = self
|
||||
.module(parent_module_name)
|
||||
.all_submodules(self.db())
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
}
|
||||
|
||||
trait MatchEvent {
|
||||
@@ -1413,7 +1398,7 @@ mod unix {
|
||||
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_project = case.project_path("bar/baz.py");
|
||||
let baz_file = baz.file(case.db()).unwrap();
|
||||
let baz_file = baz.file().unwrap();
|
||||
|
||||
assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
|
||||
assert_eq!(
|
||||
@@ -1488,7 +1473,7 @@ mod unix {
|
||||
|
||||
let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
|
||||
.expect("Expected bar.baz to exist in site-packages.");
|
||||
let baz_file = baz.file(case.db()).unwrap();
|
||||
let baz_file = baz.file().unwrap();
|
||||
let bar_baz = case.project_path("bar/baz.py");
|
||||
|
||||
let patched_bar_baz = case.project_path("patched/bar/baz.py");
|
||||
@@ -1609,10 +1594,7 @@ mod unix {
|
||||
"def baz(): ..."
|
||||
);
|
||||
assert_eq!(
|
||||
baz.file(case.db())
|
||||
.unwrap()
|
||||
.path(case.db())
|
||||
.as_system_path(),
|
||||
baz.file().unwrap().path(case.db()).as_system_path(),
|
||||
Some(&*baz_original)
|
||||
);
|
||||
|
||||
@@ -1909,9 +1891,19 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
|
||||
#[test]
|
||||
fn submodule_cache_invalidation_created() -> anyhow::Result<()> {
|
||||
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
|
||||
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
|
||||
let get_submodules = |db: &dyn Db, module: &Module| {
|
||||
let mut names = module
|
||||
.all_submodules(db)
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names.join("\n")
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -1920,7 +1912,7 @@ fn submodule_cache_invalidation_created() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
@@ -1940,9 +1932,19 @@ fn submodule_cache_invalidation_deleted() -> anyhow::Result<()> {
|
||||
("bar/foo.py", ""),
|
||||
("bar/wazoo.py", ""),
|
||||
])?;
|
||||
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
|
||||
let get_submodules = |db: &dyn Db, module: &Module| {
|
||||
let mut names = module
|
||||
.all_submodules(db)
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names.join("\n")
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
@@ -1954,7 +1956,7 @@ fn submodule_cache_invalidation_deleted() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -1966,9 +1968,19 @@ fn submodule_cache_invalidation_deleted() -> anyhow::Result<()> {
|
||||
#[test]
|
||||
fn submodule_cache_invalidation_created_then_deleted() -> anyhow::Result<()> {
|
||||
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
|
||||
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
|
||||
let get_submodules = |db: &dyn Db, module: &Module| {
|
||||
let mut names = module
|
||||
.all_submodules(db)
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names.join("\n")
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -1981,7 +1993,7 @@ fn submodule_cache_invalidation_created_then_deleted() -> anyhow::Result<()> {
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -1994,9 +2006,19 @@ fn submodule_cache_invalidation_created_then_deleted() -> anyhow::Result<()> {
|
||||
#[test]
|
||||
fn submodule_cache_invalidation_after_pyproject_created() -> anyhow::Result<()> {
|
||||
let mut case = setup([("lib.py", ""), ("bar/__init__.py", ""), ("bar/foo.py", "")])?;
|
||||
let module = resolve_module(case.db(), &ModuleName::new("bar").unwrap()).expect("`bar` module");
|
||||
let get_submodules = |db: &dyn Db, module: &Module| {
|
||||
let mut names = module
|
||||
.all_submodules(db)
|
||||
.iter()
|
||||
.map(|name| name.as_str().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
names.sort();
|
||||
names.join("\n")
|
||||
};
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@"foo",
|
||||
);
|
||||
|
||||
@@ -2007,7 +2029,7 @@ fn submodule_cache_invalidation_after_pyproject_created() -> anyhow::Result<()>
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
case.sorted_submodule_names("bar").join("\n"),
|
||||
get_submodules(case.db(), &module),
|
||||
@r"
|
||||
foo
|
||||
wazoo
|
||||
|
||||
@@ -19,15 +19,15 @@ ruff_python_trivia = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ty_python_semantic = { workspace = true }
|
||||
ty_project = { workspace = true, features = ["testing"] }
|
||||
|
||||
itertools = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ty_vendored = { workspace = true }
|
||||
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
|
||||
|
||||
117
crates/ty_ide/src/db.rs
Normal file
117
crates/ty_ide/src/db.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use ty_python_semantic::Db as SemanticDb;
|
||||
|
||||
#[salsa::db]
|
||||
pub trait Db: SemanticDb {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::Db;
|
||||
use ruff_db::Db as SourceDb;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use ty_python_semantic::{Db as SemanticDb, Program, default_lint_registry};
|
||||
|
||||
type Events = Arc<Mutex<Vec<salsa::Event>>>;
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Events,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let events = Events::default();
|
||||
Self {
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
let events = events.clone();
|
||||
move |event| {
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}))),
|
||||
system: TestSystem::default(),
|
||||
vendored: ty_vendored::file_system().clone(),
|
||||
events,
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for TestDb {
|
||||
fn should_check_file(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self, _file: File) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
default_lint_registry()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for TestDb {}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {}
|
||||
}
|
||||
@@ -52,7 +52,9 @@ pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode
|
||||
if visitor.ancestors.is_empty() {
|
||||
visitor.ancestors.push(root);
|
||||
}
|
||||
CoveringNode::from_ancestors(visitor.ancestors)
|
||||
CoveringNode {
|
||||
nodes: visitor.ancestors,
|
||||
}
|
||||
}
|
||||
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
@@ -65,12 +67,6 @@ pub(crate) struct CoveringNode<'a> {
|
||||
}
|
||||
|
||||
impl<'a> CoveringNode<'a> {
|
||||
/// Creates a new `CoveringNode` from a list of ancestor nodes.
|
||||
/// The ancestors should be ordered from root to the covering node.
|
||||
pub(crate) fn from_ancestors(ancestors: Vec<AnyNodeRef<'a>>) -> Self {
|
||||
Self { nodes: ancestors }
|
||||
}
|
||||
|
||||
/// Returns the covering node found.
|
||||
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
|
||||
*self
|
||||
|
||||
@@ -2,8 +2,6 @@ pub use crate::goto_declaration::goto_declaration;
|
||||
pub use crate::goto_definition::goto_definition;
|
||||
pub use crate::goto_type_definition::goto_type_definition;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::find_node::covering_node;
|
||||
use crate::stub_mapping::StubMapper;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
@@ -272,270 +270,10 @@ impl GotoTarget<'_> {
|
||||
definitions_to_navigation_targets(db, stub_mapper, definitions)
|
||||
}
|
||||
|
||||
// For exception variables, they are their own definitions (like parameters)
|
||||
GotoTarget::ExceptVariable(except_handler) => {
|
||||
if let Some(name) = &except_handler.name {
|
||||
let range = name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget::new(
|
||||
file, range,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// For pattern match rest variables, they are their own definitions
|
||||
GotoTarget::PatternMatchRest(pattern_mapping) => {
|
||||
if let Some(rest_name) = &pattern_mapping.rest {
|
||||
let range = rest_name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget::new(
|
||||
file, range,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// For pattern match as names, they are their own definitions
|
||||
GotoTarget::PatternMatchAsName(pattern_as) => {
|
||||
if let Some(name) = &pattern_as.name {
|
||||
let range = name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget::new(
|
||||
file, range,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle string literals that map to TypedDict fields
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the text representation of this goto target.
|
||||
/// Returns `None` if no meaningful string representation can be provided.
|
||||
/// This is used by the "references" feature, which looks for references
|
||||
/// to this goto target.
|
||||
pub(crate) fn to_string(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => match expression {
|
||||
ast::ExprRef::Name(name) => Some(Cow::Borrowed(name.id.as_str())),
|
||||
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
|
||||
_ => None,
|
||||
},
|
||||
GotoTarget::FunctionDef(function) => Some(Cow::Borrowed(function.name.as_str())),
|
||||
GotoTarget::ClassDef(class) => Some(Cow::Borrowed(class.name.as_str())),
|
||||
GotoTarget::Parameter(parameter) => Some(Cow::Borrowed(parameter.name.as_str())),
|
||||
GotoTarget::ImportSymbolAlias { alias, .. } => {
|
||||
if let Some(asname) = &alias.asname {
|
||||
Some(Cow::Borrowed(asname.as_str()))
|
||||
} else {
|
||||
Some(Cow::Borrowed(alias.name.as_str()))
|
||||
}
|
||||
}
|
||||
GotoTarget::ImportModuleComponent {
|
||||
module_name,
|
||||
component_index,
|
||||
..
|
||||
} => {
|
||||
let components: Vec<&str> = module_name.split('.').collect();
|
||||
if let Some(component) = components.get(*component_index) {
|
||||
Some(Cow::Borrowed(*component))
|
||||
} else {
|
||||
Some(Cow::Borrowed(module_name))
|
||||
}
|
||||
}
|
||||
GotoTarget::ImportModuleAlias { alias } => {
|
||||
if let Some(asname) = &alias.asname {
|
||||
Some(Cow::Borrowed(asname.as_str()))
|
||||
} else {
|
||||
Some(Cow::Borrowed(alias.name.as_str()))
|
||||
}
|
||||
}
|
||||
GotoTarget::ExceptVariable(except) => {
|
||||
Some(Cow::Borrowed(except.name.as_ref()?.as_str()))
|
||||
}
|
||||
GotoTarget::KeywordArgument { keyword, .. } => {
|
||||
Some(Cow::Borrowed(keyword.arg.as_ref()?.as_str()))
|
||||
}
|
||||
GotoTarget::PatternMatchRest(rest) => Some(Cow::Borrowed(rest.rest.as_ref()?.as_str())),
|
||||
GotoTarget::PatternKeywordArgument(keyword) => {
|
||||
Some(Cow::Borrowed(keyword.attr.as_str()))
|
||||
}
|
||||
GotoTarget::PatternMatchStarName(star) => {
|
||||
Some(Cow::Borrowed(star.name.as_ref()?.as_str()))
|
||||
}
|
||||
GotoTarget::PatternMatchAsName(as_name) => {
|
||||
Some(Cow::Borrowed(as_name.name.as_ref()?.as_str()))
|
||||
}
|
||||
GotoTarget::TypeParamTypeVarName(type_var) => {
|
||||
Some(Cow::Borrowed(type_var.name.as_str()))
|
||||
}
|
||||
GotoTarget::TypeParamParamSpecName(spec) => Some(Cow::Borrowed(spec.name.as_str())),
|
||||
GotoTarget::TypeParamTypeVarTupleName(tuple) => {
|
||||
Some(Cow::Borrowed(tuple.name.as_str()))
|
||||
}
|
||||
GotoTarget::NonLocal { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())),
|
||||
GotoTarget::Globals { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `GotoTarget` from a `CoveringNode` and an offset within the node
|
||||
pub(crate) fn from_covering_node<'a>(
|
||||
covering_node: &crate::find_node::CoveringNode<'a>,
|
||||
offset: TextSize,
|
||||
) -> Option<GotoTarget<'a>> {
|
||||
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
|
||||
|
||||
match covering_node.node() {
|
||||
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
|
||||
Some(AnyNodeRef::StmtFunctionDef(function)) => {
|
||||
Some(GotoTarget::FunctionDef(function))
|
||||
}
|
||||
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
|
||||
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
|
||||
Some(AnyNodeRef::Alias(alias)) => {
|
||||
// Find the containing import statement to determine the type
|
||||
let import_stmt = covering_node.ancestors().find(|node| {
|
||||
matches!(
|
||||
node,
|
||||
AnyNodeRef::StmtImport(_) | AnyNodeRef::StmtImportFrom(_)
|
||||
)
|
||||
});
|
||||
|
||||
match import_stmt {
|
||||
Some(AnyNodeRef::StmtImport(_)) => {
|
||||
// Regular import statement like "import x.y as z"
|
||||
|
||||
// Is the offset within the alias name (asname) part?
|
||||
if let Some(asname) = &alias.asname {
|
||||
if asname.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportModuleAlias { alias });
|
||||
}
|
||||
}
|
||||
|
||||
// Is the offset in the module name part?
|
||||
if alias.name.range.contains_inclusive(offset) {
|
||||
let full_name = alias.name.as_str();
|
||||
|
||||
if let Some((component_index, component_range)) =
|
||||
find_module_component(
|
||||
full_name,
|
||||
alias.name.range.start(),
|
||||
offset,
|
||||
)
|
||||
{
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_name.to_string(),
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Some(AnyNodeRef::StmtImportFrom(import_from)) => {
|
||||
// From import statement like "from x import y as z"
|
||||
|
||||
// Is the offset within the alias name (asname) part?
|
||||
if let Some(asname) = &alias.asname {
|
||||
if asname.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportSymbolAlias {
|
||||
alias,
|
||||
range: asname.range,
|
||||
import_from,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Is the offset in the original name part?
|
||||
if alias.name.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportSymbolAlias {
|
||||
alias,
|
||||
range: alias.name.range,
|
||||
import_from,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Some(AnyNodeRef::StmtImportFrom(from)) => {
|
||||
// Handle offset within module name in from import statements
|
||||
if let Some(module_expr) = &from.module {
|
||||
let full_module_name = module_expr.to_string();
|
||||
|
||||
if let Some((component_index, component_range)) = find_module_component(
|
||||
&full_module_name,
|
||||
module_expr.range.start(),
|
||||
offset,
|
||||
) {
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_module_name,
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
|
||||
Some(GotoTarget::ExceptVariable(handler))
|
||||
}
|
||||
Some(AnyNodeRef::Keyword(keyword)) => {
|
||||
// Find the containing call expression from the ancestor chain
|
||||
let call_expression = covering_node
|
||||
.ancestors()
|
||||
.find_map(ruff_python_ast::AnyNodeRef::expr_call)?;
|
||||
Some(GotoTarget::KeywordArgument {
|
||||
keyword,
|
||||
call_expression,
|
||||
})
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
|
||||
Some(GotoTarget::PatternMatchRest(mapping))
|
||||
}
|
||||
Some(AnyNodeRef::PatternKeyword(keyword)) => {
|
||||
Some(GotoTarget::PatternKeywordArgument(keyword))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchStar(star)) => {
|
||||
Some(GotoTarget::PatternMatchStarName(star))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
|
||||
Some(GotoTarget::PatternMatchAsName(as_pattern))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVar(var)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarName(var))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
|
||||
Some(GotoTarget::TypeParamParamSpecName(bound))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||
}
|
||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
||||
Some(GotoTarget::Expression(attribute.into()))
|
||||
}
|
||||
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||
None => None,
|
||||
Some(parent) => {
|
||||
tracing::debug!(
|
||||
"Missing `GoToTarget` for identifier with parent {:?}",
|
||||
parent.kind()
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for GotoTarget<'_> {
|
||||
@@ -590,7 +328,11 @@ fn convert_resolved_definitions_to_targets(
|
||||
}
|
||||
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
|
||||
// For file ranges, navigate to the specific range within the file
|
||||
crate::NavigationTarget::new(file_range.file(), file_range.range())
|
||||
crate::NavigationTarget {
|
||||
file: file_range.file(),
|
||||
focus_range: file_range.range(),
|
||||
full_range: file_range.range(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -633,7 +375,145 @@ pub(crate) fn find_goto_target(
|
||||
.find_first(|node| node.is_identifier() || node.is_expression())
|
||||
.ok()?;
|
||||
|
||||
GotoTarget::from_covering_node(&covering_node, offset)
|
||||
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
|
||||
|
||||
match covering_node.node() {
|
||||
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
|
||||
Some(AnyNodeRef::StmtFunctionDef(function)) => Some(GotoTarget::FunctionDef(function)),
|
||||
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
|
||||
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
|
||||
Some(AnyNodeRef::Alias(alias)) => {
|
||||
// Find the containing import statement to determine the type
|
||||
let import_stmt = covering_node.ancestors().find(|node| {
|
||||
matches!(
|
||||
node,
|
||||
AnyNodeRef::StmtImport(_) | AnyNodeRef::StmtImportFrom(_)
|
||||
)
|
||||
});
|
||||
|
||||
match import_stmt {
|
||||
Some(AnyNodeRef::StmtImport(_)) => {
|
||||
// Regular import statement like "import x.y as z"
|
||||
|
||||
// Is the offset within the alias name (asname) part?
|
||||
if let Some(asname) = &alias.asname {
|
||||
if asname.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportModuleAlias { alias });
|
||||
}
|
||||
}
|
||||
|
||||
// Is the offset in the module name part?
|
||||
if alias.name.range.contains_inclusive(offset) {
|
||||
let full_name = alias.name.as_str();
|
||||
|
||||
if let Some((component_index, component_range)) =
|
||||
find_module_component(full_name, alias.name.range.start(), offset)
|
||||
{
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_name.to_string(),
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Some(AnyNodeRef::StmtImportFrom(import_from)) => {
|
||||
// From import statement like "from x import y as z"
|
||||
|
||||
// Is the offset within the alias name (asname) part?
|
||||
if let Some(asname) = &alias.asname {
|
||||
if asname.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportSymbolAlias {
|
||||
alias,
|
||||
range: asname.range,
|
||||
import_from,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Is the offset in the original name part?
|
||||
if alias.name.range.contains_inclusive(offset) {
|
||||
return Some(GotoTarget::ImportSymbolAlias {
|
||||
alias,
|
||||
range: alias.name.range,
|
||||
import_from,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Some(AnyNodeRef::StmtImportFrom(from)) => {
|
||||
// Handle offset within module name in from import statements
|
||||
if let Some(module_expr) = &from.module {
|
||||
let full_module_name = module_expr.to_string();
|
||||
|
||||
if let Some((component_index, component_range)) =
|
||||
find_module_component(&full_module_name, module_expr.range.start(), offset)
|
||||
{
|
||||
return Some(GotoTarget::ImportModuleComponent {
|
||||
module_name: full_module_name,
|
||||
component_index,
|
||||
component_range,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
|
||||
Some(GotoTarget::ExceptVariable(handler))
|
||||
}
|
||||
Some(AnyNodeRef::Keyword(keyword)) => {
|
||||
// Find the containing call expression from the ancestor chain
|
||||
let call_expression = covering_node
|
||||
.ancestors()
|
||||
.find_map(ruff_python_ast::AnyNodeRef::expr_call)?;
|
||||
Some(GotoTarget::KeywordArgument {
|
||||
keyword,
|
||||
call_expression,
|
||||
})
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
|
||||
Some(GotoTarget::PatternMatchRest(mapping))
|
||||
}
|
||||
Some(AnyNodeRef::PatternKeyword(keyword)) => {
|
||||
Some(GotoTarget::PatternKeywordArgument(keyword))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchStar(star)) => {
|
||||
Some(GotoTarget::PatternMatchStarName(star))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
|
||||
Some(GotoTarget::PatternMatchAsName(as_pattern))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVar(var)) => Some(GotoTarget::TypeParamTypeVarName(var)),
|
||||
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
|
||||
Some(GotoTarget::TypeParamParamSpecName(bound))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||
}
|
||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
||||
Some(GotoTarget::Expression(attribute.into()))
|
||||
}
|
||||
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||
None => None,
|
||||
Some(parent) => {
|
||||
tracing::debug!(
|
||||
"Missing `GoToTarget` for identifier with parent {:?}",
|
||||
parent.kind()
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to resolve a module name and create a navigation target.
|
||||
@@ -645,10 +525,12 @@ fn resolve_module_to_navigation_target(
|
||||
|
||||
if let Some(module_name) = ModuleName::new(module_name_str) {
|
||||
if let Some(resolved_module) = resolve_module(db, &module_name) {
|
||||
if let Some(module_file) = resolved_module.file(db) {
|
||||
return Some(crate::NavigationTargets::single(
|
||||
crate::NavigationTarget::new(module_file, TextRange::default()),
|
||||
));
|
||||
if let Some(module_file) = resolved_module.file() {
|
||||
return Some(crate::NavigationTargets::single(crate::NavigationTarget {
|
||||
file: module_file,
|
||||
focus_range: TextRange::default(),
|
||||
full_range: TextRange::default(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -1350,7 +1349,7 @@ class MyClass:
|
||||
|
||||
impl IntoDiagnostic for GotoDeclarationDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
|
||||
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
@@ -29,569 +29,3 @@ pub fn goto_definition(
|
||||
value: definition_targets,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::tests::{CursorTest, IntoDiagnostic};
|
||||
use crate::{NavigationTarget, goto_definition};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// goto-definition on a module should go to the .py not the .pyi
|
||||
///
|
||||
/// TODO: this currently doesn't work right! This is especially surprising
|
||||
/// because [`goto_definition_stub_map_module_ref`] works fine.
|
||||
#[test]
|
||||
fn goto_definition_stub_map_module_import() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymo<CURSOR>dule import my_function
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.pyi:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | def my_function(): ...
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:6
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
| ^^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a module ref should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_module_ref() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
import mymodule
|
||||
x = mymo<CURSOR>dule
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | def my_function():
|
||||
3 | return "hello"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | import mymodule
|
||||
3 | x = mymodule
|
||||
| ^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a function call should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_function() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import my_function
|
||||
print(my_func<CURSOR>tion())
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
|
||||
def other_function():
|
||||
return "other"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_function(): ...
|
||||
|
||||
def other_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:5
|
||||
|
|
||||
2 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
3 | return "hello"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
3 | print(my_function())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a function that's redefined many times in the impl .py
|
||||
///
|
||||
/// Currently this yields all instances. There's an argument for only yielding
|
||||
/// the final one since that's the one "exported" but, this is consistent for
|
||||
/// how we do file-local goto-definition.
|
||||
#[test]
|
||||
fn goto_definition_stub_map_function_redefine() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import my_function
|
||||
print(my_func<CURSOR>tion())
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def my_function():
|
||||
return "hello"
|
||||
|
||||
def my_function():
|
||||
return "hello again"
|
||||
|
||||
def my_function():
|
||||
return "we can't keep doing this"
|
||||
|
||||
def other_function():
|
||||
return "other"
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
def my_function(): ...
|
||||
|
||||
def other_function(): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:5
|
||||
|
|
||||
2 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
3 | return "hello"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
3 | print(my_function())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:5:5
|
||||
|
|
||||
3 | return "hello"
|
||||
4 |
|
||||
5 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
6 | return "hello again"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
3 | print(my_function())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:8:5
|
||||
|
|
||||
6 | return "hello again"
|
||||
7 |
|
||||
8 | def my_function():
|
||||
| ^^^^^^^^^^^
|
||||
9 | return "we can't keep doing this"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from mymodule import my_function
|
||||
3 | print(my_function())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a class ref go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_ref() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyClass
|
||||
x = MyC<CURSOR>lass
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val: bool): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^^^
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
3 | x = MyClass
|
||||
| ^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a class init should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_init() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyClass
|
||||
x = MyCl<CURSOR>ass(0)
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val: bool): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^^^
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
3 | x = MyClass(0)
|
||||
| ^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a class method should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_method() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyClass
|
||||
x = MyClass(0)
|
||||
x.act<CURSOR>ion()
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
def action(self):
|
||||
print(self.val)
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val: bool): ...
|
||||
def action(self): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:5:9
|
||||
|
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
5 | def action(self):
|
||||
| ^^^^^^
|
||||
6 | print(self.val)
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:1
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
3 | x = MyClass(0)
|
||||
4 | x.action()
|
||||
| ^^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// goto-definition on a class function should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_function() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyClass
|
||||
x = MyClass.act<CURSOR>ion()
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
def action():
|
||||
print("hi!")
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val):
|
||||
self.val = val + 1
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass:
|
||||
def __init__(self, val: bool): ...
|
||||
def action(): ...
|
||||
|
||||
class MyOtherClass:
|
||||
def __init__(self, val: bool): ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:5:9
|
||||
|
|
||||
3 | def __init__(self, val):
|
||||
4 | self.val = val
|
||||
5 | def action():
|
||||
| ^^^^^^
|
||||
6 | print("hi!")
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
3 | x = MyClass.action()
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a class import should go to the .py not the .pyi
|
||||
#[test]
|
||||
fn goto_definition_stub_map_class_import() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from mymodule import MyC<CURSOR>lass
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
class MyClass: ...
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"mymodule.pyi",
|
||||
r#"
|
||||
class MyClass: ...
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> mymodule.py:2:7
|
||||
|
|
||||
2 | class MyClass: ...
|
||||
| ^^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:22
|
||||
|
|
||||
2 | from mymodule import MyClass
|
||||
| ^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_definition(&self) -> String {
|
||||
let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset)
|
||||
else {
|
||||
return "No goto target found".to_string();
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return "No definitions found".to_string();
|
||||
}
|
||||
|
||||
let source = targets.range;
|
||||
self.render_diagnostics(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| GotoDefinitionDiagnostic::new(source, &target)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoDefinitionDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
}
|
||||
|
||||
impl GotoDefinitionDiagnostic {
|
||||
fn new(source: FileRange, target: &NavigationTarget) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target: FileRange::new(target.file(), target.focus_range()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for GotoDefinitionDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
let mut main = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("goto-definition")),
|
||||
Severity::Info,
|
||||
"Definition".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.target.file()).with_range(self.target.range()),
|
||||
));
|
||||
main.sub(source);
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -641,7 +640,7 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
|
||||
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
@@ -156,8 +156,9 @@ mod tests {
|
||||
};
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPathBuf};
|
||||
use ty_project::ProjectMetadata;
|
||||
use ty_python_semantic::{
|
||||
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
|
||||
};
|
||||
@@ -166,10 +167,7 @@ mod tests {
|
||||
const START: &str = "<START>";
|
||||
const END: &str = "<END>";
|
||||
|
||||
let mut db = ty_project::TestDb::new(ProjectMetadata::new(
|
||||
"test".into(),
|
||||
SystemPathBuf::from("/"),
|
||||
));
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let start = source.find(START);
|
||||
let end = source
|
||||
@@ -207,7 +205,7 @@ mod tests {
|
||||
}
|
||||
|
||||
pub(super) struct InlayHintTest {
|
||||
pub(super) db: ty_project::TestDb,
|
||||
pub(super) db: TestDb,
|
||||
pub(super) file: File,
|
||||
pub(super) range: TextRange,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod completion;
|
||||
mod db;
|
||||
mod docstring;
|
||||
mod find_node;
|
||||
mod goto;
|
||||
@@ -8,18 +9,17 @@ mod goto_type_definition;
|
||||
mod hover;
|
||||
mod inlay_hints;
|
||||
mod markup;
|
||||
mod references;
|
||||
mod semantic_tokens;
|
||||
mod signature_help;
|
||||
mod stub_mapping;
|
||||
|
||||
pub use completion::completion;
|
||||
pub use db::Db;
|
||||
pub use docstring::get_parameter_documentation;
|
||||
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
|
||||
pub use hover::hover;
|
||||
pub use inlay_hints::inlay_hints;
|
||||
pub use markup::MarkupKind;
|
||||
pub use references::references;
|
||||
pub use semantic_tokens::{
|
||||
SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, semantic_tokens,
|
||||
};
|
||||
@@ -29,7 +29,6 @@ use ruff_db::files::{File, FileRange};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use ty_project::Db;
|
||||
use ty_python_semantic::types::{Type, TypeDefinition};
|
||||
|
||||
/// Information associated with a text range.
|
||||
@@ -88,15 +87,6 @@ pub struct NavigationTarget {
|
||||
}
|
||||
|
||||
impl NavigationTarget {
|
||||
/// Creates a new `NavigationTarget` where the focus and full range are identical.
|
||||
pub fn new(file: File, range: TextRange) -> Self {
|
||||
Self {
|
||||
file,
|
||||
focus_range: range,
|
||||
full_range: range,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
@@ -221,13 +211,13 @@ impl HasNavigationTargets for TypeDefinition<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use ruff_db::Db;
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
|
||||
use ruff_db::files::{File, system_path_to_file};
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_text_size::TextSize;
|
||||
use ty_project::ProjectMetadata;
|
||||
use ty_python_semantic::{
|
||||
Program, ProgramSettings, PythonPlatform, PythonVersionWithSource, SearchPathSettings,
|
||||
};
|
||||
@@ -241,7 +231,7 @@ mod tests {
|
||||
}
|
||||
|
||||
pub(super) struct CursorTest {
|
||||
pub(super) db: ty_project::TestDb,
|
||||
pub(super) db: TestDb,
|
||||
pub(super) cursor: Cursor,
|
||||
_insta_settings_guard: SettingsBindDropGuard,
|
||||
}
|
||||
@@ -296,13 +286,8 @@ mod tests {
|
||||
|
||||
impl CursorTestBuilder {
|
||||
pub(super) fn build(&self) -> CursorTest {
|
||||
let mut db = ty_project::TestDb::new(ProjectMetadata::new(
|
||||
"test".into(),
|
||||
SystemPathBuf::from("/"),
|
||||
));
|
||||
|
||||
let mut db = TestDb::new();
|
||||
let mut cursor: Option<Cursor> = None;
|
||||
|
||||
for &Source {
|
||||
ref path,
|
||||
ref contents,
|
||||
@@ -311,19 +296,19 @@ mod tests {
|
||||
{
|
||||
db.write_file(path, contents)
|
||||
.expect("write to memory file system to be successful");
|
||||
let Some(offset) = cursor_offset else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let file = system_path_to_file(&db, path).expect("newly written file to existing");
|
||||
|
||||
if let Some(offset) = cursor_offset {
|
||||
// This assert should generally never trip, since
|
||||
// we have an assert on `CursorTestBuilder::source`
|
||||
// to ensure we never have more than one marker.
|
||||
assert!(
|
||||
cursor.is_none(),
|
||||
"found more than one source that contains `<CURSOR>`"
|
||||
);
|
||||
cursor = Some(Cursor { file, offset });
|
||||
}
|
||||
// This assert should generally never trip, since
|
||||
// we have an assert on `CursorTestBuilder::source`
|
||||
// to ensure we never have more than one marker.
|
||||
assert!(
|
||||
cursor.is_none(),
|
||||
"found more than one source that contains `<CURSOR>`"
|
||||
);
|
||||
cursor = Some(Cursor { file, offset });
|
||||
}
|
||||
|
||||
let search_paths = SearchPathSettings::new(vec![SystemPathBuf::from("/")])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -664,26 +664,6 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Stmt::Nonlocal(nonlocal_stmt) => {
|
||||
// Handle nonlocal statements - classify identifiers as variables
|
||||
for identifier in &nonlocal_stmt.names {
|
||||
self.add_token(
|
||||
identifier.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::Stmt::Global(global_stmt) => {
|
||||
// Handle global statements - classify identifiers as variables
|
||||
for identifier in &global_stmt.names {
|
||||
self.add_token(
|
||||
identifier.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// For all other statement types, let the default visitor handle them
|
||||
walk_stmt(self, stmt);
|
||||
@@ -851,71 +831,6 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_except_handler(&mut self, except_handler: &ast::ExceptHandler) {
|
||||
match except_handler {
|
||||
ast::ExceptHandler::ExceptHandler(handler) => {
|
||||
// Visit the exception type expression if present
|
||||
if let Some(type_expr) = &handler.type_ {
|
||||
self.visit_expr(type_expr);
|
||||
}
|
||||
|
||||
// Handle the exception variable name (after "as")
|
||||
if let Some(name) = &handler.name {
|
||||
self.add_token(
|
||||
name.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
|
||||
// Visit the handler body
|
||||
self.visit_body(&handler.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &ast::Pattern) {
|
||||
match pattern {
|
||||
ast::Pattern::MatchAs(pattern_as) => {
|
||||
// Visit the nested pattern first to maintain source order
|
||||
if let Some(nested_pattern) = &pattern_as.pattern {
|
||||
self.visit_pattern(nested_pattern);
|
||||
}
|
||||
|
||||
// Now add the "as" variable name token
|
||||
if let Some(name) = &pattern_as.name {
|
||||
self.add_token(
|
||||
name.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::Pattern::MatchMapping(pattern_mapping) => {
|
||||
// Visit keys and patterns in source order by interleaving them
|
||||
for (key, nested_pattern) in
|
||||
pattern_mapping.keys.iter().zip(&pattern_mapping.patterns)
|
||||
{
|
||||
self.visit_expr(key);
|
||||
self.visit_pattern(nested_pattern);
|
||||
}
|
||||
|
||||
// Handle the rest parameter (after "**") - this comes last
|
||||
if let Some(rest_name) = &pattern_mapping.rest {
|
||||
self.add_token(
|
||||
rest_name.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// For all other pattern types, use the default walker
|
||||
ruff_python_ast::visitor::source_order::walk_pattern(self, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2027,200 +1942,4 @@ complex_fstring = f"User: {name.upper()}, Count: {len(data)}, Hex: {value:x}"<CU
|
||||
"x" @ 414..415: String
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonlocal_and_global_statements() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
x = "global_value"
|
||||
y = "another_global"
|
||||
|
||||
def outer():
|
||||
x = "outer_value"
|
||||
z = "outer_local"
|
||||
|
||||
def inner():
|
||||
nonlocal x, z # These should be variable tokens
|
||||
global y # This should be a variable token
|
||||
x = "modified"
|
||||
y = "modified_global"
|
||||
z = "modified_local"
|
||||
|
||||
def deeper():
|
||||
nonlocal x # Variable token
|
||||
global y, x # Both should be variable tokens
|
||||
return x + y
|
||||
|
||||
return deeper
|
||||
|
||||
return inner<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
|
||||
|
||||
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
|
||||
"x" @ 1..2: Variable
|
||||
"/"global_value/"" @ 5..19: String
|
||||
"y" @ 20..21: Variable
|
||||
"/"another_global/"" @ 24..40: String
|
||||
"outer" @ 46..51: Function [definition]
|
||||
"x" @ 59..60: Variable
|
||||
"/"outer_value/"" @ 63..76: String
|
||||
"z" @ 81..82: Variable
|
||||
"/"outer_local/"" @ 85..98: String
|
||||
"inner" @ 112..117: Function [definition]
|
||||
"x" @ 138..139: Variable
|
||||
"z" @ 141..142: Variable
|
||||
"y" @ 193..194: Variable
|
||||
"x" @ 243..244: Variable
|
||||
"/"modified/"" @ 247..257: String
|
||||
"y" @ 266..267: Variable
|
||||
"/"modified_global/"" @ 270..287: String
|
||||
"z" @ 296..297: Variable
|
||||
"/"modified_local/"" @ 300..316: String
|
||||
"deeper" @ 338..344: Function [definition]
|
||||
"x" @ 369..370: Variable
|
||||
"y" @ 410..411: Variable
|
||||
"x" @ 413..414: Variable
|
||||
"x" @ 469..470: Variable
|
||||
"y" @ 473..474: Variable
|
||||
"deeper" @ 499..505: Function
|
||||
"inner" @ 522..527: Function
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonlocal_global_edge_cases() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
# Single variable statements
|
||||
def test():
|
||||
global x
|
||||
nonlocal y
|
||||
|
||||
# Multiple variables in one statement
|
||||
global a, b, c
|
||||
nonlocal d, e, f
|
||||
|
||||
return x + y + a + b + c + d + e + f<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
|
||||
|
||||
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
|
||||
"test" @ 34..38: Function [definition]
|
||||
"x" @ 53..54: Variable
|
||||
"y" @ 68..69: Variable
|
||||
"a" @ 128..129: Variable
|
||||
"b" @ 131..132: Variable
|
||||
"c" @ 134..135: Variable
|
||||
"d" @ 149..150: Variable
|
||||
"e" @ 152..153: Variable
|
||||
"f" @ 155..156: Variable
|
||||
"x" @ 173..174: Variable
|
||||
"y" @ 177..178: Variable
|
||||
"a" @ 181..182: Variable
|
||||
"b" @ 185..186: Variable
|
||||
"c" @ 189..190: Variable
|
||||
"d" @ 193..194: Variable
|
||||
"e" @ 197..198: Variable
|
||||
"f" @ 201..202: Variable
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_matching() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def process_data(data):
|
||||
match data:
|
||||
case {"name": name, "age": age, **rest} as person:
|
||||
print(f"Person {name}, age {age}, extra: {rest}")
|
||||
return person
|
||||
case [first, *remaining] as sequence:
|
||||
print(f"First: {first}, remaining: {remaining}")
|
||||
return sequence
|
||||
case value as fallback:
|
||||
print(f"Fallback: {fallback}")
|
||||
return fallback<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
|
||||
|
||||
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
|
||||
"process_data" @ 5..17: Function [definition]
|
||||
"data" @ 18..22: Parameter
|
||||
"data" @ 35..39: Variable
|
||||
"/"name/"" @ 55..61: String
|
||||
"name" @ 63..67: Variable
|
||||
"/"age/"" @ 69..74: String
|
||||
"age" @ 76..79: Variable
|
||||
"rest" @ 83..87: Variable
|
||||
"person" @ 92..98: Variable
|
||||
"print" @ 112..117: Function
|
||||
"Person " @ 120..127: String
|
||||
"name" @ 128..132: Variable
|
||||
", age " @ 133..139: String
|
||||
"age" @ 140..143: Variable
|
||||
", extra: " @ 144..153: String
|
||||
"rest" @ 154..158: Variable
|
||||
"person" @ 181..187: Variable
|
||||
"first" @ 202..207: Variable
|
||||
"sequence" @ 224..232: Variable
|
||||
"print" @ 246..251: Function
|
||||
"First: " @ 254..261: String
|
||||
"first" @ 262..267: Variable
|
||||
", remaining: " @ 268..281: String
|
||||
"remaining" @ 282..291: Variable
|
||||
"sequence" @ 314..322: Variable
|
||||
"value" @ 336..341: Variable
|
||||
"fallback" @ 345..353: Variable
|
||||
"print" @ 367..372: Function
|
||||
"Fallback: " @ 375..385: String
|
||||
"fallback" @ 386..394: Variable
|
||||
"fallback" @ 417..425: Variable
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exception_handlers() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
try:
|
||||
x = 1 / 0
|
||||
except ValueError as ve:
|
||||
print(ve)
|
||||
except (TypeError, RuntimeError) as re:
|
||||
print(re)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
pass<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
|
||||
|
||||
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
|
||||
"x" @ 10..11: Variable
|
||||
"1" @ 14..15: Number
|
||||
"0" @ 18..19: Number
|
||||
"ValueError" @ 27..37: Class
|
||||
"ve" @ 41..43: Variable
|
||||
"print" @ 49..54: Function
|
||||
"ve" @ 55..57: Variable
|
||||
"TypeError" @ 67..76: Class
|
||||
"RuntimeError" @ 78..90: Class
|
||||
"re" @ 95..97: Variable
|
||||
"print" @ 103..108: Function
|
||||
"re" @ 109..111: Variable
|
||||
"Exception" @ 120..129: Class
|
||||
"e" @ 133..134: Variable
|
||||
"print" @ 140..145: Function
|
||||
"e" @ 146..147: Variable
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ fn create_signature_details_from_call_signature_details(
|
||||
details
|
||||
.argument_to_parameter_mapping
|
||||
.get(current_arg_index)
|
||||
.and_then(|mapping| mapping.parameters.first().copied())
|
||||
.and_then(|¶m_index| param_index)
|
||||
.or({
|
||||
// If we can't find a mapping for this argument, but we have a current
|
||||
// argument index, use that as the active parameter if it's within bounds.
|
||||
@@ -242,11 +242,11 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]
|
||||
|
||||
// First, try to find a signature where all arguments have valid parameter mappings.
|
||||
let perfect_match = signature_details.iter().position(|details| {
|
||||
// Check if all arguments have valid parameter mappings.
|
||||
// Check if all arguments have valid parameter mappings (i.e., are not None).
|
||||
details
|
||||
.argument_to_parameter_mapping
|
||||
.iter()
|
||||
.all(|mapping| mapping.matched)
|
||||
.all(Option::is_some)
|
||||
});
|
||||
|
||||
if let Some(index) = perfect_match {
|
||||
@@ -261,7 +261,7 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]
|
||||
details
|
||||
.argument_to_parameter_mapping
|
||||
.iter()
|
||||
.filter(|mapping| mapping.matched)
|
||||
.filter(|mapping| mapping.is_some())
|
||||
.count()
|
||||
})?;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use itertools::Either;
|
||||
use ty_python_semantic::{ResolvedDefinition, map_stub_definition};
|
||||
use ty_python_semantic::ResolvedDefinition;
|
||||
|
||||
/// Maps `ResolvedDefinitions` from stub files to corresponding definitions in source files.
|
||||
///
|
||||
@@ -8,10 +7,12 @@ use ty_python_semantic::{ResolvedDefinition, map_stub_definition};
|
||||
/// other language server providers (like hover, completion, and signature help) to find
|
||||
/// docstrings for functions that resolve to stubs.
|
||||
pub(crate) struct StubMapper<'db> {
|
||||
#[allow(dead_code)] // Will be used when implementation is added
|
||||
db: &'db dyn crate::Db,
|
||||
}
|
||||
|
||||
impl<'db> StubMapper<'db> {
|
||||
#[allow(dead_code)] // Will be used in the future
|
||||
pub(crate) fn new(db: &'db dyn crate::Db) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
@@ -20,14 +21,15 @@ impl<'db> StubMapper<'db> {
|
||||
///
|
||||
/// If the definition is in a stub file and a corresponding source file definition exists,
|
||||
/// returns the source file definition(s). Otherwise, returns the original definition.
|
||||
#[allow(dead_code)] // Will be used when implementation is added
|
||||
#[allow(clippy::unused_self)] // Will use self when implementation is added
|
||||
pub(crate) fn map_definition(
|
||||
&self,
|
||||
def: ResolvedDefinition<'db>,
|
||||
) -> impl Iterator<Item = ResolvedDefinition<'db>> {
|
||||
if let Some(definitions) = map_stub_definition(self.db, &def) {
|
||||
return Either::Left(definitions.into_iter());
|
||||
}
|
||||
Either::Right(std::iter::once(def))
|
||||
) -> Vec<ResolvedDefinition<'db>> {
|
||||
// TODO: Implement stub-to-source mapping logic
|
||||
// For now, just return the original definition
|
||||
vec![def]
|
||||
}
|
||||
|
||||
/// Map multiple `ResolvedDefinitions`, applying stub-to-source mapping to each.
|
||||
|
||||
@@ -19,6 +19,7 @@ ruff_options_metadata = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["serde"] }
|
||||
ruff_python_formatter = { workspace = true, optional = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ty_ide = { workspace = true }
|
||||
ty_python_semantic = { workspace = true, features = ["serde"] }
|
||||
ty_vendored = { workspace = true }
|
||||
|
||||
@@ -52,7 +53,6 @@ deflate = ["ty_vendored/deflate"]
|
||||
schemars = ["dep:schemars", "ruff_db/schemars", "ty_python_semantic/schemars"]
|
||||
zstd = ["ty_vendored/zstd"]
|
||||
format = ["ruff_python_formatter"]
|
||||
testing = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -14,6 +14,7 @@ use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Event, Setter};
|
||||
use ty_ide::Db as IdeDb;
|
||||
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use ty_python_semantic::{Db as SemanticDb, Program};
|
||||
|
||||
@@ -403,6 +404,9 @@ impl SalsaMemoryDump {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl IdeDb for ProjectDatabase {}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for ProjectDatabase {
|
||||
fn should_check_file(&self, file: File) -> bool {
|
||||
@@ -464,7 +468,7 @@ mod format {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "testing"))]
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -483,7 +487,7 @@ pub(crate) mod tests {
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub struct TestDb {
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
events: Events,
|
||||
files: Files,
|
||||
@@ -493,7 +497,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub fn new(project: ProjectMetadata) -> Self {
|
||||
pub(crate) fn new(project: ProjectMetadata) -> Self {
|
||||
let events = Events::default();
|
||||
let mut db = Self {
|
||||
storage: salsa::Storage::new(Some(Box::new({
|
||||
@@ -518,7 +522,7 @@ pub(crate) mod tests {
|
||||
|
||||
impl TestDb {
|
||||
/// Takes the salsa events.
|
||||
pub fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let mut events = self.events.lock().unwrap();
|
||||
|
||||
std::mem::take(&mut *events)
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
use crate::glob::{GlobFilterCheckMode, IncludeResult};
|
||||
use crate::metadata::options::{OptionDiagnostic, ToSettingsError};
|
||||
use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
|
||||
#[cfg(feature = "testing")]
|
||||
pub use db::tests::TestDb;
|
||||
pub use db::{ChangeResult, CheckMode, Db, ProjectDatabase, SalsaMemoryDump};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
use metadata::settings::Settings;
|
||||
pub use metadata::{ProjectMetadata, ProjectMetadataError};
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, SubDiagnosticSeverity,
|
||||
};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic};
|
||||
use ruff_db::files::{File, FileRootKind};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{SourceTextError, source_text};
|
||||
@@ -678,17 +674,14 @@ where
|
||||
|
||||
let mut diagnostic = Diagnostic::new(DiagnosticId::Panic, Severity::Fatal, message);
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::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(Severity::Info, report_message));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
report_message,
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
format!(
|
||||
"Platform: {os} {arch}",
|
||||
os = std::env::consts::OS,
|
||||
@@ -697,13 +690,13 @@ where
|
||||
));
|
||||
if let Some(version) = ruff_db::program_version() {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
format!("Version: {version}"),
|
||||
));
|
||||
}
|
||||
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
format!(
|
||||
"Args: {args:?}",
|
||||
args = std::env::args().collect::<Vec<_>>()
|
||||
@@ -714,13 +707,13 @@ where
|
||||
match backtrace.status() {
|
||||
BacktraceStatus::Disabled => {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information",
|
||||
));
|
||||
}
|
||||
BacktraceStatus::Captured => {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
format!("Backtrace:\n{backtrace}"),
|
||||
));
|
||||
}
|
||||
@@ -730,10 +723,7 @@ where
|
||||
|
||||
if let Some(backtrace) = error.salsa_backtrace {
|
||||
salsa::attach(db, || {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
backtrace.to_string(),
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(Severity::Info, backtrace.to_string()));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use ordermap::OrderMap;
|
||||
use ruff_db::RustDoc;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, Severity,
|
||||
Span, SubDiagnostic, SubDiagnosticSeverity,
|
||||
Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
@@ -318,7 +318,7 @@ impl Options {
|
||||
|
||||
if self.environment.or_default().root.is_some() {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"The `src.root` setting was ignored in favor of the `environment.root` setting",
|
||||
));
|
||||
}
|
||||
@@ -811,7 +811,7 @@ fn build_include_filter(
|
||||
Severity::Warning,
|
||||
)
|
||||
.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"Remove the `include` option to match all files or add a pattern to match specific files",
|
||||
));
|
||||
|
||||
@@ -853,13 +853,13 @@ fn build_include_filter(
|
||||
))
|
||||
} else {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
format!("The pattern is defined in the `{}` option in your configuration file", context.include_name()),
|
||||
))
|
||||
}
|
||||
}
|
||||
ValueSource::Cli => diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"The pattern was specified on the CLI",
|
||||
)),
|
||||
ValueSource::PythonVSCodeExtension => unreachable!("Can't configure includes from the Python VSCode extension"),
|
||||
@@ -883,7 +883,7 @@ fn build_include_filter(
|
||||
Severity::Error,
|
||||
);
|
||||
Box::new(diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"Please open an issue on the ty repository and share the patterns that caused the error.",
|
||||
)))
|
||||
})
|
||||
@@ -936,13 +936,13 @@ fn build_exclude_filter(
|
||||
))
|
||||
} else {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
format!("The pattern is defined in the `{}` option in your configuration file", context.exclude_name()),
|
||||
))
|
||||
}
|
||||
}
|
||||
ValueSource::Cli => diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"The pattern was specified on the CLI",
|
||||
)),
|
||||
ValueSource::PythonVSCodeExtension => unreachable!(
|
||||
@@ -960,7 +960,7 @@ fn build_exclude_filter(
|
||||
Severity::Error,
|
||||
);
|
||||
Box::new(diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"Please open an issue on the ty repository and share the patterns that caused the error.",
|
||||
)))
|
||||
})
|
||||
@@ -1197,26 +1197,26 @@ impl RangedValue<OverrideOptions> {
|
||||
|
||||
diagnostic = if self.rules.is_none() {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"It has no `rules` table",
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"Add a `[overrides.rules]` table...",
|
||||
))
|
||||
} else {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"The rules table is empty",
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"Add a rule to `[overrides.rules]` to override specific rules...",
|
||||
))
|
||||
};
|
||||
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"or remove the `[[overrides]]` section if there's nothing to override",
|
||||
));
|
||||
|
||||
@@ -1251,23 +1251,23 @@ impl RangedValue<OverrideOptions> {
|
||||
|
||||
diagnostic = if self.exclude.is_none() {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"It has no `include` or `exclude` option restricting the files",
|
||||
))
|
||||
} else {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"It has no `include` option and `exclude` is empty",
|
||||
))
|
||||
};
|
||||
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"Restrict the files by adding a pattern to `include` or `exclude`...",
|
||||
));
|
||||
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
Severity::Info,
|
||||
"or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files",
|
||||
));
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ ty_static = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
bitvec = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
@@ -36,7 +35,6 @@ indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true, features = ["compact_str"] }
|
||||
thin-vec = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
@@ -4,7 +4,7 @@ References:
|
||||
|
||||
- <https://typing.python.org/en/latest/spec/callables.html#callable>
|
||||
|
||||
Note that `typing.Callable` is deprecated at runtime, in favor of `collections.abc.Callable` (see:
|
||||
Note that `typing.Callable` is deprecated at runtime, in favour of `collections.abc.Callable` (see:
|
||||
<https://docs.python.org/3/library/typing.html#deprecated-aliases>). However, removal of
|
||||
`typing.Callable` is not currently planned, and the canonical location of the stub for the symbol in
|
||||
typeshed is still `typing.pyi`.
|
||||
|
||||
@@ -25,9 +25,6 @@ class Color(Enum):
|
||||
|
||||
b1: Literal[Color.RED]
|
||||
|
||||
MissingT = Enum("MissingT", {"MISSING": "MISSING"})
|
||||
b2: Literal[MissingT.MISSING]
|
||||
|
||||
def f():
|
||||
reveal_type(mode) # revealed: Literal["w", "r"]
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
@@ -54,16 +51,6 @@ invalid4: Literal[
|
||||
hello, # error: [invalid-type-form]
|
||||
(1, 2, 3), # error: [invalid-type-form]
|
||||
]
|
||||
|
||||
class NotAnEnum:
|
||||
x: int = 1
|
||||
|
||||
# error: [invalid-type-form]
|
||||
invalid5: Literal[NotAnEnum.x]
|
||||
|
||||
a_list: list[int] = [1, 2, 3]
|
||||
# error: [invalid-type-form]
|
||||
invalid6: Literal[a_list[0]]
|
||||
```
|
||||
|
||||
## Shortening unions of literals
|
||||
|
||||
@@ -72,7 +72,7 @@ def f(x: Union) -> None:
|
||||
|
||||
## Implicit type aliases using new-style unions
|
||||
|
||||
We don't recognize these as type aliases yet, but we also don't emit false-positive diagnostics if
|
||||
We don't recognise these as type aliases yet, but we also don't emit false-positive diagnostics if
|
||||
you use them in type expressions:
|
||||
|
||||
```toml
|
||||
|
||||
@@ -2355,13 +2355,12 @@ import enum
|
||||
|
||||
reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Unknown]
|
||||
|
||||
class Answer(enum.Enum):
|
||||
NO = 0
|
||||
YES = 1
|
||||
class Foo(enum.Enum):
|
||||
BAR = 1
|
||||
|
||||
reveal_type(Answer.NO) # revealed: Literal[Answer.NO]
|
||||
reveal_type(Answer.NO.value) # revealed: Any
|
||||
reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown]
|
||||
reveal_type(Foo.BAR) # revealed: Literal[Foo.BAR]
|
||||
reveal_type(Foo.BAR.value) # revealed: Any
|
||||
reveal_type(Foo.__members__) # revealed: MappingProxyType[str, Unknown]
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
@@ -105,59 +105,3 @@ str("Müsli", "utf-8")
|
||||
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[b"utf-8"]`"
|
||||
str(b"M\xc3\xbcsli", b"utf-8")
|
||||
```
|
||||
|
||||
## Calls to `isinstance`
|
||||
|
||||
We infer `Literal[True]` for a limited set of cases where we can be sure that the answer is correct,
|
||||
but fall back to `bool` otherwise.
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from types import FunctionType
|
||||
|
||||
class Answer(Enum):
|
||||
NO = 0
|
||||
YES = 1
|
||||
|
||||
reveal_type(isinstance(True, bool)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(True, int)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(True, object)) # revealed: Literal[True]
|
||||
reveal_type(isinstance("", str)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(1, int)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(b"", bytes)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(Answer.NO, Answer)) # revealed: Literal[True]
|
||||
|
||||
reveal_type(isinstance((1, 2), tuple)) # revealed: Literal[True]
|
||||
|
||||
def f(): ...
|
||||
|
||||
reveal_type(isinstance(f, FunctionType)) # revealed: Literal[True]
|
||||
|
||||
reveal_type(isinstance("", int)) # revealed: bool
|
||||
|
||||
class A: ...
|
||||
class SubclassOfA(A): ...
|
||||
class B: ...
|
||||
|
||||
reveal_type(isinstance(A, type)) # revealed: Literal[True]
|
||||
|
||||
a = A()
|
||||
|
||||
reveal_type(isinstance(a, A)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(a, object)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(a, SubclassOfA)) # revealed: bool
|
||||
reveal_type(isinstance(a, B)) # revealed: bool
|
||||
|
||||
s = SubclassOfA()
|
||||
reveal_type(isinstance(s, SubclassOfA)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(s, A)) # revealed: Literal[True]
|
||||
|
||||
def _(x: A | B):
|
||||
reveal_type(isinstance(x, A)) # revealed: bool
|
||||
|
||||
if isinstance(x, A):
|
||||
pass
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
reveal_type(isinstance(x, B)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -69,321 +69,6 @@ def _(flag: bool):
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
## Splatted arguments
|
||||
|
||||
### Unknown argument length
|
||||
|
||||
```py
|
||||
def takes_zero() -> None: ...
|
||||
def takes_one(x: int) -> None: ...
|
||||
def takes_two(x: int, y: int) -> None: ...
|
||||
def takes_two_positional_only(x: int, y: int, /) -> None: ...
|
||||
def takes_two_different(x: int, y: str) -> None: ...
|
||||
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
|
||||
def takes_at_least_zero(*args) -> None: ...
|
||||
def takes_at_least_one(x: int, *args) -> None: ...
|
||||
def takes_at_least_two(x: int, y: int, *args) -> None: ...
|
||||
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(args: list[int]) -> None:
|
||||
takes_zero(*args)
|
||||
takes_one(*args)
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, ...]) -> None:
|
||||
takes_zero(*args)
|
||||
takes_one(*args)
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
```
|
||||
|
||||
### Fixed-length tuple argument
|
||||
|
||||
```py
|
||||
def takes_zero() -> None: ...
|
||||
def takes_one(x: int) -> None: ...
|
||||
def takes_two(x: int, y: int) -> None: ...
|
||||
def takes_two_positional_only(x: int, y: int, /) -> None: ...
|
||||
def takes_two_different(x: int, y: str) -> None: ...
|
||||
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
|
||||
def takes_at_least_zero(*args) -> None: ...
|
||||
def takes_at_least_one(x: int, *args) -> None: ...
|
||||
def takes_at_least_two(x: int, y: int, *args) -> None: ...
|
||||
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(args: tuple[int]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args)
|
||||
takes_two(*args) # error: [missing-argument]
|
||||
takes_two_positional_only(*args) # error: [missing-argument]
|
||||
takes_two_different(*args) # error: [missing-argument]
|
||||
takes_two_different_positional_only(*args) # error: [missing-argument]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [missing-argument]
|
||||
takes_at_least_two_positional_only(*args) # error: [missing-argument]
|
||||
|
||||
def _(args: tuple[int, int]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, str]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args) # error: [invalid-argument-type]
|
||||
takes_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_two_different(*args)
|
||||
takes_two_different_positional_only(*args)
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Mixed tuple argument
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
def takes_zero() -> None: ...
|
||||
def takes_one(x: int) -> None: ...
|
||||
def takes_two(x: int, y: int) -> None: ...
|
||||
def takes_two_positional_only(x: int, y: int, /) -> None: ...
|
||||
def takes_two_different(x: int, y: str) -> None: ...
|
||||
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
|
||||
def takes_at_least_zero(*args) -> None: ...
|
||||
def takes_at_least_one(x: int, *args) -> None: ...
|
||||
def takes_at_least_two(x: int, y: int, *args) -> None: ...
|
||||
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(args: tuple[int, *tuple[int, ...]]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args)
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, *tuple[str, ...]]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args)
|
||||
takes_two(*args) # error: [invalid-argument-type]
|
||||
takes_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_two_different(*args)
|
||||
takes_two_different_positional_only(*args)
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
|
||||
def _(args: tuple[int, int, *tuple[int, ...]]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, int, *tuple[str, ...]]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, *tuple[int, ...], int]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: tuple[int, *tuple[str, ...], int]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args) # error: [invalid-argument-type]
|
||||
takes_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_two_different(*args)
|
||||
takes_two_different_positional_only(*args)
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### String argument
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def takes_zero() -> None: ...
|
||||
def takes_one(x: str) -> None: ...
|
||||
def takes_two(x: str, y: str) -> None: ...
|
||||
def takes_two_positional_only(x: str, y: str, /) -> None: ...
|
||||
def takes_two_different(x: int, y: str) -> None: ...
|
||||
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
|
||||
def takes_at_least_zero(*args) -> None: ...
|
||||
def takes_at_least_one(x: str, *args) -> None: ...
|
||||
def takes_at_least_two(x: str, y: str, *args) -> None: ...
|
||||
def takes_at_least_two_positional_only(x: str, y: str, /, *args) -> None: ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(args: Literal["a"]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args)
|
||||
takes_two(*args) # error: [missing-argument]
|
||||
takes_two_positional_only(*args) # error: [missing-argument]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [missing-argument]
|
||||
takes_two_different(*args)
|
||||
# error: [invalid-argument-type]
|
||||
# error: [missing-argument]
|
||||
takes_two_different_positional_only(*args)
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args) # error: [missing-argument]
|
||||
takes_at_least_two_positional_only(*args) # error: [missing-argument]
|
||||
|
||||
def _(args: Literal["ab"]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: Literal["abc"]) -> None:
|
||||
takes_zero(*args) # error: [too-many-positional-arguments]
|
||||
takes_one(*args) # error: [too-many-positional-arguments]
|
||||
takes_two(*args) # error: [too-many-positional-arguments]
|
||||
takes_two_positional_only(*args) # error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [too-many-positional-arguments]
|
||||
takes_two_different(*args)
|
||||
# error: [invalid-argument-type]
|
||||
# error: [too-many-positional-arguments]
|
||||
takes_two_different_positional_only(*args)
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
|
||||
def _(args: str) -> None:
|
||||
takes_zero(*args)
|
||||
takes_one(*args)
|
||||
takes_two(*args)
|
||||
takes_two_positional_only(*args)
|
||||
takes_two_different(*args) # error: [invalid-argument-type]
|
||||
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
|
||||
takes_at_least_zero(*args)
|
||||
takes_at_least_one(*args)
|
||||
takes_at_least_two(*args)
|
||||
takes_at_least_two_positional_only(*args)
|
||||
```
|
||||
|
||||
### Argument expansion regression
|
||||
|
||||
This is a regression that was highlighted by the ecosystem check, which shows that we might need to
|
||||
rethink how we perform argument expansion during overload resolution. In particular, we might need
|
||||
to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry
|
||||
`check_types`.
|
||||
|
||||
The issue is that argument expansion might produce a splatted value with a different arity than what
|
||||
we originally inferred for the unexpanded value, and that in turn can affect which parameters the
|
||||
splatted value is matched with.
|
||||
|
||||
The first example correctly produces an error. The `tuple[int, str]` union element has a precise
|
||||
arity of two, and so parameter matching chooses the first overload. The second element of the tuple
|
||||
does not match the second parameter type, which yielding an `invalid-argument-type` error.
|
||||
|
||||
The third example should produce the same error. However, because we have a union, we do not see the
|
||||
precise arity of each union element during parameter matching. Instead, we infer an arity of "zero
|
||||
or more" for the union as a whole, and use that less precise arity when matching parameters. We
|
||||
therefore consider the second overload to still be a potential candidate for the `tuple[int, str]`
|
||||
union element. During type checking, we have to force the arity of each union element to match the
|
||||
inferred arity of the union as a whole (turning `tuple[int, str]` into `tuple[int | str, ...]`).
|
||||
That less precise tuple type-checks successfully against the second overload, making us incorrectly
|
||||
think that `tuple[int, str]` is a valid splatted call.
|
||||
|
||||
If we update argument expansion to retry parameter matching with the precise arity of each union
|
||||
element, we will correctly rule out the second overload for `tuple[int, str]`, just like we do when
|
||||
splatting that tuple directly (instead of as part of a union).
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def f(x: int, y: int) -> None: ...
|
||||
@overload
|
||||
def f(x: int, y: str, z: int) -> None: ...
|
||||
def f(*args): ...
|
||||
|
||||
# Test all of the above with a number of different splatted argument types
|
||||
|
||||
def _(t: tuple[int, str]) -> None:
|
||||
f(*t) # error: [invalid-argument-type]
|
||||
|
||||
def _(t: tuple[int, str, int]) -> None:
|
||||
f(*t)
|
||||
|
||||
def _(t: tuple[int, str] | tuple[int, str, int]) -> None:
|
||||
# TODO: error: [invalid-argument-type]
|
||||
f(*t)
|
||||
```
|
||||
|
||||
## Wrong argument type
|
||||
|
||||
### Positional argument, positional-or-keyword parameter
|
||||
@@ -625,7 +310,7 @@ len()
|
||||
len([], 1)
|
||||
```
|
||||
|
||||
### Type property predicates
|
||||
### Type API predicates
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of
|
||||
|
||||
@@ -204,7 +204,7 @@ reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or
|
||||
## Method calls on types not disjoint from `None`
|
||||
|
||||
Very few methods are defined on `object`, `None`, and other types not disjoint from `None`. However,
|
||||
descriptor-binding behavior works on these types in exactly the same way as descriptor binding on
|
||||
descriptor-binding behaviour works on these types in exactly the same way as descriptor binding on
|
||||
other types. This is despite the fact that `None` is used as a sentinel internally by the descriptor
|
||||
protocol to indicate that a method was accessed on the class itself rather than an instance of the
|
||||
class:
|
||||
|
||||
@@ -5,12 +5,6 @@ with one or more overloads. This document describes the algorithm that it uses f
|
||||
matching, which is the same as the one mentioned in the
|
||||
[spec](https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation).
|
||||
|
||||
Note that all of the examples that involve positional parameters are tested multiple times: once
|
||||
with the parameters matched with individual positional arguments, and once with the parameters
|
||||
matched with a single positional argument that is splatted into the argument list. Overload
|
||||
resolution is performed _after_ splatted arguments have been expanded, and so both approaches (TODO:
|
||||
should) produce the same results.
|
||||
|
||||
## Arity check
|
||||
|
||||
The first step is to perform arity check. The non-overloaded cases are described in the
|
||||
@@ -32,15 +26,10 @@ from overloaded import f
|
||||
|
||||
# These match a single overload
|
||||
reveal_type(f()) # revealed: None
|
||||
reveal_type(f(*())) # revealed: None
|
||||
|
||||
reveal_type(f(1)) # revealed: int
|
||||
reveal_type(f(*(1,))) # revealed: int
|
||||
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f("a", "b")) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(*("a", "b"))) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Type checking
|
||||
@@ -70,13 +59,8 @@ which filters out all but the matching overload:
|
||||
from overloaded import f
|
||||
|
||||
reveal_type(f(1)) # revealed: int
|
||||
reveal_type(f(*(1,))) # revealed: int
|
||||
|
||||
reveal_type(f("a")) # revealed: str
|
||||
reveal_type(f(*("a",))) # revealed: str
|
||||
|
||||
reveal_type(f(b"b")) # revealed: bytes
|
||||
reveal_type(f(*(b"b",))) # revealed: bytes
|
||||
```
|
||||
|
||||
### Single match error
|
||||
@@ -104,12 +88,9 @@ from typing_extensions import reveal_type
|
||||
from overloaded import f
|
||||
|
||||
reveal_type(f()) # revealed: None
|
||||
reveal_type(f(*())) # revealed: None
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`"
|
||||
reveal_type(f("a")) # revealed: Unknown
|
||||
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`"
|
||||
reveal_type(f(*("a",))) # revealed: Unknown
|
||||
```
|
||||
|
||||
More examples of this diagnostic can be found in the
|
||||
@@ -136,15 +117,10 @@ from overloaded import A, B, f
|
||||
|
||||
# These calls pass the arity check, and type checking matches both overloads:
|
||||
reveal_type(f(A())) # revealed: A
|
||||
reveal_type(f(*(A(),))) # revealed: A
|
||||
|
||||
reveal_type(f(B())) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(B(),))) # revealed: Unknown
|
||||
|
||||
# But, in this case, the arity check filters out the first overload, so we only have one match:
|
||||
reveal_type(f(B(), 1)) # revealed: B
|
||||
reveal_type(f(*(B(), 1))) # revealed: B
|
||||
```
|
||||
|
||||
## Argument type expansion
|
||||
@@ -179,13 +155,8 @@ from overloaded import A, B, C, f
|
||||
|
||||
def _(ab: A | B, ac: A | C, bc: B | C):
|
||||
reveal_type(f(ab)) # revealed: A | B
|
||||
reveal_type(f(*(ab,))) # revealed: A | B
|
||||
|
||||
reveal_type(f(bc)) # revealed: B | C
|
||||
reveal_type(f(*(bc,))) # revealed: B | C
|
||||
|
||||
reveal_type(f(ac)) # revealed: A | C
|
||||
reveal_type(f(*(ac,))) # revealed: A | C
|
||||
```
|
||||
|
||||
### Expanding first argument
|
||||
@@ -218,15 +189,11 @@ from overloaded import A, B, C, D, f
|
||||
|
||||
def _(a_b: A | B):
|
||||
reveal_type(f(a_b, C())) # revealed: A | C
|
||||
reveal_type(f(*(a_b, C()))) # revealed: A | C
|
||||
|
||||
reveal_type(f(a_b, D())) # revealed: B | D
|
||||
reveal_type(f(*(a_b, D()))) # revealed: B | D
|
||||
|
||||
# But, if it doesn't, it should expand the second argument and try again:
|
||||
def _(a_b: A | B, c_d: C | D):
|
||||
reveal_type(f(a_b, c_d)) # revealed: A | B | C | D
|
||||
reveal_type(f(*(a_b, c_d))) # revealed: A | B | C | D
|
||||
```
|
||||
|
||||
### Expanding second argument
|
||||
@@ -259,12 +226,9 @@ def _(a: A, bc: B | C, cd: C | D):
|
||||
# This also tests that partial matching works correctly as the argument type expansion results
|
||||
# in matching the first and second overloads, but not the third one.
|
||||
reveal_type(f(a, bc)) # revealed: B | C
|
||||
reveal_type(f(*(a, bc))) # revealed: B | C
|
||||
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(a, cd)) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(*(a, cd))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generics (legacy)
|
||||
@@ -290,16 +254,7 @@ from overloaded import A, f
|
||||
|
||||
def _(x: int, y: A | int):
|
||||
reveal_type(f(x)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(x,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(y)) # revealed: A | int
|
||||
# TODO: revealed: A | int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(y,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generics (PEP 695)
|
||||
@@ -328,16 +283,7 @@ from overloaded import B, f
|
||||
|
||||
def _(x: int, y: B | int):
|
||||
reveal_type(f(x)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(x,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(y)) # revealed: B | int
|
||||
# TODO: revealed: B | int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(y,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Expanding `bool`
|
||||
@@ -361,13 +307,8 @@ from overloaded import f
|
||||
|
||||
def _(flag: bool):
|
||||
reveal_type(f(True)) # revealed: T
|
||||
reveal_type(f(*(True,))) # revealed: T
|
||||
|
||||
reveal_type(f(False)) # revealed: F
|
||||
reveal_type(f(*(False,))) # revealed: F
|
||||
|
||||
reveal_type(f(flag)) # revealed: T | F
|
||||
reveal_type(f(*(flag,))) # revealed: T | F
|
||||
```
|
||||
|
||||
### Expanding `tuple`
|
||||
@@ -397,7 +338,6 @@ from overloaded import A, B, f
|
||||
|
||||
def _(x: tuple[A | B, int], y: tuple[int, bool]):
|
||||
reveal_type(f(x, y)) # revealed: A | B | C | D
|
||||
reveal_type(f(*(x, y))) # revealed: A | B | C | D
|
||||
```
|
||||
|
||||
### Expanding `type`
|
||||
@@ -425,13 +365,10 @@ from overloaded import A, B, f
|
||||
def _(x: type[A | B]):
|
||||
reveal_type(x) # revealed: type[A] | type[B]
|
||||
reveal_type(f(x)) # revealed: A | B
|
||||
reveal_type(f(*(x,))) # revealed: A | B
|
||||
```
|
||||
|
||||
### Expanding enums
|
||||
|
||||
#### Basic
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
@@ -457,125 +394,15 @@ def f(x: Literal[SomeEnum.C]) -> C: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from overloaded import SomeEnum, A, B, C, f
|
||||
|
||||
def _(x: SomeEnum, y: Literal[SomeEnum.A, SomeEnum.C]):
|
||||
def _(x: SomeEnum):
|
||||
reveal_type(f(SomeEnum.A)) # revealed: A
|
||||
reveal_type(f(*(SomeEnum.A,))) # revealed: A
|
||||
|
||||
reveal_type(f(SomeEnum.B)) # revealed: B
|
||||
reveal_type(f(*(SomeEnum.B,))) # revealed: B
|
||||
|
||||
reveal_type(f(SomeEnum.C)) # revealed: C
|
||||
reveal_type(f(*(SomeEnum.C,))) # revealed: C
|
||||
|
||||
reveal_type(f(x)) # revealed: A | B | C
|
||||
reveal_type(f(*(x,))) # revealed: A | B | C
|
||||
|
||||
reveal_type(f(y)) # revealed: A | C
|
||||
reveal_type(f(*(y,))) # revealed: A | C
|
||||
```
|
||||
|
||||
#### Enum with single member
|
||||
|
||||
This pattern appears in typeshed. Here, it is used to represent two optional, mutually exclusive
|
||||
keyword parameters:
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from enum import Enum, auto
|
||||
from typing import overload, Literal
|
||||
|
||||
class Missing(Enum):
|
||||
Value = auto()
|
||||
|
||||
class OnlyASpecified: ...
|
||||
class OnlyBSpecified: ...
|
||||
class BothMissing: ...
|
||||
|
||||
@overload
|
||||
def f(*, a: int, b: Literal[Missing.Value] = ...) -> OnlyASpecified: ...
|
||||
@overload
|
||||
def f(*, a: Literal[Missing.Value] = ..., b: int) -> OnlyBSpecified: ...
|
||||
@overload
|
||||
def f(*, a: Literal[Missing.Value] = ..., b: Literal[Missing.Value] = ...) -> BothMissing: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from overloaded import f, Missing
|
||||
|
||||
reveal_type(f()) # revealed: BothMissing
|
||||
reveal_type(f(a=0)) # revealed: OnlyASpecified
|
||||
reveal_type(f(b=0)) # revealed: OnlyBSpecified
|
||||
|
||||
f(a=0, b=0) # error: [no-matching-overload]
|
||||
|
||||
def _(missing: Literal[Missing.Value], missing_or_present: Literal[Missing.Value] | int):
|
||||
reveal_type(f(a=missing, b=missing)) # revealed: BothMissing
|
||||
reveal_type(f(a=missing)) # revealed: BothMissing
|
||||
reveal_type(f(b=missing)) # revealed: BothMissing
|
||||
reveal_type(f(a=0, b=missing)) # revealed: OnlyASpecified
|
||||
reveal_type(f(a=missing, b=0)) # revealed: OnlyBSpecified
|
||||
|
||||
reveal_type(f(a=missing_or_present)) # revealed: BothMissing | OnlyASpecified
|
||||
reveal_type(f(b=missing_or_present)) # revealed: BothMissing | OnlyBSpecified
|
||||
|
||||
# Here, both could be present, so this should be an error
|
||||
f(a=missing_or_present, b=missing_or_present) # error: [no-matching-overload]
|
||||
```
|
||||
|
||||
#### Enum subclass without members
|
||||
|
||||
An `Enum` subclass without members should _not_ be expanded:
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from enum import Enum
|
||||
from typing import overload, Literal
|
||||
|
||||
class MyEnumSubclass(Enum):
|
||||
pass
|
||||
|
||||
class ActualEnum(MyEnumSubclass):
|
||||
A = 1
|
||||
B = 2
|
||||
|
||||
class OnlyA: ...
|
||||
class OnlyB: ...
|
||||
class Both: ...
|
||||
|
||||
@overload
|
||||
def f(x: Literal[ActualEnum.A]) -> OnlyA: ...
|
||||
@overload
|
||||
def f(x: Literal[ActualEnum.B]) -> OnlyB: ...
|
||||
@overload
|
||||
def f(x: ActualEnum) -> Both: ...
|
||||
@overload
|
||||
def f(x: MyEnumSubclass) -> MyEnumSubclass: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from overloaded import MyEnumSubclass, ActualEnum, f
|
||||
|
||||
def _(actual_enum: ActualEnum, my_enum_instance: MyEnumSubclass):
|
||||
reveal_type(f(actual_enum)) # revealed: Both
|
||||
# TODO: revealed: Both
|
||||
reveal_type(f(*(actual_enum,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(ActualEnum.A)) # revealed: OnlyA
|
||||
# TODO: revealed: OnlyA
|
||||
reveal_type(f(*(ActualEnum.A,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(ActualEnum.B)) # revealed: OnlyB
|
||||
# TODO: revealed: OnlyB
|
||||
reveal_type(f(*(ActualEnum.B,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(my_enum_instance)) # revealed: MyEnumSubclass
|
||||
reveal_type(f(*(my_enum_instance,))) # revealed: MyEnumSubclass
|
||||
# TODO: This should not be an error. The return type should be `A | B | C` once enums are expanded
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(x)) # revealed: Unknown
|
||||
```
|
||||
|
||||
### No matching overloads
|
||||
@@ -604,22 +431,21 @@ from overloaded import A, B, C, D, f
|
||||
|
||||
def _(ab: A | B, ac: A | C, cd: C | D):
|
||||
reveal_type(f(ab)) # revealed: A | B
|
||||
reveal_type(f(*(ab,))) # revealed: A | B
|
||||
|
||||
# The `[A | C]` argument list is expanded to `[A], [C]` where the first list matches the first
|
||||
# overload while the second list doesn't match any of the overloads, so we generate an
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(ac)) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(*(ac,))) # revealed: Unknown
|
||||
|
||||
# None of the expanded argument lists (`[C], [D]`) match any of the overloads, so we generate an
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(cd)) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
reveal_type(f(*(cd,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Filtering overloads with variadic arguments and parameters
|
||||
|
||||
TODO
|
||||
|
||||
## Filtering based on `Any` / `Unknown`
|
||||
|
||||
This is the step 5 of the overload call evaluation algorithm which specifies that:
|
||||
@@ -654,16 +480,10 @@ from overloaded import f
|
||||
|
||||
# Anything other than `list` should match the last overload
|
||||
reveal_type(f(1)) # revealed: str
|
||||
reveal_type(f(*(1,))) # revealed: str
|
||||
|
||||
def _(list_int: list[int], list_any: list[Any]):
|
||||
reveal_type(f(list_int)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(list_any)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Single list argument (ambiguous)
|
||||
@@ -691,20 +511,16 @@ from overloaded import f
|
||||
|
||||
# Anything other than `list` should match the last overload
|
||||
reveal_type(f(1)) # revealed: str
|
||||
reveal_type(f(*(1,))) # revealed: str
|
||||
|
||||
def _(list_int: list[int], list_any: list[Any]):
|
||||
# All materializations of `list[int]` are assignable to `list[int]`, so it matches the first
|
||||
# overload.
|
||||
reveal_type(f(list_int)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
|
||||
# All materializations of `list[Any]` are assignable to `list[int]` and `list[Any]`, but the
|
||||
# return type of first and second overloads are not equivalent, so the overload matching
|
||||
# is ambiguous.
|
||||
reveal_type(f(list_any)) # revealed: Unknown
|
||||
reveal_type(f(*(list_any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Single tuple argument
|
||||
@@ -728,33 +544,21 @@ from typing import Any
|
||||
from overloaded import f
|
||||
|
||||
reveal_type(f("a")) # revealed: str
|
||||
reveal_type(f(*("a",))) # revealed: str
|
||||
|
||||
reveal_type(f((1, "b"))) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*((1, "b"),))) # revealed: Unknown
|
||||
|
||||
reveal_type(f((1, 2))) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*((1, 2),))) # revealed: Unknown
|
||||
|
||||
def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]):
|
||||
# All materializations are assignable to first overload, so second and third overloads are
|
||||
# eliminated
|
||||
reveal_type(f(int_str)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(int_str,))) # revealed: Unknown
|
||||
|
||||
# All materializations are assignable to second overload, so the third overload is eliminated;
|
||||
# the return type of first and second overload is equivalent
|
||||
reveal_type(f(int_any)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*(int_any,))) # revealed: Unknown
|
||||
|
||||
# All materializations of `tuple[Any, Any]` are assignable to the parameters of all the
|
||||
# overloads, but the return types aren't equivalent, so the overload matching is ambiguous
|
||||
reveal_type(f(any_any)) # revealed: Unknown
|
||||
reveal_type(f(*(any_any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Multiple arguments
|
||||
@@ -784,32 +588,23 @@ def _(list_int: list[int], list_any: list[Any], int_str: tuple[int, str], int_an
|
||||
# All materializations of both argument types are assignable to the first overload, so the
|
||||
# second and third overloads are filtered out
|
||||
reveal_type(f(list_int, int_str)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int, int_str))) # revealed: Unknown
|
||||
|
||||
# All materialization of first argument is assignable to first overload and for the second
|
||||
# argument, they're assignable to the second overload, so the third overload is filtered out
|
||||
reveal_type(f(list_int, int_any)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int, int_any))) # revealed: Unknown
|
||||
|
||||
# All materialization of first argument is assignable to second overload and for the second
|
||||
# argument, they're assignable to the first overload, so the third overload is filtered out
|
||||
reveal_type(f(list_any, int_str)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_any, int_str))) # revealed: Unknown
|
||||
|
||||
# All materializations of both arguments are assignable to the second overload, so the third
|
||||
# overload is filtered out
|
||||
reveal_type(f(list_any, int_any)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_any, int_any))) # revealed: Unknown
|
||||
|
||||
# All materializations of first argument is assignable to the second overload and for the second
|
||||
# argument, they're assignable to the third overload, so no overloads are filtered out; the
|
||||
# return types of the remaining overloads are not equivalent, so overload matching is ambiguous
|
||||
reveal_type(f(list_int, any_any)) # revealed: Unknown
|
||||
reveal_type(f(*(list_int, any_any))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `LiteralString` and `str`
|
||||
@@ -834,16 +629,11 @@ from overloaded import f
|
||||
|
||||
def _(literal: LiteralString, string: str, any: Any):
|
||||
reveal_type(f(literal)) # revealed: LiteralString
|
||||
# TODO: revealed: LiteralString
|
||||
reveal_type(f(*(literal,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(string)) # revealed: str
|
||||
reveal_type(f(*(string,))) # revealed: str
|
||||
|
||||
# `Any` matches both overloads, but the return types are not equivalent.
|
||||
# Pyright and mypy both reveal `str` here, contrary to the spec.
|
||||
reveal_type(f(any)) # revealed: Unknown
|
||||
reveal_type(f(*(any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generics
|
||||
@@ -873,19 +663,10 @@ from overloaded import f
|
||||
|
||||
def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any):
|
||||
reveal_type(f(list_int)) # revealed: A
|
||||
# TODO: revealed: A
|
||||
reveal_type(f(*(list_int,))) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `str`
|
||||
reveal_type(f(list_str)) # revealed: Unknown
|
||||
# TODO: Should be `str`
|
||||
reveal_type(f(*(list_str,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(list_any)) # revealed: Unknown
|
||||
reveal_type(f(*(list_any,))) # revealed: Unknown
|
||||
|
||||
reveal_type(f(any)) # revealed: Unknown
|
||||
reveal_type(f(*(any,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generics (multiple arguments)
|
||||
@@ -910,24 +691,12 @@ from overloaded import f
|
||||
|
||||
def _(integer: int, string: str, any: Any, list_any: list[Any]):
|
||||
reveal_type(f(integer, string)) # revealed: int
|
||||
reveal_type(f(*(integer, string))) # revealed: int
|
||||
|
||||
reveal_type(f(string, integer)) # revealed: int
|
||||
# TODO: revealed: int
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(string, integer))) # revealed: Unknown
|
||||
|
||||
# This matches the second overload and is _not_ the case of ambiguous overload matching.
|
||||
reveal_type(f(string, any)) # revealed: Any
|
||||
# TODO: Any
|
||||
reveal_type(f(*(string, any))) # revealed: tuple[str, Any]
|
||||
|
||||
reveal_type(f(string, list_any)) # revealed: list[Any]
|
||||
# TODO: revealed: list[Any]
|
||||
# TODO: no error
|
||||
# error: [no-matching-overload]
|
||||
reveal_type(f(*(string, list_any))) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Generic `self`
|
||||
@@ -970,54 +739,7 @@ def _(b_int: B[int], b_str: B[str], b_any: B[Any]):
|
||||
|
||||
### Variadic argument
|
||||
|
||||
`overloaded.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import Any, overload
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
@overload
|
||||
def f1(x: int) -> A: ...
|
||||
@overload
|
||||
def f1(x: Any, y: Any) -> A: ...
|
||||
|
||||
@overload
|
||||
def f2(x: int) -> A: ...
|
||||
@overload
|
||||
def f2(x: Any, y: Any) -> B: ...
|
||||
|
||||
@overload
|
||||
def f3(x: int) -> A: ...
|
||||
@overload
|
||||
def f3(x: Any, y: Any) -> A: ...
|
||||
@overload
|
||||
def f3(x: Any, y: Any, *, z: str) -> B: ...
|
||||
|
||||
@overload
|
||||
def f4(x: int) -> A: ...
|
||||
@overload
|
||||
def f4(x: Any, y: Any) -> B: ...
|
||||
@overload
|
||||
def f4(x: Any, y: Any, *, z: str) -> B: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
from overloaded import f1, f2, f3, f4
|
||||
|
||||
def _(arg: list[Any]):
|
||||
# Matches both overload and the return types are equivalent
|
||||
reveal_type(f1(*arg)) # revealed: A
|
||||
# Matches both overload but the return types aren't equivalent
|
||||
reveal_type(f2(*arg)) # revealed: Unknown
|
||||
# Filters out the final overload and the return types are equivalent
|
||||
reveal_type(f3(*arg)) # revealed: A
|
||||
# Filters out the final overload but the return types aren't equivalent
|
||||
reveal_type(f4(*arg)) # revealed: Unknown
|
||||
```
|
||||
TODO: A variadic parameter is being assigned to a number of parameters of the same type
|
||||
|
||||
### Non-participating fully-static parameter
|
||||
|
||||
@@ -1053,10 +775,7 @@ from overloaded import f
|
||||
|
||||
def _(any: Any):
|
||||
reveal_type(f(any, flag=True)) # revealed: int
|
||||
reveal_type(f(*(any,), flag=True)) # revealed: int
|
||||
|
||||
reveal_type(f(any, flag=False)) # revealed: str
|
||||
reveal_type(f(*(any,), flag=False)) # revealed: str
|
||||
```
|
||||
|
||||
### Non-participating gradual parameter
|
||||
@@ -1067,32 +786,21 @@ def _(any: Any):
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
@overload
|
||||
def f(x: tuple[str, Any], flag: Literal[True]) -> int: ...
|
||||
def f(x: tuple[str, Any], *, flag: Literal[True]) -> int: ...
|
||||
@overload
|
||||
def f(x: tuple[str, Any], flag: Literal[False] = ...) -> str: ...
|
||||
def f(x: tuple[str, Any], *, flag: Literal[False] = ...) -> str: ...
|
||||
@overload
|
||||
def f(x: tuple[str, Any], flag: bool = ...) -> int | str: ...
|
||||
def f(x: tuple[str, Any], *, flag: bool = ...) -> int | str: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
|
||||
from overloaded import f
|
||||
|
||||
def _(any: Any):
|
||||
reveal_type(f(any, flag=True)) # revealed: int
|
||||
reveal_type(f(*(any,), flag=True)) # revealed: int
|
||||
|
||||
reveal_type(f(any, flag=False)) # revealed: str
|
||||
reveal_type(f(*(any,), flag=False)) # revealed: str
|
||||
|
||||
def _(args: tuple[Any, Literal[True]]):
|
||||
# TODO: revealed: int
|
||||
reveal_type(f(*args)) # revealed: Unknown
|
||||
|
||||
def _(args: tuple[Any, Literal[False]]):
|
||||
# TODO: revealed: str
|
||||
reveal_type(f(*args)) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Argument type expansion
|
||||
@@ -1137,7 +845,6 @@ from overloaded import A, B, f
|
||||
|
||||
def _(arg: tuple[A | B, Any]):
|
||||
reveal_type(f(arg)) # revealed: A | B
|
||||
reveal_type(f(*(arg,))) # revealed: A | B
|
||||
```
|
||||
|
||||
#### One argument list ambiguous
|
||||
@@ -1172,7 +879,6 @@ from overloaded import A, B, C, f
|
||||
|
||||
def _(arg: tuple[A | B, Any]):
|
||||
reveal_type(f(arg)) # revealed: A | Unknown
|
||||
reveal_type(f(*(arg,))) # revealed: A | Unknown
|
||||
```
|
||||
|
||||
#### Both argument lists ambiguous
|
||||
@@ -1206,5 +912,4 @@ from overloaded import A, B, C, f
|
||||
|
||||
def _(arg: tuple[A | B, Any]):
|
||||
reveal_type(f(arg)) # revealed: Unknown
|
||||
reveal_type(f(*(arg,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -80,8 +80,6 @@ def _(subject: C):
|
||||
A `case` branch with a class pattern is taken if the subject is an instance of the given class, and
|
||||
all subpatterns in the class pattern match.
|
||||
|
||||
### Without arguments
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@@ -138,51 +136,6 @@ def _(target: FooSub | str):
|
||||
reveal_type(y) # revealed: Literal[1, 3, 4]
|
||||
```
|
||||
|
||||
### With arguments
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_never
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
class Other: ...
|
||||
|
||||
def _(target: Point):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Point(0, 0):
|
||||
y = 2
|
||||
case Point(x=0, y=1):
|
||||
y = 3
|
||||
case Point(x=1, y=0):
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3, 4]
|
||||
|
||||
def _(target: Point):
|
||||
match target:
|
||||
case Point(x, y): # irrefutable sub-patterns
|
||||
pass
|
||||
case _:
|
||||
assert_never(target)
|
||||
|
||||
def _(target: Point | Other):
|
||||
match target:
|
||||
case Point(0, 0):
|
||||
reveal_type(target) # revealed: Point
|
||||
case Point(x=0, y=1):
|
||||
reveal_type(target) # revealed: Point
|
||||
case Point(x=1, y=0):
|
||||
reveal_type(target) # revealed: Point
|
||||
case Other():
|
||||
reveal_type(target) # revealed: Other
|
||||
```
|
||||
|
||||
## Singleton match
|
||||
|
||||
Singleton patterns are matched based on identity, not equality comparisons or `isinstance()` checks.
|
||||
@@ -201,7 +154,8 @@ def _(target: Literal[True, False]):
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: bool):
|
||||
y = 1
|
||||
@@ -214,7 +168,8 @@ def _(target: bool):
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
@@ -240,7 +195,8 @@ def _(target: None | Literal[True]):
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 4]
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 4]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
|
||||
# bool is an int subclass
|
||||
def _(target: int):
|
||||
@@ -289,7 +245,7 @@ def _(answer: Answer):
|
||||
reveal_type(answer) # revealed: Literal[Answer.NO]
|
||||
y = 2
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## Or match
|
||||
@@ -308,7 +264,8 @@ def _(target: Literal["foo", "baz"]):
|
||||
case "baz":
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
# TODO: with exhaustiveness, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
@@ -640,8 +640,6 @@ reveal_type(C.__init__) # revealed: (self: C, normal: int, conditionally_presen
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
### Basic
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -660,34 +658,6 @@ reveal_type(d_int.description) # revealed: str
|
||||
DataWithDescription[int](None, "description")
|
||||
```
|
||||
|
||||
### Deriving from generic dataclasses
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/853>.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Wrap[T]:
|
||||
data: T
|
||||
|
||||
reveal_type(Wrap[int].__init__) # revealed: (self: Wrap[int], data: int) -> None
|
||||
|
||||
@dataclass
|
||||
class WrappedInt(Wrap[int]):
|
||||
other_field: str
|
||||
|
||||
reveal_type(WrappedInt.__init__) # revealed: (self: WrappedInt, data: int, other_field: str) -> None
|
||||
|
||||
# Make sure that another generic type parameter does not affect the `data` field
|
||||
@dataclass
|
||||
class WrappedIntAndExtraData[T](Wrap[int]):
|
||||
extra_data: T
|
||||
|
||||
# revealed: (self: WrappedIntAndExtraData[bytes], data: int, extra_data: bytes) -> None
|
||||
reveal_type(WrappedIntAndExtraData[bytes].__init__)
|
||||
```
|
||||
|
||||
## Descriptor-typed fields
|
||||
|
||||
### Same type in `__get__` and `__set__`
|
||||
|
||||
@@ -54,7 +54,7 @@ class deprecated:
|
||||
```
|
||||
|
||||
Only the mandatory message string is of interest to static analysis, the other two affect only
|
||||
runtime behavior.
|
||||
runtime behaviour.
|
||||
|
||||
```py
|
||||
from typing_extensions import deprecated
|
||||
|
||||
@@ -119,6 +119,8 @@ def match_singletons_success(obj: Literal[1, "a"] | None):
|
||||
case None:
|
||||
pass
|
||||
case _ as obj:
|
||||
# TODO: Ideally, we would not emit an error here
|
||||
# error: [type-assertion-failure] "Argument does not have asserted type `Never`"
|
||||
assert_never(obj)
|
||||
|
||||
def match_singletons_error(obj: Literal[1, "a"] | None):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user