Compare commits

..

1 Commits

Author SHA1 Message Date
Douglas Creager
e4be76e812 lazy/eager bounds/etc should not cause assignability failures 2025-11-17 21:54:37 -05:00
150 changed files with 2958 additions and 11893 deletions

View File

@@ -55,7 +55,6 @@ jobs:
- name: Run mypy_primer
env:
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
CLICOLOR_FORCE: "1"
DIFF_FILE: mypy_primer.diff
run: |
cd ruff

View File

@@ -67,7 +67,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
ecosystem-analyzer \
--repository ruff \

View File

@@ -52,7 +52,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e26ebfb78d372b8b091e1cb1d6fc522e135474c1"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
ecosystem-analyzer \
--verbose \

26
Cargo.lock generated
View File

@@ -642,7 +642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -651,7 +651,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1016,7 +1016,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1256,7 +1256,6 @@ dependencies = [
"compact_str",
"get-size-derive2",
"hashbrown 0.16.0",
"indexmap",
"smallvec",
]
@@ -1699,7 +1698,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1763,7 +1762,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3005,7 +3004,6 @@ dependencies = [
"serde",
"serde_json",
"similar",
"supports-hyperlinks",
"tempfile",
"thiserror 2.0.17",
"tracing",
@@ -3570,7 +3568,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3927,12 +3925,6 @@ dependencies = [
"syn",
]
[[package]]
name = "supports-hyperlinks"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
[[package]]
name = "syn"
version = "2.0.110"
@@ -3971,7 +3963,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -5020,7 +5012,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]

View File

@@ -173,7 +173,6 @@ snapbox = { version = "0.6.0", features = [
static_assertions = "1.1.0"
strum = { version = "0.27.0", features = ["strum_macros"] }
strum_macros = { version = "0.27.0" }
supports-hyperlinks = { version = "3.1.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }

View File

@@ -31,7 +31,7 @@
//! styling.
//!
//! The above snippet has been built out of the following structure:
use crate::{Id, snippet};
use crate::snippet;
use std::cmp::{Reverse, max, min};
use std::collections::HashMap;
use std::fmt::Display;
@@ -189,7 +189,6 @@ impl DisplaySet<'_> {
}
Ok(())
}
fn format_annotation(
&self,
line_offset: usize,
@@ -200,13 +199,11 @@ impl DisplaySet<'_> {
) -> fmt::Result {
let hide_severity = annotation.annotation_type.is_none();
let color = get_annotation_style(&annotation.annotation_type, stylesheet);
let formatted_len = if let Some(id) = &annotation.id {
let id_len = id.id.len();
if hide_severity {
id_len
id.len()
} else {
2 + id_len + annotation_type_len(&annotation.annotation_type)
2 + id.len() + annotation_type_len(&annotation.annotation_type)
}
} else {
annotation_type_len(&annotation.annotation_type)
@@ -259,20 +256,9 @@ impl DisplaySet<'_> {
let annotation_type = annotation_type_str(&annotation.annotation_type);
if let Some(id) = annotation.id {
if hide_severity {
buffer.append(
line_offset,
&format!("{id} ", id = fmt_with_hyperlink(id.id, id.url, stylesheet)),
*stylesheet.error(),
);
buffer.append(line_offset, &format!("{id} "), *stylesheet.error());
} else {
buffer.append(
line_offset,
&format!(
"{annotation_type}[{id}]",
id = fmt_with_hyperlink(id.id, id.url, stylesheet)
),
*color,
);
buffer.append(line_offset, &format!("{annotation_type}[{id}]"), *color);
}
} else {
buffer.append(line_offset, annotation_type, *color);
@@ -721,7 +707,7 @@ impl DisplaySet<'_> {
let style =
get_annotation_style(&annotation.annotation_type, stylesheet);
let mut formatted_len = if let Some(id) = &annotation.annotation.id {
2 + id.id.len()
2 + id.len()
+ annotation_type_len(&annotation.annotation.annotation_type)
} else {
annotation_type_len(&annotation.annotation.annotation_type)
@@ -738,10 +724,7 @@ impl DisplaySet<'_> {
} else if formatted_len != 0 {
formatted_len += 2;
let id = match &annotation.annotation.id {
Some(id) => format!(
"[{id}]",
id = fmt_with_hyperlink(&id.id, id.url, stylesheet)
),
Some(id) => format!("[{id}]"),
None => String::new(),
};
buffer.puts(
@@ -844,7 +827,7 @@ impl DisplaySet<'_> {
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Annotation<'a> {
pub(crate) annotation_type: DisplayAnnotationType,
pub(crate) id: Option<Id<'a>>,
pub(crate) id: Option<&'a str>,
pub(crate) label: Vec<DisplayTextFragment<'a>>,
pub(crate) is_fixable: bool,
}
@@ -1157,7 +1140,7 @@ fn format_message<'m>(
fn format_title<'a>(
level: crate::Level,
id: Option<Id<'a>>,
id: Option<&'a str>,
label: &'a str,
is_fixable: bool,
) -> DisplayLine<'a> {
@@ -1175,7 +1158,7 @@ fn format_title<'a>(
fn format_footer<'a>(
level: crate::Level,
id: Option<Id<'a>>,
id: Option<&'a str>,
label: &'a str,
) -> Vec<DisplayLine<'a>> {
let mut result = vec![];
@@ -1723,7 +1706,6 @@ fn format_body<'m>(
annotation: Annotation {
annotation_type,
id: None,
label: format_label(annotation.label, None),
is_fixable: false,
},
@@ -1905,40 +1887,3 @@ fn char_width(c: char) -> Option<usize> {
unicode_width::UnicodeWidthChar::width(c)
}
}
pub(super) fn fmt_with_hyperlink<'a, T>(
content: T,
url: Option<&'a str>,
stylesheet: &Stylesheet,
) -> impl std::fmt::Display + 'a
where
T: std::fmt::Display + 'a,
{
struct FmtHyperlink<'a, T> {
content: T,
url: Option<&'a str>,
}
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(url) = self.url {
write!(f, "\x1B]8;;{url}\x1B\\")?;
}
self.content.fmt(f)?;
if self.url.is_some() {
f.write_str("\x1B]8;;\x1B\\")?;
}
Ok(())
}
}
let url = if stylesheet.hyperlink { url } else { None };
FmtHyperlink { content, url }
}

View File

@@ -76,7 +76,6 @@ impl Renderer {
}
.effects(Effects::BOLD),
none: Style::new(),
hyperlink: true,
},
..Self::plain()
}
@@ -155,11 +154,6 @@ impl Renderer {
self
}
pub const fn hyperlink(mut self, hyperlink: bool) -> Self {
self.stylesheet.hyperlink = hyperlink;
self
}
/// Set the string used for when a long line is cut.
///
/// The default is `...` (three `U+002E` characters).

View File

@@ -10,7 +10,6 @@ pub(crate) struct Stylesheet {
pub(crate) line_no: Style,
pub(crate) emphasis: Style,
pub(crate) none: Style,
pub(crate) hyperlink: bool,
}
impl Default for Stylesheet {
@@ -30,7 +29,6 @@ impl Stylesheet {
line_no: Style::new(),
emphasis: Style::new(),
none: Style::new(),
hyperlink: false,
}
}
}

View File

@@ -12,19 +12,13 @@
use std::ops::Range;
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub(crate) struct Id<'a> {
pub(crate) id: &'a str,
pub(crate) url: Option<&'a str>,
}
/// Primary structure provided for formatting
///
/// See [`Level::title`] to create a [`Message`]
#[derive(Debug)]
pub struct Message<'a> {
pub(crate) level: Level,
pub(crate) id: Option<Id<'a>>,
pub(crate) id: Option<&'a str>,
pub(crate) title: &'a str,
pub(crate) snippets: Vec<Snippet<'a>>,
pub(crate) footer: Vec<Message<'a>>,
@@ -34,12 +28,7 @@ pub struct Message<'a> {
impl<'a> Message<'a> {
pub fn id(mut self, id: &'a str) -> Self {
self.id = Some(Id { id, url: None });
self
}
pub fn id_with_url(mut self, id: &'a str, url: Option<&'a str>) -> Self {
self.id = Some(Id { id, url });
self.id = Some(id);
self
}

View File

@@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
120,
110,
);
bench_project(&benchmark, criterion);

View File

@@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
600,
525,
);
static PANDAS: Benchmark = Benchmark::new(
@@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
4000,
3000,
);
static PYDANTIC: Benchmark = Benchmark::new(
@@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
7000,
5000,
);
static SYMPY: Benchmark = Benchmark::new(

View File

@@ -42,7 +42,6 @@ schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
similar = { workspace = true }
supports-hyperlinks = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, optional = true }

View File

@@ -64,8 +64,6 @@ impl Diagnostic {
id,
severity,
message: message.into_diagnostic_message(),
custom_concise_message: None,
documentation_url: None,
annotations: vec![],
subs: vec![],
fix: None,
@@ -215,10 +213,6 @@ impl Diagnostic {
/// cases, just converting it to a string (or printing it) will do what
/// you want.
pub fn concise_message(&self) -> ConciseMessage<'_> {
if let Some(custom_message) = &self.inner.custom_concise_message {
return ConciseMessage::Custom(custom_message.as_str());
}
let main = self.inner.message.as_str();
let annotation = self
.primary_annotation()
@@ -232,15 +226,6 @@ impl Diagnostic {
}
}
/// Set a custom message for the concise formatting of this diagnostic.
///
/// This overrides the default behavior of generating a concise message
/// from the main diagnostic message and the primary annotation.
pub fn set_concise_message(&mut self, message: impl IntoDiagnosticMessage) {
Arc::make_mut(&mut self.inner).custom_concise_message =
Some(message.into_diagnostic_message());
}
/// Returns the severity of this diagnostic.
///
/// Note that this may be different than the severity of sub-diagnostics.
@@ -371,14 +356,6 @@ impl Diagnostic {
.is_some_and(|fix| fix.applies(config.fix_applicability))
}
pub fn documentation_url(&self) -> Option<&str> {
self.inner.documentation_url.as_deref()
}
pub fn set_documentation_url(&mut self, url: Option<String>) {
Arc::make_mut(&mut self.inner).documentation_url = url;
}
/// Returns the offset of the parent statement for this diagnostic if it exists.
///
/// This is primarily used for checking noqa/secondary code suppressions.
@@ -452,6 +429,28 @@ impl Diagnostic {
.map(|sub| sub.inner.message.as_str())
}
/// Returns the URL for the rule documentation, if it exists.
pub fn to_ruff_url(&self) -> Option<String> {
match self.id() {
DiagnosticId::Panic
| DiagnosticId::Io
| DiagnosticId::InvalidSyntax
| DiagnosticId::RevealedType
| DiagnosticId::UnknownRule
| DiagnosticId::InvalidGlob
| DiagnosticId::EmptyInclude
| DiagnosticId::UnnecessaryOverridesSection
| DiagnosticId::UselessOverridesSection
| DiagnosticId::DeprecatedSetting
| DiagnosticId::Unformatted
| DiagnosticId::InvalidCliOption
| DiagnosticId::InternalError => None,
DiagnosticId::Lint(lint_name) => {
Some(format!("{}/rules/{lint_name}", env!("CARGO_PKG_HOMEPAGE")))
}
}
}
/// Returns the filename for the message.
///
/// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`.
@@ -531,10 +530,8 @@ impl Diagnostic {
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
struct DiagnosticInner {
id: DiagnosticId,
documentation_url: Option<String>,
severity: Severity,
message: DiagnosticMessage,
custom_concise_message: Option<DiagnosticMessage>,
annotations: Vec<Annotation>,
subs: Vec<SubDiagnostic>,
fix: Option<Fix>,
@@ -1523,8 +1520,6 @@ pub enum ConciseMessage<'a> {
/// This indicates that the diagnostic is probably using the old
/// model.
Empty,
/// A custom concise message has been provided.
Custom(&'a str),
}
impl std::fmt::Display for ConciseMessage<'_> {
@@ -1540,9 +1535,6 @@ impl std::fmt::Display for ConciseMessage<'_> {
write!(f, "{main}: {annotation}")
}
ConciseMessage::Empty => Ok(()),
ConciseMessage::Custom(message) => {
write!(f, "{message}")
}
}
}
}

View File

@@ -205,7 +205,6 @@ impl<'a> Resolved<'a> {
struct ResolvedDiagnostic<'a> {
level: AnnotateLevel,
id: Option<String>,
documentation_url: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
is_fixable: bool,
@@ -241,12 +240,12 @@ impl<'a> ResolvedDiagnostic<'a> {
// `DisplaySet::format_annotation` for both cases, but this is a small hack to improve
// the formatting of syntax errors for now. This should also be kept consistent with the
// concise formatting.
diag.secondary_code().map_or_else(
Some(diag.secondary_code().map_or_else(
|| format!("{id}:", id = diag.inner.id),
|code| code.to_string(),
)
))
} else {
diag.inner.id.to_string()
Some(diag.inner.id.to_string())
};
let level = if config.hide_severity {
@@ -257,8 +256,7 @@ impl<'a> ResolvedDiagnostic<'a> {
ResolvedDiagnostic {
level,
id: Some(id),
documentation_url: diag.documentation_url().map(ToString::to_string),
id,
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: config.show_fix_status && diag.has_applicable_fix(config),
@@ -289,7 +287,6 @@ impl<'a> ResolvedDiagnostic<'a> {
ResolvedDiagnostic {
level: diag.inner.severity.to_annotate(),
id: None,
documentation_url: None,
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: false,
@@ -388,7 +385,6 @@ impl<'a> ResolvedDiagnostic<'a> {
RenderableDiagnostic {
level: self.level,
id: self.id.as_deref(),
documentation_url: self.documentation_url.as_deref(),
message: &self.message,
snippets_by_input,
is_fixable: self.is_fixable,
@@ -489,7 +485,6 @@ struct RenderableDiagnostic<'r> {
/// An ID is always present for top-level diagnostics and always absent for
/// sub-diagnostics.
id: Option<&'r str>,
documentation_url: Option<&'r str>,
/// The message emitted with the diagnostic, before any snippets are
/// rendered.
message: &'r str,
@@ -524,7 +519,7 @@ impl RenderableDiagnostic<'_> {
.is_fixable(self.is_fixable)
.lineno_offset(self.header_offset);
if let Some(id) = self.id {
message = message.id_with_url(id, self.documentation_url);
message = message.id(id);
}
message.snippets(snippets)
}
@@ -2881,12 +2876,6 @@ watermelon
self.diag.help(message);
self
}
/// Set the documentation URL for the diagnostic.
pub(super) fn documentation_url(mut self, url: impl Into<String>) -> DiagnosticBuilder<'e> {
self.diag.set_documentation_url(Some(url.into()));
self
}
}
/// A helper builder for tersely populating a `SubDiagnostic`.
@@ -3001,7 +2990,6 @@ def fibonacci(n):
TextSize::from(10),
))))
.noqa_offset(TextSize::from(7))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-variable",
@@ -3016,13 +3004,11 @@ def fibonacci(n):
TextSize::from(99),
)))
.noqa_offset(TextSize::from(94))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
.build(),
env.builder("undefined-name", Severity::Error, "Undefined name `a`")
.primary("undef.py", "1:3", "1:4", "")
.secondary_code("F821")
.noqa_offset(TextSize::from(3))
.documentation_url("https://docs.astral.sh/ruff/rules/undefined-name")
.build(),
];
@@ -3137,7 +3123,6 @@ if call(foo
TextSize::from(19),
))))
.noqa_offset(TextSize::from(16))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-import",
@@ -3152,7 +3137,6 @@ if call(foo
TextSize::from(40),
))))
.noqa_offset(TextSize::from(35))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-variable",
@@ -3167,7 +3151,6 @@ if call(foo
TextSize::from(104),
))))
.noqa_offset(TextSize::from(98))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
.build(),
];

View File

@@ -1,6 +1,6 @@
use crate::diagnostic::{
Diagnostic, DisplayDiagnosticConfig, Severity,
stylesheet::{DiagnosticStylesheet, fmt_styled, fmt_with_hyperlink},
stylesheet::{DiagnosticStylesheet, fmt_styled},
};
use super::FileResolver;
@@ -62,29 +62,18 @@ impl<'a> ConciseRenderer<'a> {
}
write!(f, "{sep} ")?;
}
if self.config.hide_severity {
if let Some(code) = diag.secondary_code() {
write!(
f,
"{code} ",
code = fmt_styled(
fmt_with_hyperlink(&code, diag.documentation_url(), &stylesheet),
stylesheet.secondary_code
)
code = fmt_styled(code, stylesheet.secondary_code)
)?;
} else {
write!(
f,
"{id}: ",
id = fmt_styled(
fmt_with_hyperlink(
&diag.inner.id,
diag.documentation_url(),
&stylesheet
),
stylesheet.secondary_code
)
id = fmt_styled(diag.inner.id.as_str(), stylesheet.secondary_code)
)?;
}
if self.config.show_fix_status {
@@ -104,10 +93,7 @@ impl<'a> ConciseRenderer<'a> {
f,
"{severity}[{id}] ",
severity = fmt_styled(severity, severity_style),
id = fmt_styled(
fmt_with_hyperlink(&diag.id(), diag.documentation_url(), &stylesheet),
stylesheet.emphasis
)
id = fmt_styled(diag.id(), stylesheet.emphasis)
)?;
}

View File

@@ -49,8 +49,7 @@ impl<'a> FullRenderer<'a> {
.help(stylesheet.help)
.line_no(stylesheet.line_no)
.emphasis(stylesheet.emphasis)
.none(stylesheet.none)
.hyperlink(stylesheet.hyperlink);
.none(stylesheet.none);
for diag in diagnostics {
let resolved = Resolved::new(self.resolver, diag, self.config);
@@ -704,7 +703,52 @@ print()
env.show_fix_status(true);
env.fix_applicability(Applicability::DisplayOnly);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
error[unused-import][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
error[unused-variable][*]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior
");
}
#[test]
@@ -724,7 +768,31 @@ print()
}
*fix = Fix::unsafe_edits(edits.remove(0), edits);
insta::assert_snapshot!(env.render(&diagnostic));
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior
");
}
/// Carriage return (`\r`) is a valid line-ending in Python, so we should normalize this to a

View File

@@ -100,7 +100,7 @@ pub(super) fn diagnostic_to_json<'a>(
if config.preview {
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.documentation_url(),
url: diagnostic.to_ruff_url(),
message: diagnostic.body(),
fix,
cell: notebook_cell_index,
@@ -112,7 +112,7 @@ pub(super) fn diagnostic_to_json<'a>(
} else {
JsonDiagnostic {
code: diagnostic.secondary_code_or_id(),
url: diagnostic.documentation_url(),
url: diagnostic.to_ruff_url(),
message: diagnostic.body(),
fix,
cell: notebook_cell_index,
@@ -228,7 +228,7 @@ pub(crate) struct JsonDiagnostic<'a> {
location: Option<JsonLocation>,
message: &'a str,
noqa_row: Option<OneIndexed>,
url: Option<&'a str>,
url: Option<String>,
}
#[derive(Serialize)]
@@ -294,10 +294,7 @@ mod tests {
env.format(DiagnosticFormat::Json);
env.preview(false);
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
let diag = env.err().build();
insta::assert_snapshot!(
env.render(&diag),
@@ -331,10 +328,7 @@ mod tests {
env.format(DiagnosticFormat::Json);
env.preview(true);
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
let diag = env.err().build();
insta::assert_snapshot!(
env.render(&diag),

View File

@@ -82,7 +82,7 @@ fn diagnostic_to_rdjson<'a>(
value: diagnostic
.secondary_code()
.map_or_else(|| diagnostic.name(), |code| code.as_str()),
url: diagnostic.documentation_url(),
url: diagnostic.to_ruff_url(),
},
suggestions: rdjson_suggestions(
edits,
@@ -182,7 +182,7 @@ impl RdjsonRange {
#[derive(Serialize)]
struct RdjsonCode<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<&'a str>,
url: Option<String>,
value: &'a str,
}
@@ -217,10 +217,7 @@ mod tests {
env.format(DiagnosticFormat::Rdjson);
env.preview(false);
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
let diag = env.err().build();
insta::assert_snapshot!(env.render(&diag));
}
@@ -231,10 +228,7 @@ mod tests {
env.format(DiagnosticFormat::Rdjson);
env.preview(true);
let diag = env
.err()
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
.build();
let diag = env.err().build();
insta::assert_snapshot!(env.render(&diag));
}

View File

@@ -1,48 +0,0 @@
---
source: crates/ruff_db/src/diagnostic/render/full.rs
expression: env.render_diagnostics(&diagnostics)
---
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
error[unused-import][*]: `math` imported but unused
--> notebook.ipynb:cell 2:2:8
|
1 | # cell 2
2 | import math
| ^^^^
3 |
4 | print('hello world')
|
help: Remove unused import: `math`
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
error[unused-variable][*]: Local variable `x` is assigned to but never used
--> notebook.ipynb:cell 3:4:5
|
2 | def foo():
3 | print()
4 | x = 1
| ^
|
help: Remove assignment to unused variable `x`
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,27 +0,0 @@
---
source: crates/ruff_db/src/diagnostic/render/full.rs
expression: env.render(&diagnostic)
---
error[unused-import][*]: `os` imported but unused
--> notebook.ipynb:cell 1:2:8
|
1 | # cell 1
2 | import os
| ^^
|
help: Remove unused import: `os`
::: cell 1
1 | # cell 1
- import os
::: cell 2
1 | # cell 2
- import math
2 |
3 | print('hello world')
::: cell 3
1 | # cell 3
2 | def foo():
3 | print()
- x = 1
4 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -31,43 +31,6 @@ where
FmtStyled { content, style }
}
pub(super) fn fmt_with_hyperlink<'a, T>(
content: T,
url: Option<&'a str>,
stylesheet: &DiagnosticStylesheet,
) -> impl std::fmt::Display + 'a
where
T: std::fmt::Display + 'a,
{
struct FmtHyperlink<'a, T> {
content: T,
url: Option<&'a str>,
}
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(url) = self.url {
write!(f, "\x1B]8;;{url}\x1B\\")?;
}
self.content.fmt(f)?;
if self.url.is_some() {
f.write_str("\x1B]8;;\x1B\\")?;
}
Ok(())
}
}
let url = if stylesheet.hyperlink { url } else { None };
FmtHyperlink { content, url }
}
#[derive(Clone, Debug)]
pub struct DiagnosticStylesheet {
pub(crate) error: Style,
@@ -84,7 +47,6 @@ pub struct DiagnosticStylesheet {
pub(crate) deletion: Style,
pub(crate) insertion_line_no: Style,
pub(crate) deletion_line_no: Style,
pub(crate) hyperlink: bool,
}
impl Default for DiagnosticStylesheet {
@@ -97,8 +59,6 @@ impl DiagnosticStylesheet {
/// Default terminal styling
pub fn styled() -> Self {
let bright_blue = AnsiColor::BrightBlue.on_default();
let hyperlink = supports_hyperlinks::supports_hyperlinks();
Self {
error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD),
warning: AnsiColor::Yellow.on_default().effects(Effects::BOLD),
@@ -114,7 +74,6 @@ impl DiagnosticStylesheet {
deletion: AnsiColor::Red.on_default(),
insertion_line_no: AnsiColor::Green.on_default().effects(Effects::BOLD),
deletion_line_no: AnsiColor::Red.on_default().effects(Effects::BOLD),
hyperlink,
}
}
@@ -134,7 +93,6 @@ impl DiagnosticStylesheet {
deletion: Style::new(),
insertion_line_no: Style::new(),
deletion_line_no: Style::new(),
hyperlink: false,
}
}
}

View File

@@ -4,31 +4,3 @@ CommunityData("public", mpModel=0) # S508
CommunityData("public", mpModel=1) # S508
CommunityData("public", mpModel=2) # OK
# New API paths
import pysnmp.hlapi.asyncio
import pysnmp.hlapi.v1arch
import pysnmp.hlapi.v1arch.asyncio
import pysnmp.hlapi.v1arch.asyncio.auth
import pysnmp.hlapi.v3arch
import pysnmp.hlapi.v3arch.asyncio
import pysnmp.hlapi.v3arch.asyncio.auth
import pysnmp.hlapi.auth
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=2) # OK
pysnmp.hlapi.auth.CommunityData("public", mpModel=2) # OK

View File

@@ -5,19 +5,3 @@ insecure = UsmUserData("securityName") # S509
auth_no_priv = UsmUserData("securityName", "authName") # S509
less_insecure = UsmUserData("securityName", "authName", "privName") # OK
# New API paths
import pysnmp.hlapi.asyncio
import pysnmp.hlapi.v3arch.asyncio
import pysnmp.hlapi.v3arch.asyncio.auth
import pysnmp.hlapi.auth
pysnmp.hlapi.asyncio.UsmUserData("user") # S509
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
pysnmp.hlapi.auth.UsmUserData("user") # S509
pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user", "authkey", "privkey") # OK
pysnmp.hlapi.auth.UsmUserData("user", "authkey", "privkey") # OK

View File

@@ -16,19 +16,3 @@ logging.warning("%s", str(**{"object": b"\xf0\x9f\x9a\xa8", "encoding": "utf-8"}
# str() with single keyword argument - should be flagged (equivalent to str("!"))
logging.warning("%s", str(object="!"))
# Complex conversion specifiers that make oct() and hex() necessary
# These should NOT be flagged because the behavior differs between %s and %#o/%#x
# https://github.com/astral-sh/ruff/issues/21458
# %06s with oct() - zero-pad flag with width (should NOT be flagged)
logging.warning("%06s", oct(123))
# % s with oct() - blank sign flag (should NOT be flagged)
logging.warning("% s", oct(123))
# %+s with oct() - sign char flag (should NOT be flagged)
logging.warning("%+s", oct(123))
# %.3s with hex() - precision (should NOT be flagged)
logging.warning("%.3s", hex(123))

View File

@@ -125,7 +125,6 @@ where
}
diagnostic.set_secondary_code(SecondaryCode::new(rule.noqa_code().to_string()));
diagnostic.set_documentation_url(rule.url());
diagnostic
}

View File

@@ -270,11 +270,6 @@ pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &Linter
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21374
pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/21395
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()

View File

@@ -104,8 +104,6 @@ mod tests {
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -4,7 +4,6 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_extended_snmp_api_path_detection_enabled;
/// ## What it does
/// Checks for uses of SNMPv1 or SNMPv2.
@@ -48,17 +47,10 @@ pub(crate) fn snmp_insecure_version(checker: &Checker, call: &ast::ExprCall) {
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", .., "CommunityData"]
)
} else {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "CommunityData"]
)
}
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "CommunityData"]
)
})
{
if let Some(keyword) = call.arguments.find_keyword("mpModel") {

View File

@@ -4,7 +4,6 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_extended_snmp_api_path_detection_enabled;
/// ## What it does
/// Checks for uses of the SNMPv3 protocol without encryption.
@@ -48,17 +47,10 @@ pub(crate) fn snmp_weak_cryptography(checker: &Checker, call: &ast::ExprCall) {
.semantic()
.resolve_qualified_name(&call.func)
.is_some_and(|qualified_name| {
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", .., "UsmUserData"]
)
} else {
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "UsmUserData"]
)
}
matches!(
qualified_name.segments(),
["pysnmp", "hlapi", "UsmUserData"]
)
})
{
checker.report_diagnostic(SnmpWeakCryptography, call.func.range());

View File

@@ -1,108 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:3:25
|
1 | from pysnmp.hlapi import CommunityData
2 |
3 | CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
4 | CommunityData("public", mpModel=1) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:4:25
|
3 | CommunityData("public", mpModel=0) # S508
4 | CommunityData("public", mpModel=1) # S508
| ^^^^^^^^^
5 |
6 | CommunityData("public", mpModel=2) # OK
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:18:46
|
16 | import pysnmp.hlapi.auth
17 |
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:19:58
|
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:20:53
|
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:21:45
|
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:22:58
|
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:23:53
|
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:24:45
|
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
--> S508.py:25:43
|
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
| ^^^^^^^^^
26 |
27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
|

View File

@@ -1,62 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:4:12
|
4 | insecure = UsmUserData("securityName") # S509
| ^^^^^^^^^^^
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:5:16
|
4 | insecure = UsmUserData("securityName") # S509
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
| ^^^^^^^^^^^
6 |
7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:15:1
|
13 | import pysnmp.hlapi.auth
14 |
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:16:1
|
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:17:1
|
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
--> S509.py:18:1
|
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
19 |
20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
|

View File

@@ -2,9 +2,7 @@ use std::str::FromStr;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_literal::cformat::{
CConversionFlags, CFormatPart, CFormatSpec, CFormatString, CFormatType,
};
use ruff_python_literal::cformat::{CFormatPart, CFormatString, CFormatType};
use ruff_python_literal::format::FormatConversion;
use ruff_text_size::Ranged;
@@ -197,8 +195,7 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
// %s with oct() - suggest using %#o instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "oct")
&& !has_complex_conversion_specifier(spec) =>
if checker.semantic().match_builtin_expr(func.as_ref(), "oct") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
@@ -210,8 +207,7 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
// %s with hex() - suggest using %#x instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "hex")
&& !has_complex_conversion_specifier(spec) =>
if checker.semantic().match_builtin_expr(func.as_ref(), "hex") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
@@ -226,23 +222,3 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
}
}
}
/// Check if a conversion specifier has complex flags or precision that make `oct()` or `hex()` necessary.
///
/// Returns `true` if any of these conditions are met:
/// - Flag `0` (zero-pad) is used, flag `-` (left-adjust) is not used, and minimum width is specified
/// - Flag ` ` (blank sign) is used
/// - Flag `+` (sign char) is used
/// - Precision is specified
fn has_complex_conversion_specifier(spec: &CFormatSpec) -> bool {
if spec.flags.intersects(CConversionFlags::ZERO_PAD)
&& !spec.flags.intersects(CConversionFlags::LEFT_ADJUST)
&& spec.min_field_width.is_some()
{
return true;
}
spec.flags
.intersects(CConversionFlags::BLANK_SIGN | CConversionFlags::SIGN_CHAR)
|| spec.precision.is_some()
}

View File

@@ -193,58 +193,3 @@ def foo():
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
if '' and (not #
0):
pass
if '' and (not #
(0)
):
pass
if '' and (not
( #
0
)):
pass
if (
not
# comment
(a)):
pass
if not ( # comment
a):
pass
if not (
# comment
(a)):
pass
if not (
# comment
a):
pass
not (# comment
(a))
(-#comment
(a))
if ( # a
# b
not # c
# d
( # e
# f
a # g
# h
) # i
# j
):
pass

View File

@@ -1,149 +0,0 @@
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans \
and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else: print("this")
while foo(): print(i) # fmt: skip
else: print("this")
# again but only the second skip
for i in range(2): print(i)
else: print("this") # fmt: skip
while foo(): print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it: foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip

View File

@@ -1890,11 +1890,9 @@ fn handle_lambda_comment<'a>(
CommentPlacement::Default(comment)
}
/// Move an end-of-line comment between a unary op and its operand after the operand by marking
/// it as dangling.
/// Move comment between a unary op and its operand before the unary op by marking them as trailing.
///
/// For example, given:
///
/// ```python
/// (
/// not # comment
@@ -1902,13 +1900,8 @@ fn handle_lambda_comment<'a>(
/// )
/// ```
///
/// the `# comment` will be attached as a dangling comment on the unary op and formatted as:
///
/// ```python
/// (
/// not True # comment
/// )
/// ```
/// The `# comment` will be attached as a dangling comment on the enclosing node, to ensure that
/// it remains on the same line as the operator.
fn handle_unary_op_comment<'a>(
comment: DecoratedComment<'a>,
unary_op: &'a ast::ExprUnaryOp,
@@ -1930,8 +1923,8 @@ fn handle_unary_op_comment<'a>(
let up_to = tokenizer
.find(|token| token.kind == SimpleTokenKind::LParen)
.map_or(unary_op.operand.start(), |lparen| lparen.start());
if comment.end() < up_to && comment.line_position().is_end_of_line() {
CommentPlacement::dangling(unary_op, comment)
if comment.end() < up_to {
CommentPlacement::leading(unary_op, comment)
} else {
CommentPlacement::Default(comment)
}

View File

@@ -1,8 +1,6 @@
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprUnaryOp;
use ruff_python_ast::UnaryOp;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_text_size::Ranged;
use crate::comments::trailing_comments;
use crate::expression::parentheses::{
@@ -41,25 +39,20 @@ impl FormatNodeRule<ExprUnaryOp> for FormatExprUnaryOp {
// ```
trailing_comments(dangling).fmt(f)?;
// Insert a line break if the operand has comments but itself is not parenthesized or if the
// operand is parenthesized but has a leading comment before the parentheses.
// Insert a line break if the operand has comments but itself is not parenthesized.
// ```python
// if (
// not
// # comment
// a):
// pass
//
// if 1 and (
// not
// # comment
// (
// a
// )
// ):
// pass
// a)
// ```
if needs_line_break(item, f.context()) {
if comments.has_leading(operand.as_ref())
&& !is_expression_parenthesized(
operand.as_ref().into(),
f.context().comments().ranges(),
f.context().source(),
)
{
hard_line_break().fmt(f)?;
} else if op.is_not() {
space().fmt(f)?;
@@ -83,51 +76,17 @@ impl NeedsParentheses for ExprUnaryOp {
context: &PyFormatContext,
) -> OptionalParentheses {
if parent.is_expr_await() {
return OptionalParentheses::Always;
}
if needs_line_break(self, context) {
return OptionalParentheses::Always;
}
if is_expression_parenthesized(
OptionalParentheses::Always
} else if is_expression_parenthesized(
self.operand.as_ref().into(),
context.comments().ranges(),
context.source(),
) {
return OptionalParentheses::Never;
OptionalParentheses::Never
} else if context.comments().has(self.operand.as_ref()) {
OptionalParentheses::Always
} else {
self.operand.needs_parentheses(self.into(), context)
}
if context.comments().has(self.operand.as_ref()) {
return OptionalParentheses::Always;
}
self.operand.needs_parentheses(self.into(), context)
}
}
/// Returns `true` if the unary operator will have a hard line break between the operator and its
/// operand and thus requires parentheses.
fn needs_line_break(item: &ExprUnaryOp, context: &PyFormatContext) -> bool {
let comments = context.comments();
let parenthesized_operand_range = parenthesized_range(
item.operand.as_ref().into(),
item.into(),
comments.ranges(),
context.source(),
);
let leading_operand_comments = comments.leading(item.operand.as_ref());
let has_leading_comments_before_parens = parenthesized_operand_range.is_some_and(|range| {
leading_operand_comments
.iter()
.any(|comment| comment.start() < range.start())
});
!leading_operand_comments.is_empty()
&& !is_expression_parenthesized(
item.operand.as_ref().into(),
context.comments().ranges(),
context.source(),
)
|| has_leading_comments_before_parens
}

View File

@@ -7,7 +7,7 @@ use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::preview::is_remove_parens_around_except_types_enabled;
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Copy, Clone, Default)]
@@ -55,68 +55,77 @@ impl FormatNodeRule<ExceptHandlerExceptHandler> for FormatExceptHandlerExceptHan
write!(
f,
[clause(
ClauseHeader::ExceptHandler(item),
&format_with(|f: &mut PyFormatter| {
write!(
f,
[
token("except"),
match except_handler_kind {
ExceptHandlerKind::Regular => None,
ExceptHandlerKind::Starred => Some(token("*")),
}
]
)?;
[
clause_header(
ClauseHeader::ExceptHandler(item),
dangling_comments,
&format_with(|f: &mut PyFormatter| {
write!(
f,
[
token("except"),
match except_handler_kind {
ExceptHandlerKind::Regular => None,
ExceptHandlerKind::Starred => Some(token("*")),
}
]
)?;
match type_.as_deref() {
// For tuples of exception types without an `as` name and on 3.14+, the
// parentheses are optional.
//
// ```py
// try:
// ...
// except BaseException, Exception: # Ok
// ...
// ```
Some(Expr::Tuple(tuple))
if f.options().target_version() >= PythonVersion::PY314
&& is_remove_parens_around_except_types_enabled(f.context())
&& name.is_none() =>
{
write!(
f,
[
space(),
tuple.format().with_options(TupleParentheses::NeverPreserve)
]
)?;
}
Some(type_) => {
write!(
f,
[
space(),
maybe_parenthesize_expression(
type_,
item,
Parenthesize::IfBreaks
match type_.as_deref() {
// For tuples of exception types without an `as` name and on 3.14+, the
// parentheses are optional.
//
// ```py
// try:
// ...
// except BaseException, Exception: # Ok
// ...
// ```
Some(Expr::Tuple(tuple))
if f.options().target_version() >= PythonVersion::PY314
&& is_remove_parens_around_except_types_enabled(
f.context(),
)
]
)?;
if let Some(name) = name {
write!(f, [space(), token("as"), space(), name.format()])?;
&& name.is_none() =>
{
write!(
f,
[
space(),
tuple
.format()
.with_options(TupleParentheses::NeverPreserve)
]
)?;
}
Some(type_) => {
write!(
f,
[
space(),
maybe_parenthesize_expression(
type_,
item,
Parenthesize::IfBreaks
)
]
)?;
if let Some(name) = name {
write!(f, [space(), token("as"), space(), name.format()])?;
}
}
_ => {}
}
_ => {}
}
Ok(())
}),
dangling_comments,
body,
SuiteKind::other(self.last_suite_in_statement),
)]
Ok(())
}),
),
clause_body(
body,
SuiteKind::other(self.last_suite_in_statement),
dangling_comments
),
]
)
}
}

View File

@@ -5,7 +5,7 @@ use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::pattern::maybe_parenthesize_pattern;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -46,18 +46,23 @@ impl FormatNodeRule<MatchCase> for FormatMatchCase {
write!(
f,
[clause(
ClauseHeader::MatchCase(item),
&format_args![
token("case"),
space(),
maybe_parenthesize_pattern(pattern, item),
format_guard
],
dangling_item_comments,
body,
SuiteKind::other(self.last_suite_in_statement),
)]
[
clause_header(
ClauseHeader::MatchCase(item),
dangling_item_comments,
&format_args![
token("case"),
space(),
maybe_parenthesize_pattern(pattern, item),
format_guard
],
),
clause_body(
body,
SuiteKind::other(self.last_suite_in_statement),
dangling_item_comments
),
]
)
}
}

View File

@@ -5,12 +5,11 @@ use ruff_python_ast::{
StmtIf, StmtMatch, StmtTry, StmtWhile, StmtWith, Suite,
};
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments};
use crate::statement::suite::{SuiteKind, as_only_an_ellipsis};
use crate::verbatim::{verbatim_text, write_suppressed_clause_header};
use crate::verbatim::write_suppressed_clause_header;
use crate::{has_skip_comment, prelude::*};
/// The header of a compound statement clause.
@@ -37,41 +36,7 @@ pub(crate) enum ClauseHeader<'a> {
OrElse(ElseClause<'a>),
}
impl<'a> ClauseHeader<'a> {
/// Returns the last child in the clause body immediately following this clause header.
///
/// For most clauses, this is the last statement in
/// the primary body. For clauses like `try`, it specifically returns the last child
/// in the `try` body, not the `except`/`else`/`finally` clauses.
///
/// This is similar to [`ruff_python_ast::AnyNodeRef::last_child_in_body`]
/// but restricted to the clause.
pub(crate) fn last_child_in_clause(self) -> Option<AnyNodeRef<'a>> {
match self {
ClauseHeader::Class(StmtClassDef { body, .. })
| ClauseHeader::Function(StmtFunctionDef { body, .. })
| ClauseHeader::If(StmtIf { body, .. })
| ClauseHeader::ElifElse(ElifElseClause { body, .. })
| ClauseHeader::Try(StmtTry { body, .. })
| ClauseHeader::MatchCase(MatchCase { body, .. })
| ClauseHeader::For(StmtFor { body, .. })
| ClauseHeader::While(StmtWhile { body, .. })
| ClauseHeader::With(StmtWith { body, .. })
| ClauseHeader::ExceptHandler(ExceptHandlerExceptHandler { body, .. })
| ClauseHeader::OrElse(
ElseClause::Try(StmtTry { orelse: body, .. })
| ElseClause::For(StmtFor { orelse: body, .. })
| ElseClause::While(StmtWhile { orelse: body, .. }),
)
| ClauseHeader::TryFinally(StmtTry {
finalbody: body, ..
}) => body.last().map(AnyNodeRef::from),
ClauseHeader::Match(StmtMatch { cases, .. }) => cases
.last()
.and_then(|case| case.body.last().map(AnyNodeRef::from)),
}
}
impl ClauseHeader<'_> {
/// The range from the clause keyword up to and including the final colon.
pub(crate) fn range(self, source: &str) -> FormatResult<TextRange> {
let keyword_range = self.first_keyword_range(source)?;
@@ -373,28 +338,6 @@ impl<'a> ClauseHeader<'a> {
}
}
impl<'a> From<ClauseHeader<'a>> for AnyNodeRef<'a> {
fn from(value: ClauseHeader<'a>) -> Self {
match value {
ClauseHeader::Class(stmt_class_def) => stmt_class_def.into(),
ClauseHeader::Function(stmt_function_def) => stmt_function_def.into(),
ClauseHeader::If(stmt_if) => stmt_if.into(),
ClauseHeader::ElifElse(elif_else_clause) => elif_else_clause.into(),
ClauseHeader::Try(stmt_try) => stmt_try.into(),
ClauseHeader::ExceptHandler(except_handler_except_handler) => {
except_handler_except_handler.into()
}
ClauseHeader::TryFinally(stmt_try) => stmt_try.into(),
ClauseHeader::Match(stmt_match) => stmt_match.into(),
ClauseHeader::MatchCase(match_case) => match_case.into(),
ClauseHeader::For(stmt_for) => stmt_for.into(),
ClauseHeader::While(stmt_while) => stmt_while.into(),
ClauseHeader::With(stmt_with) => stmt_with.into(),
ClauseHeader::OrElse(else_clause) => else_clause.into(),
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum ElseClause<'a> {
Try(&'a StmtTry),
@@ -402,16 +345,6 @@ pub(crate) enum ElseClause<'a> {
While(&'a StmtWhile),
}
impl<'a> From<ElseClause<'a>> for AnyNodeRef<'a> {
fn from(value: ElseClause<'a>) -> Self {
match value {
ElseClause::Try(stmt_try) => stmt_try.into(),
ElseClause::For(stmt_for) => stmt_for.into(),
ElseClause::While(stmt_while) => stmt_while.into(),
}
}
}
pub(crate) struct FormatClauseHeader<'a, 'ast> {
header: ClauseHeader<'a>,
/// How to format the clause header
@@ -445,6 +378,22 @@ where
}
}
impl<'a> FormatClauseHeader<'a, '_> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = self.leading_comments {
@@ -474,13 +423,13 @@ impl<'ast> Format<PyFormatContext<'ast>> for FormatClauseHeader<'_, 'ast> {
}
}
struct FormatClauseBody<'a> {
pub(crate) struct FormatClauseBody<'a> {
body: &'a Suite,
kind: SuiteKind,
trailing_comments: &'a [SourceComment],
}
fn clause_body<'a>(
pub(crate) fn clause_body<'a>(
body: &'a Suite,
kind: SuiteKind,
trailing_comments: &'a [SourceComment],
@@ -516,84 +465,6 @@ impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
}
}
pub(crate) struct FormatClause<'a, 'ast> {
header: ClauseHeader<'a>,
/// How to format the clause header
header_formatter: Argument<'a, PyFormatContext<'ast>>,
/// Leading comments coming before the branch, together with the previous node, if any. Only relevant
/// for alternate branches.
leading_comments: Option<(&'a [SourceComment], Option<AnyNodeRef<'a>>)>,
/// The trailing comments coming after the colon.
trailing_colon_comment: &'a [SourceComment],
body: &'a Suite,
kind: SuiteKind,
}
impl<'a, 'ast> FormatClause<'a, 'ast> {
/// Sets the leading comments that precede an alternate branch.
#[must_use]
pub(crate) fn with_leading_comments<N>(
mut self,
comments: &'a [SourceComment],
last_node: Option<N>,
) -> Self
where
N: Into<AnyNodeRef<'a>>,
{
self.leading_comments = Some((comments, last_node.map(Into::into)));
self
}
fn clause_header(&self) -> FormatClauseHeader<'a, 'ast> {
FormatClauseHeader {
header: self.header,
formatter: self.header_formatter,
leading_comments: self.leading_comments,
trailing_colon_comment: self.trailing_colon_comment,
}
}
fn clause_body(&self) -> FormatClauseBody<'a> {
clause_body(self.body, self.kind, self.trailing_colon_comment)
}
}
/// Formats a clause, handling the case where the compound
/// statement lies on a single line with `# fmt: skip` and
/// should be suppressed.
pub(crate) fn clause<'a, 'ast, Content>(
header: ClauseHeader<'a>,
header_formatter: &'a Content,
trailing_colon_comment: &'a [SourceComment],
body: &'a Suite,
kind: SuiteKind,
) -> FormatClause<'a, 'ast>
where
Content: Format<PyFormatContext<'ast>>,
{
FormatClause {
header,
header_formatter: Argument::new(header_formatter),
leading_comments: None,
trailing_colon_comment,
body,
kind,
}
}
impl<'ast> Format<PyFormatContext<'ast>> for FormatClause<'_, 'ast> {
fn fmt(&self, f: &mut Formatter<PyFormatContext<'ast>>) -> FormatResult<()> {
match should_suppress_clause(self, f)? {
SuppressClauseHeader::Yes {
last_child_in_clause,
} => write_suppressed_clause(self, f, last_child_in_clause),
SuppressClauseHeader::No => {
write!(f, [self.clause_header(), self.clause_body()])
}
}
}
}
/// Finds the range of `keyword` starting the search at `start_position`.
///
/// If the start position is at the end of the previous statement, the
@@ -716,96 +587,3 @@ fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResu
}
}
}
fn should_suppress_clause<'a>(
clause: &FormatClause<'a, '_>,
f: &mut Formatter<PyFormatContext<'_>>,
) -> FormatResult<SuppressClauseHeader<'a>> {
let source = f.context().source();
let Some(last_child_in_clause) = clause.header.last_child_in_clause() else {
return Ok(SuppressClauseHeader::No);
};
// Early return if we don't have a skip comment
// to avoid computing header range in the common case
if !has_skip_comment(
f.context().comments().trailing(last_child_in_clause),
source,
) {
return Ok(SuppressClauseHeader::No);
}
let clause_start = clause.header.range(source)?.end();
let clause_range = TextRange::new(clause_start, last_child_in_clause.end());
// Only applies to clauses on a single line
if source.contains_line_break(clause_range) {
return Ok(SuppressClauseHeader::No);
}
Ok(SuppressClauseHeader::Yes {
last_child_in_clause,
})
}
#[cold]
fn write_suppressed_clause(
clause: &FormatClause,
f: &mut Formatter<PyFormatContext<'_>>,
last_child_in_clause: AnyNodeRef,
) -> FormatResult<()> {
if let Some((leading_comments, last_node)) = clause.leading_comments {
leading_alternate_branch_comments(leading_comments, last_node).fmt(f)?;
}
let header = clause.header;
let clause_start = header.first_keyword_range(f.context().source())?.start();
let comments = f.context().comments().clone();
let clause_end = last_child_in_clause.end();
// Write the outer comments and format the node as verbatim
write!(
f,
[
source_position(clause_start),
verbatim_text(TextRange::new(clause_start, clause_end)),
source_position(clause_end),
trailing_comments(comments.trailing(last_child_in_clause)),
hard_line_break()
]
)?;
// We mark comments in the header as formatted as in
// the implementation of [`write_suppressed_clause_header`].
//
// Note that the header may be multi-line and contain
// various comments since we only require that the range
// starting at the _colon_ and ending at the `# fmt: skip`
// fits on one line.
header.visit(&mut |child| {
for comment in comments.leading_trailing(child) {
comment.mark_formatted();
}
comments.mark_verbatim_node_comments_formatted(child);
});
// Similarly we mark the comments in the body as formatted.
// Note that the trailing comments for the last child in the
// body have already been handled above.
for stmt in clause.body {
comments.mark_verbatim_node_comments_formatted(stmt.into());
}
Ok(())
}
enum SuppressClauseHeader<'a> {
No,
Yes {
last_child_in_clause: AnyNodeRef<'a>,
},
}

View File

@@ -8,7 +8,7 @@ use crate::comments::format::{
};
use crate::comments::{SourceComment, leading_comments, trailing_comments};
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -65,8 +65,9 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
decorators: decorator_list,
leading_definition_comments,
},
clause(
clause_header(
ClauseHeader::Class(item),
trailing_definition_comments,
&format_with(|f| {
write!(f, [token("class"), space(), name.format()])?;
@@ -131,10 +132,8 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
Ok(())
}),
trailing_definition_comments,
body,
SuiteKind::Class,
),
clause_body(body, SuiteKind::Class, trailing_definition_comments),
]
)?;

View File

@@ -6,7 +6,7 @@ use crate::expression::expr_tuple::TupleParentheses;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Debug)]
@@ -50,22 +50,27 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
write!(
f,
[clause(
ClauseHeader::For(item),
&format_args![
is_async.then_some(format_args![token("async"), space()]),
token("for"),
space(),
ExprTupleWithoutParentheses(target),
space(),
token("in"),
space(),
maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks),
],
trailing_condition_comments,
body,
SuiteKind::other(orelse.is_empty()),
),]
[
clause_header(
ClauseHeader::For(item),
trailing_condition_comments,
&format_args![
is_async.then_some(format_args![token("async"), space()]),
token("for"),
space(),
ExprTupleWithoutParentheses(target),
space(),
token("in"),
space(),
maybe_parenthesize_expression(iter, item, Parenthesize::IfBreaks),
],
),
clause_body(
body,
SuiteKind::other(orelse.is_empty()),
trailing_condition_comments
),
]
)?;
if orelse.is_empty() {
@@ -79,14 +84,15 @@ impl FormatNodeRule<StmtFor> for FormatStmtFor {
write!(
f,
[clause(
ClauseHeader::OrElse(ElseClause::For(item)),
&token("else"),
trailing,
orelse,
SuiteKind::other(true),
)
.with_leading_comments(leading, body.last())]
[
clause_header(
ClauseHeader::OrElse(ElseClause::For(item)),
trailing,
&token("else"),
)
.with_leading_comments(leading, body.last()),
clause_body(orelse, SuiteKind::other(true), trailing),
]
)?;
}

View File

@@ -4,7 +4,7 @@ use crate::comments::format::{
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::{Parentheses, Parenthesize};
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::stmt_class_def::FormatDecorators;
use crate::statement::suite::SuiteKind;
use ruff_formatter::write;
@@ -60,13 +60,12 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
decorators: decorator_list,
leading_definition_comments,
},
clause(
clause_header(
ClauseHeader::Function(item),
&format_with(|f| format_function_header(f, item)),
trailing_definition_comments,
body,
SuiteKind::Function,
&format_with(|f| format_function_header(f, item)),
),
clause_body(body, SuiteKind::Function, trailing_definition_comments),
]
)?;

View File

@@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -26,17 +26,22 @@ impl FormatNodeRule<StmtIf> for FormatStmtIf {
write!(
f,
[clause(
ClauseHeader::If(item),
&format_args![
token("if"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
],
trailing_colon_comment,
body,
SuiteKind::other(elif_else_clauses.is_empty()),
)]
[
clause_header(
ClauseHeader::If(item),
trailing_colon_comment,
&format_args![
token("if"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
],
),
clause_body(
body,
SuiteKind::other(elif_else_clauses.is_empty()),
trailing_colon_comment
),
]
)?;
let mut last_node = body.last().unwrap().into();
@@ -76,8 +81,9 @@ pub(crate) fn format_elif_else_clause(
write!(
f,
[
clause(
clause_header(
ClauseHeader::ElifElse(item),
trailing_colon_comment,
&format_with(|f: &mut PyFormatter| {
f.options()
.source_map_generation()
@@ -97,11 +103,9 @@ pub(crate) fn format_elif_else_clause(
token("else").fmt(f)
}
}),
trailing_colon_comment,
body,
suite_kind,
)
.with_leading_comments(leading_comments, last_node),
clause_body(body, suite_kind, trailing_colon_comment),
f.options()
.source_map_generation()
.is_enabled()

View File

@@ -9,7 +9,7 @@ use crate::other::except_handler_except_handler::{
ExceptHandlerKind, FormatExceptHandlerExceptHandler,
};
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
use crate::statement::{FormatRefWithRule, Stmt};
@@ -154,14 +154,15 @@ fn format_case<'a>(
write!(
f,
[clause(
header,
&token(kind.keyword()),
trailing_case_comments,
body,
SuiteKind::other(last_suite_in_statement),
)
.with_leading_comments(leading_case_comments, previous_node),]
[
clause_header(header, trailing_case_comments, &token(kind.keyword()))
.with_leading_comments(leading_case_comments, previous_node),
clause_body(
body,
SuiteKind::other(last_suite_in_statement),
trailing_case_comments
),
]
)?;
(Some(last), rest)
} else {

View File

@@ -5,7 +5,7 @@ use ruff_text_size::Ranged;
use crate::expression::maybe_parenthesize_expression;
use crate::expression::parentheses::Parenthesize;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, ElseClause, clause};
use crate::statement::clause::{ClauseHeader, ElseClause, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -33,17 +33,22 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
write!(
f,
[clause(
ClauseHeader::While(item),
&format_args![
token("while"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
],
trailing_condition_comments,
body,
SuiteKind::other(orelse.is_empty()),
)]
[
clause_header(
ClauseHeader::While(item),
trailing_condition_comments,
&format_args![
token("while"),
space(),
maybe_parenthesize_expression(test, item, Parenthesize::IfBreaks),
]
),
clause_body(
body,
SuiteKind::other(orelse.is_empty()),
trailing_condition_comments
),
]
)?;
if !orelse.is_empty() {
@@ -55,14 +60,15 @@ impl FormatNodeRule<StmtWhile> for FormatStmtWhile {
write!(
f,
[clause(
ClauseHeader::OrElse(ElseClause::While(item)),
&token("else"),
trailing,
orelse,
SuiteKind::other(true),
)
.with_leading_comments(leading, body.last()),]
[
clause_header(
ClauseHeader::OrElse(ElseClause::While(item)),
trailing,
&token("else")
)
.with_leading_comments(leading, body.last()),
clause_body(orelse, SuiteKind::other(true), trailing),
]
)?;
}

View File

@@ -13,7 +13,7 @@ use crate::expression::parentheses::{
use crate::other::commas;
use crate::other::with_item::WithItemLayout;
use crate::prelude::*;
use crate::statement::clause::{ClauseHeader, clause};
use crate::statement::clause::{ClauseHeader, clause_body, clause_header};
use crate::statement::suite::SuiteKind;
#[derive(Default)]
@@ -46,103 +46,106 @@ impl FormatNodeRule<StmtWith> for FormatStmtWith {
write!(
f,
[clause(
ClauseHeader::With(with_stmt),
&format_with(|f| {
write!(
f,
[
with_stmt
.is_async
.then_some(format_args![token("async"), space()]),
token("with"),
space()
]
)?;
[
clause_header(
ClauseHeader::With(with_stmt),
colon_comments,
&format_with(|f| {
write!(
f,
[
with_stmt
.is_async
.then_some(format_args![token("async"), space()]),
token("with"),
space()
]
)?;
let layout = WithItemsLayout::from_statement(
with_stmt,
f.context(),
parenthesized_comments,
)?;
let layout = WithItemsLayout::from_statement(
with_stmt,
f.context(),
parenthesized_comments,
)?;
match layout {
WithItemsLayout::SingleWithTarget(single) => {
optional_parentheses(&single.format().with_options(
WithItemLayout::ParenthesizedContextManagers { single: true },
))
.fmt(f)
}
match layout {
WithItemsLayout::SingleWithTarget(single) => {
optional_parentheses(&single.format().with_options(
WithItemLayout::ParenthesizedContextManagers { single: true },
))
.fmt(f)
}
WithItemsLayout::SingleWithoutTarget(single) => single
.format()
.with_options(WithItemLayout::SingleWithoutTarget)
.fmt(f),
WithItemsLayout::SingleWithoutTarget(single) => single
.format()
.with_options(WithItemLayout::SingleWithoutTarget)
.fmt(f),
WithItemsLayout::SingleParenthesizedContextManager(single) => single
.format()
.with_options(WithItemLayout::SingleParenthesizedContextManager)
.fmt(f),
WithItemsLayout::SingleParenthesizedContextManager(single) => single
.format()
.with_options(WithItemLayout::SingleParenthesizedContextManager)
.fmt(f),
WithItemsLayout::ParenthesizeIfExpands => {
parenthesize_if_expands(&format_with(|f| {
let mut joiner =
f.join_comma_separated(with_stmt.body.first().unwrap().start());
for item in &with_stmt.items {
joiner.entry_with_line_separator(
item,
&item.format().with_options(
WithItemLayout::ParenthesizedContextManagers {
single: with_stmt.items.len() == 1,
},
),
soft_line_break_or_space(),
WithItemsLayout::ParenthesizeIfExpands => {
parenthesize_if_expands(&format_with(|f| {
let mut joiner = f.join_comma_separated(
with_stmt.body.first().unwrap().start(),
);
}
joiner.finish()
}))
.fmt(f)
}
WithItemsLayout::Python38OrOlder => f
.join_with(format_args![token(","), space()])
.entries(with_stmt.items.iter().map(|item| {
item.format().with_options(WithItemLayout::Python38OrOlder {
single: with_stmt.items.len() == 1,
})
}))
.finish(),
for item in &with_stmt.items {
joiner.entry_with_line_separator(
item,
&item.format().with_options(
WithItemLayout::ParenthesizedContextManagers {
single: with_stmt.items.len() == 1,
},
),
soft_line_break_or_space(),
);
}
joiner.finish()
}))
.fmt(f)
}
WithItemsLayout::Parenthesized => parenthesized(
"(",
&format_with(|f: &mut PyFormatter| {
let mut joiner =
f.join_comma_separated(with_stmt.body.first().unwrap().start());
WithItemsLayout::Python38OrOlder => f
.join_with(format_args![token(","), space()])
.entries(with_stmt.items.iter().map(|item| {
item.format().with_options(WithItemLayout::Python38OrOlder {
single: with_stmt.items.len() == 1,
})
}))
.finish(),
for item in &with_stmt.items {
joiner.entry(
item,
&item.format().with_options(
WithItemLayout::ParenthesizedContextManagers {
single: with_stmt.items.len() == 1,
},
),
WithItemsLayout::Parenthesized => parenthesized(
"(",
&format_with(|f: &mut PyFormatter| {
let mut joiner = f.join_comma_separated(
with_stmt.body.first().unwrap().start(),
);
}
joiner.finish()
}),
")",
)
.with_dangling_comments(parenthesized_comments)
.fmt(f),
}
}),
colon_comments,
&with_stmt.body,
SuiteKind::other(true),
)]
for item in &with_stmt.items {
joiner.entry(
item,
&item.format().with_options(
WithItemLayout::ParenthesizedContextManagers {
single: with_stmt.items.len() == 1,
},
),
);
}
joiner.finish()
}),
")",
)
.with_dangling_comments(parenthesized_comments)
.fmt(f),
}
})
),
clause_body(&with_stmt.body, SuiteKind::other(true), colon_comments)
]
)
}
}

View File

@@ -20,16 +20,24 @@ b = [c for c in "A very long string that would normally generate some kind of co
```diff
--- Black
+++ Ruff
@@ -1,8 +1,10 @@
def foo(): return "mock" # fmt: skip
@@ -1,8 +1,14 @@
-def foo(): return "mock" # fmt: skip
-if True: print("yay") # fmt: skip
-for i in range(10): print(i) # fmt: skip
+def foo():
+ return "mock" # fmt: skip
+
+
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
+if True:
+ print("yay") # fmt: skip
+for i in range(10):
+ print(i) # fmt: skip
-j = 1 # fmt: skip
-while j < 10: j += 1 # fmt: skip
+j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
+while j < 10:
+ j += 1 # fmt: skip
-b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
+b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
@@ -38,14 +46,18 @@ b = [c for c in "A very long string that would normally generate some kind of co
## Ruff Output
```python
def foo(): return "mock" # fmt: skip
def foo():
return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
if True:
print("yay") # fmt: skip
for i in range(10):
print(i) # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
while j < 10:
j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip
```

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/unary.py
snapshot_kind: text
---
## Input
```python
@@ -199,61 +200,6 @@ def foo():
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
if '' and (not #
0):
pass
if '' and (not #
(0)
):
pass
if '' and (not
( #
0
)):
pass
if (
not
# comment
(a)):
pass
if not ( # comment
a):
pass
if not (
# comment
(a)):
pass
if not (
# comment
a):
pass
not (# comment
(a))
(-#comment
(a))
if ( # a
# b
not # c
# d
( # e
# f
a # g
# h
) # i
# j
):
pass
```
## Output
@@ -304,35 +250,31 @@ if +(
pass
if (
not
# comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
~
# comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
-
# comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
+
# comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
@@ -341,9 +283,8 @@ if (
if (
# unary comment
not
# operand comment
(
not (
# comment
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
@@ -377,28 +318,31 @@ if (
## Trailing operator comments
if (
not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment
if ( # comment
not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment
# comment
~aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment
# comment
-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
if (
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # comment
# comment
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
):
pass
@@ -418,13 +362,14 @@ if (
pass
if (
not
# comment
a
not a
):
pass
if not a: # comment
if ( # comment
not a
):
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7423
@@ -440,9 +385,9 @@ if True:
# Regression test for: https://github.com/astral-sh/ruff/issues/7448
x = (
# a
not # b
# b
# c
( # d
not ( # d
# e
True
)
@@ -470,68 +415,4 @@ def foo():
not (aaaaaaaaaaaaaaaaaaaaa[bbbbbbbb, ccccccc]) and dddddddddd < eeeeeeeeeeeeeee
):
pass
# Regression tests for https://github.com/astral-sh/ruff/issues/19226
if "" and (
not 0 #
):
pass
if "" and (
not (0) #
):
pass
if "" and (
not ( #
0
)
):
pass
if (
not
# comment
(a)
):
pass
if not ( # comment
a
):
pass
if not (
# comment
a
):
pass
if not (
# comment
a
):
pass
not ( # comment
a
)
(
-(a) # comment
)
if ( # a
# b
not # c
# d
( # e
# f
a # g
# h
) # i
# j
):
pass
```

View File

@@ -1,341 +0,0 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/compound_one_liners.py
---
## Input
```python
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans \
and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else: print("this")
while foo(): print(i) # fmt: skip
else: print("this")
# again but only the second skip
for i in range(2): print(i)
else: print("this") # fmt: skip
while foo(): print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it: foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip
```
## Output
```python
# Test cases for fmt: skip on compound statements that fit on one line
# Basic single-line compound statements
def simple_func(): return "hello" # fmt: skip
if True: print("condition met") # fmt: skip
for i in range(5): print(i) # fmt: skip
while x < 10: x += 1 # fmt: skip
# With expressions that would normally trigger formatting
def long_params(a, b, c, d, e, f, g): return a + b + c + d + e + f + g # fmt: skip
if some_very_long_condition_that_might_wrap: do_something_else_that_is_long() # fmt: skip
# Nested compound statements (outer should be preserved)
if True:
for i in range(10): print(i) # fmt: skip
# Multiple statements in body (should not apply - multiline)
if True:
x = 1
y = 2 # fmt: skip
# With decorators - decorated function on one line
@overload
def decorated_func(x: int) -> str: return str(x) # fmt: skip
@property
def prop_method(self): return self._value # fmt: skip
# Class definitions on one line
class SimpleClass: pass # fmt: skip
class GenericClass(Generic[T]): pass # fmt: skip
# Try/except blocks
try: risky_operation() # fmt: skip
except ValueError: handle_error() # fmt: skip
except: handle_any_error() # fmt: skip
else: success_case() # fmt: skip
finally: cleanup() # fmt: skip
# Match statements (Python 3.10+)
match value:
case 1: print("one") # fmt: skip
case _: print("other") # fmt: skip
# With statements
with open("file.txt") as f: content = f.read() # fmt: skip
with context_manager() as cm: result = cm.process() # fmt: skip
# Async variants
async def async_func(): return await some_call() # fmt: skip
async for item in async_iterator(): await process(item) # fmt: skip
async with async_context() as ctx: await ctx.work() # fmt: skip
# Complex expressions that would normally format
def complex_expr(): return [x for x in range(100) if x % 2 == 0 and x > 50] # fmt: skip
if condition_a and condition_b or (condition_c and not condition_d): execute_complex_logic() # fmt: skip
# Edge case: comment positioning
def func_with_comment(): # some comment
return "value" # fmt: skip
# Edge case: multiple fmt: skip (only last one should matter)
def multiple_skip(): return "test" # fmt: skip # fmt: skip
# Should NOT be affected (already multiline)
def multiline_func():
return "this should format normally"
if long_condition_that_spans and continues_on_next_line:
print("multiline condition")
# Mix of skipped and non-skipped
for i in range(10): print(f"item {i}") # fmt: skip
for j in range(5):
print(f"formatted item {j}")
# With trailing comma that would normally be removed
def trailing_comma_func(a, b, c,): return a + b + c # fmt: skip
# Dictionary/list comprehensions
def dict_comp(): return {k: v for k, v in items.items() if v is not None} # fmt: skip
def list_comp(): return [x * 2 for x in numbers if x > threshold_value] # fmt: skip
# Lambda in one-liner
def with_lambda(): return lambda x, y, z: x + y + z if all([x, y, z]) else None # fmt: skip
# String formatting that would normally be reformatted
def format_string(): return f"Hello {name}, you have {count} items in your cart totaling ${total:.2f}" # fmt: skip
# loop else clauses
for i in range(2): print(i) # fmt: skip
else: print("this") # fmt: skip
while foo(): print(i) # fmt: skip
else: print("this") # fmt: skip
# again but only the first skip
for i in range(2): print(i) # fmt: skip
else:
print("this")
while foo(): print(i) # fmt: skip
else:
print("this")
# again but only the second skip
for i in range(2):
print(i)
else: print("this") # fmt: skip
while foo():
print(i)
else: print("this") # fmt: skip
# multiple statements in body
if True: print("this"); print("that") # fmt: skip
# Examples with more comments
try: risky_operation() # fmt: skip
# leading 1
except ValueError: handle_error() # fmt: skip
# leading 2
except: handle_any_error() # fmt: skip
# leading 3
else: success_case() # fmt: skip
# leading 4
finally: cleanup() # fmt: skip
# trailing
# multi-line before colon (should remain as is)
if (
long_condition
): a + b # fmt: skip
# over-indented comment example
# See https://github.com/astral-sh/ruff/pull/20633#issuecomment-3453288910
# and https://github.com/astral-sh/ruff/pull/21185
for x in it:
foo()
# comment
else: bar() # fmt: skip
if this(
'is a long',
# commented
'condition'
): with_a_skip # fmt: skip
```

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_skip/docstrings.py
snapshot_kind: text
---
## Input
```python

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/parentheses/expression_parentheses_comments.py
snapshot_kind: text
---
## Input
```python
@@ -178,13 +179,13 @@ nested_parentheses4 = [
x = (
# unary comment
not
# in-between comment
(
not (
# leading inner
"a"
),
not ( # in-between comment
# in-between comment
not (
# leading inner
"b"
),
@@ -193,7 +194,8 @@ x = (
"c"
),
# 1
not ( # 2 # 3
# 2
not ( # 3
# 4
"d"
),
@@ -201,9 +203,8 @@ x = (
if (
# unary comment
not
# in-between comment
(
not (
# leading inner
1
)

View File

@@ -301,9 +301,9 @@ fn to_lsp_diagnostic(
severity,
tags,
code,
code_description: diagnostic.documentation_url().and_then(|url| {
code_description: diagnostic.to_ruff_url().and_then(|url| {
Some(lsp_types::CodeDescription {
href: lsp_types::Url::parse(url).ok()?,
href: lsp_types::Url::parse(&url).ok()?,
})
}),
source: Some(DIAGNOSTIC_NAME.into()),

136
crates/ty/docs/rules.md generated
View File

@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L121" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L120" target="_blank">View source</a>
</small>
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L165" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L164" target="_blank">View source</a>
</small>
@@ -95,7 +95,7 @@ f(int) # error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L191" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L190" target="_blank">View source</a>
</small>
@@ -126,7 +126,7 @@ a = 1
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L216" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L215" target="_blank">View source</a>
</small>
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L242" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L241" target="_blank">View source</a>
</small>
@@ -190,7 +190,7 @@ class B(A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L307" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L306" target="_blank">View source</a>
</small>
@@ -217,7 +217,7 @@ class B(A, A): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L328" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L327" target="_blank">View source</a>
</small>
@@ -329,7 +329,7 @@ def test(): -> "Literal[5]":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L532" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L531" target="_blank">View source</a>
</small>
@@ -359,7 +359,7 @@ class C(A, B): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L556" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L555" target="_blank">View source</a>
</small>
@@ -385,7 +385,7 @@ t[3] # IndexError: tuple index out of range
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L360" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L359" target="_blank">View source</a>
</small>
@@ -474,7 +474,7 @@ an atypical memory layout.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L610" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L609" target="_blank">View source</a>
</small>
@@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L649" target="_blank">View source</a>
</small>
@@ -529,7 +529,7 @@ a: int = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1809" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1808" target="_blank">View source</a>
</small>
@@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L672" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L671" target="_blank">View source</a>
</small>
@@ -599,7 +599,7 @@ asyncio.run(main())
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L701" target="_blank">View source</a>
</small>
@@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L753" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L752" target="_blank">View source</a>
</small>
@@ -650,7 +650,7 @@ with 1:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L774" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L773" target="_blank">View source</a>
</small>
@@ -679,7 +679,7 @@ a: str
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L797" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L796" target="_blank">View source</a>
</small>
@@ -723,7 +723,7 @@ except ZeroDivisionError:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L833" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L832" target="_blank">View source</a>
</small>
@@ -756,7 +756,7 @@ class C[U](Generic[T]): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L577" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L576" target="_blank">View source</a>
</small>
@@ -795,7 +795,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L859" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L858" target="_blank">View source</a>
</small>
@@ -830,7 +830,7 @@ def f(t: TypeVar("U")): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L955" target="_blank">View source</a>
</small>
@@ -864,7 +864,7 @@ class B(metaclass=f): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L505" target="_blank">View source</a>
</small>
@@ -896,7 +896,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L932" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L931" target="_blank">View source</a>
</small>
@@ -926,7 +926,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L982" target="_blank">View source</a>
</small>
@@ -976,7 +976,7 @@ def foo(x: int) -> int: ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1082" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1081" target="_blank">View source</a>
</small>
@@ -1002,7 +1002,7 @@ def f(a: int = ''): ...
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-paramspec" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L887" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L886" target="_blank">View source</a>
</small>
@@ -1033,7 +1033,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L442" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L441" target="_blank">View source</a>
</small>
@@ -1067,7 +1067,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1102" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1101" target="_blank">View source</a>
</small>
@@ -1116,7 +1116,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L631" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L630" target="_blank">View source</a>
</small>
@@ -1141,7 +1141,7 @@ def func() -> int:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1145" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1144" target="_blank">View source</a>
</small>
@@ -1199,7 +1199,7 @@ TODO #14889
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L910" target="_blank">View source</a>
</small>
@@ -1226,7 +1226,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1184" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1183" target="_blank">View source</a>
</small>
@@ -1256,7 +1256,7 @@ TYPE_CHECKING = ''
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1208" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1207" target="_blank">View source</a>
</small>
@@ -1286,7 +1286,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1260" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1259" target="_blank">View source</a>
</small>
@@ -1320,7 +1320,7 @@ f(10) # Error
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1232" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1231" target="_blank">View source</a>
</small>
@@ -1354,7 +1354,7 @@ class C:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1288" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1287" target="_blank">View source</a>
</small>
@@ -1389,7 +1389,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1317" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1316" target="_blank">View source</a>
</small>
@@ -1414,7 +1414,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1910" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1909" target="_blank">View source</a>
</small>
@@ -1447,7 +1447,7 @@ alice["age"] # KeyError
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1336" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1335" target="_blank">View source</a>
</small>
@@ -1476,7 +1476,7 @@ func("string") # error: [no-matching-overload]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1359" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1358" target="_blank">View source</a>
</small>
@@ -1500,7 +1500,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1377" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1376" target="_blank">View source</a>
</small>
@@ -1526,7 +1526,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1428" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1427" target="_blank">View source</a>
</small>
@@ -1553,7 +1553,7 @@ f(1, x=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1663" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1662" target="_blank">View source</a>
</small>
@@ -1611,7 +1611,7 @@ def test(): -> "int":
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1785" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1784" target="_blank">View source</a>
</small>
@@ -1641,7 +1641,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1519" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1518" target="_blank">View source</a>
</small>
@@ -1670,7 +1670,7 @@ class B(A): ... # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1564" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563" target="_blank">View source</a>
</small>
@@ -1697,7 +1697,7 @@ f("foo") # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1542" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541" target="_blank">View source</a>
</small>
@@ -1725,7 +1725,7 @@ def _(x: int):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1585" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1584" target="_blank">View source</a>
</small>
@@ -1771,7 +1771,7 @@ class A:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1642" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1641" target="_blank">View source</a>
</small>
@@ -1798,7 +1798,7 @@ f(x=1, y=2) # Error raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1684" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1683" target="_blank">View source</a>
</small>
@@ -1826,7 +1826,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1706" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1705" target="_blank">View source</a>
</small>
@@ -1851,7 +1851,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1725" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1724" target="_blank">View source</a>
</small>
@@ -1876,7 +1876,7 @@ print(x) # NameError: name 'x' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1397" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1396" target="_blank">View source</a>
</small>
@@ -1913,7 +1913,7 @@ b1 < b2 < b1 # exception raised here
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1744" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1743" target="_blank">View source</a>
</small>
@@ -1941,7 +1941,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1766" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1765" target="_blank">View source</a>
</small>
@@ -1966,7 +1966,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L471" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L470" target="_blank">View source</a>
</small>
@@ -2007,7 +2007,7 @@ class SubProto(BaseProto, Protocol):
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.16">0.0.1-alpha.16</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L286" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L285" target="_blank">View source</a>
</small>
@@ -2095,7 +2095,7 @@ a = 20 / 0 # type: ignore
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1449" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1448" target="_blank">View source</a>
</small>
@@ -2123,7 +2123,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L139" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L138" target="_blank">View source</a>
</small>
@@ -2155,7 +2155,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1471" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1470" target="_blank">View source</a>
</small>
@@ -2187,7 +2187,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1837" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1836" target="_blank">View source</a>
</small>
@@ -2214,7 +2214,7 @@ cast(int, f()) # Redundant
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1624" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1623" target="_blank">View source</a>
</small>
@@ -2238,7 +2238,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.15">0.0.1-alpha.15</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1858" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1857" target="_blank">View source</a>
</small>
@@ -2296,7 +2296,7 @@ def g():
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.7">0.0.1-alpha.7</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L720" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L719" target="_blank">View source</a>
</small>
@@ -2335,7 +2335,7 @@ class D(C): ... # error: [unsupported-base]
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1026" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1025" target="_blank">View source</a>
</small>
@@ -2398,7 +2398,7 @@ def foo(x: int | str) -> int | str:
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L268" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L267" target="_blank">View source</a>
</small>
@@ -2422,7 +2422,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1497" target="_blank">View source</a>
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1496" target="_blank">View source</a>
</small>

View File

@@ -5,7 +5,6 @@ mod python_version;
mod version;
pub use args::Cli;
use ty_project::metadata::settings::TerminalSettings;
use ty_static::EnvVars;
use std::fmt::Write;
@@ -22,9 +21,7 @@ use clap::{CommandFactory, Parser};
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
use ruff_db::diagnostic::{
Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity,
};
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, DisplayDiagnostics, Severity};
use ruff_db::files::File;
use ruff_db::max_parallelism;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
@@ -196,12 +193,6 @@ pub enum ExitStatus {
InternalError = 101,
}
impl ExitStatus {
pub const fn is_internal_error(self) -> bool {
matches!(self, ExitStatus::InternalError)
}
}
impl Termination for ExitStatus {
fn report(self) -> ExitCode {
ExitCode::from(self as u8)
@@ -343,8 +334,11 @@ impl MainLoop {
let diagnostics_count = result.len();
let mut stdout = self.printer.stream_for_details().lock();
let exit_status =
exit_status_from_diagnostics(&result, terminal_settings);
let max_severity = result
.iter()
.map(Diagnostic::severity)
.max()
.unwrap_or(Severity::Info);
// Only render diagnostics if they're going to be displayed, since doing
// so is expensive.
@@ -365,14 +359,25 @@ impl MainLoop {
)?;
}
if exit_status.is_internal_error() {
if max_severity.is_fatal() {
tracing::warn!(
"A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details."
);
}
if self.watcher.is_none() {
return Ok(exit_status);
return Ok(match max_severity {
Severity::Info => ExitStatus::Success,
Severity::Warning => {
if terminal_settings.error_on_warning {
ExitStatus::Failure
} else {
ExitStatus::Success
}
}
Severity::Error => ExitStatus::Failure,
Severity::Fatal => ExitStatus::InternalError,
});
}
}
} else {
@@ -405,40 +410,6 @@ impl MainLoop {
}
}
fn exit_status_from_diagnostics(
diagnostics: &[Diagnostic],
terminal_settings: &TerminalSettings,
) -> ExitStatus {
if diagnostics.is_empty() {
return ExitStatus::Success;
}
let mut max_severity = Severity::Info;
let mut io_error = false;
for diagnostic in diagnostics {
max_severity = max_severity.max(diagnostic.severity());
io_error = io_error || matches!(diagnostic.id(), DiagnosticId::Io);
}
if !max_severity.is_fatal() && io_error {
return ExitStatus::Error;
}
match max_severity {
Severity::Info => ExitStatus::Success,
Severity::Warning => {
if terminal_settings.error_on_warning {
ExitStatus::Failure
} else {
ExitStatus::Success
}
}
Severity::Error => ExitStatus::Failure,
Severity::Fatal => ExitStatus::InternalError,
}
}
/// A progress reporter for `ty check`.
struct IndicatifReporter {
collector: CollectReporter,

View File

@@ -41,24 +41,22 @@ fn test_quiet_output() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", "x: int = 'foo'")?;
// By default, we emit a diagnostic
assert_cmd_snapshot!(case.command(), @r#"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int`
--> test.py:1:4
--> test.py:1:1
|
1 | x: int = 'foo'
| --- ^^^^^ Incompatible value of type `Literal["foo"]`
| |
| Declared type
| ^
|
info: rule `invalid-assignment` is enabled by default
Found 1 diagnostic
----- stderr -----
"#);
"###);
// With `quiet`, the diagnostic is not displayed, just the summary message
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
@@ -562,9 +560,9 @@ fn check_non_existing_path() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command().arg("project/main.py").arg("project/tests"),
@r"
@r###"
success: false
exit_code: 2
exit_code: 1
----- stdout -----
error[io]: `<temp_dir>/project/main.py`: No such file or directory (os error 2)
@@ -574,7 +572,7 @@ fn check_non_existing_path() -> anyhow::Result<()> {
----- stderr -----
WARN No python files found under the given path(s)
"
"###
);
Ok(())

View File

@@ -8,12 +8,13 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
///
/// Returns symbols from all files in the workspace and dependencies, filtered
/// by the query.
pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolInfo<'db>> {
pub fn all_symbols<'db>(db: &'db dyn Db, query: &str) -> Vec<AllSymbolInfo<'db>> {
// If the query is empty, return immediately to avoid expensive file scanning
if query.will_match_everything() {
if query.is_empty() {
return Vec::new();
}
let query = QueryPattern::new(query);
let results = std::sync::Mutex::new(Vec::new());
{
let modules = all_modules(db);
@@ -143,7 +144,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
impl CursorTest {
fn all_symbols(&self, query: &str) -> String {
let symbols = all_symbols(&self.db, &QueryPattern::new(query));
let symbols = all_symbols(&self.db, query);
if symbols.is_empty() {
return "No symbols found".to_string();

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::Type;
use ty_python_semantic::types::ide_support::{
call_signature_details, call_type_simplified_by_overloads, definitions_for_keyword_argument,
call_signature_details, definitions_for_keyword_argument,
};
use ty_python_semantic::{
HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol,
@@ -213,7 +213,6 @@ impl<'db> DefinitionsOrTargets<'db> {
| ty_python_semantic::types::TypeDefinition::Function(definition)
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
| ty_python_semantic::types::TypeDefinition::SpecialForm(definition)
| ty_python_semantic::types::TypeDefinition::NewType(definition) => {
ResolvedDefinition::Definition(definition)
}
@@ -327,18 +326,6 @@ impl GotoTarget<'_> {
Some(ty)
}
/// Try to get a simplified display of this callable type by resolving overloads
pub(crate) fn call_type_simplified_by_overloads(
&self,
model: &SemanticModel,
) -> Option<String> {
if let GotoTarget::Call { call, .. } = self {
call_type_simplified_by_overloads(model.db(), model, call)
} else {
None
}
}
/// Gets the definitions for this goto target.
///
/// The `alias_resolution` parameter controls whether import aliases

View File

@@ -68,134 +68,6 @@ mod tests {
");
}
#[test]
fn goto_type_of_typing_dot_literal() {
let test = cursor_test(
r#"
from typing import Literal
a<CURSOR>b = Literal
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/typing.pyi:351:1
|
349 | Final: _SpecialForm
350 |
351 | Literal: _SpecialForm
| ^^^^^^^
352 | TypedDict: _SpecialForm
|
info: Source
--> main.py:4:1
|
2 | from typing import Literal
3 |
4 | ab = Literal
| ^^
|
");
}
// this is a slightly different case to the one above,
// since `Any` is a class in typeshed rather than a variable
#[test]
fn goto_type_of_typing_dot_any() {
let test = cursor_test(
r#"
from typing import Any
a<CURSOR>b = Any
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/typing.pyi:166:7
|
164 | # from _typeshed import AnnotationForm
165 |
166 | class Any:
| ^^^
167 | """Special type indicating an unconstrained type.
|
info: Source
--> main.py:4:1
|
2 | from typing import Any
3 |
4 | ab = Any
| ^^
|
"#);
}
// Similarly, `Generic` is a `type[]` type in typeshed
#[test]
fn goto_type_of_typing_dot_generic() {
let test = cursor_test(
r#"
from typing import Generic
a<CURSOR>b = Generic
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/typing.pyi:770:1
|
768 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...
769 |
770 | Generic: type[_Generic]
| ^^^^^^^
771 |
772 | class _ProtocolMeta(ABCMeta):
|
info: Source
--> main.py:4:1
|
2 | from typing import Generic
3 |
4 | ab = Generic
| ^^
|
");
}
#[test]
fn goto_type_of_ty_extensions_special_form() {
let test = cursor_test(
r#"
from ty_extensions import AlwaysTruthy
a<CURSOR>b = AlwaysTruthy
"#,
);
assert_snapshot!(test.goto_type_definition(), @r"
info[goto-type-definition]: Type definition
--> stdlib/ty_extensions.pyi:21:1
|
19 | # Types
20 | Unknown = object()
21 | AlwaysTruthy = object()
| ^^^^^^^^^^^^
22 | AlwaysFalsy = object()
|
info: Source
--> main.py:4:1
|
2 | from ty_extensions import AlwaysTruthy
3 |
4 | ab = AlwaysTruthy
| ^^
|
");
}
#[test]
fn goto_type_of_expression_with_function_type() {
let test = cursor_test(

View File

@@ -20,6 +20,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
}
let model = SemanticModel::new(db, file);
let ty = goto_target.inferred_type(&model);
let docs = goto_target
.get_definition_targets(
file,
@@ -29,10 +30,9 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Ho
.and_then(|definitions| definitions.docstring(db))
.map(HoverContent::Docstring);
// TODO: Render the symbol's signature instead of just its type.
let mut contents = Vec::new();
if let Some(signature) = goto_target.call_type_simplified_by_overloads(&model) {
contents.push(HoverContent::Signature(signature));
} else if let Some(ty) = goto_target.inferred_type(&model) {
if let Some(ty) = ty {
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
contents.push(match ty {
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => typevar
@@ -62,7 +62,7 @@ pub struct Hover<'db> {
impl<'db> Hover<'db> {
/// Renders the hover to a string using the specified markup kind.
pub const fn display<'a>(&'a self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHover<'db, 'a> {
pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> {
DisplayHover {
db,
hover: self,
@@ -93,13 +93,13 @@ impl<'a, 'db> IntoIterator for &'a Hover<'db> {
}
}
pub struct DisplayHover<'db, 'a> {
db: &'db dyn Db,
hover: &'a Hover<'db>,
pub struct DisplayHover<'a> {
db: &'a dyn Db,
hover: &'a Hover<'a>,
kind: MarkupKind,
}
impl fmt::Display for DisplayHover<'_, '_> {
impl fmt::Display for DisplayHover<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut first = true;
for content in &self.hover.contents {
@@ -115,9 +115,8 @@ impl fmt::Display for DisplayHover<'_, '_> {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum HoverContent<'db> {
Signature(String),
Type(Type<'db>, Option<TypeVarVariance>),
Docstring(Docstring),
}
@@ -141,9 +140,6 @@ pub(crate) struct DisplayHoverContent<'a, 'db> {
impl fmt::Display for DisplayHoverContent<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.content {
HoverContent::Signature(signature) => {
self.kind.fenced_code_block(&signature, "python").fmt(f)
}
HoverContent::Type(ty, variance) => {
let variance = match variance {
Some(TypeVarVariance::Covariant) => " (covariant)",
@@ -965,12 +961,14 @@ def ab(a: str): ...
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
the int overload
---------------------------------------------
```python
(a: int) -> Unknown
(a: str) -> Unknown
```
---
```text
@@ -1027,12 +1025,14 @@ def ab(a: str):
.build();
assert_snapshot!(test.hover(), @r#"
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
the int overload
---------------------------------------------
```python
(a: int) -> Unknown
(a: str) -> Unknown
```
---
@@ -1094,6 +1094,7 @@ def ab(a: int):
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the two arg overload
@@ -1103,6 +1104,7 @@ def ab(a: int):
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
```
---
```text
@@ -1159,12 +1161,20 @@ def ab(a: int):
.build();
assert_snapshot!(test.hover(), @r"
(
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the two arg overload
---------------------------------------------
```python
(
a: int,
b: int
) -> Unknown
(a: int) -> Unknown
```
---
@@ -1226,21 +1236,33 @@ def ab(a: int, *, c: int):
.build();
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
---------------------------------------------
keywordless overload
---------------------------------------------
```python
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
c: int
) -> Unknown
```
---
```text
@@ -1301,6 +1323,12 @@ def ab(a: int, *, c: int):
.build();
assert_snapshot!(test.hover(), @r"
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,
@@ -1311,6 +1339,12 @@ def ab(a: int, *, c: int):
---------------------------------------------
```python
(a: int) -> Unknown
(
a: int,
*,
b: int
) -> Unknown
(
a: int,
*,

File diff suppressed because it is too large Load Diff

View File

@@ -673,23 +673,6 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
self.visit_body(&class.body);
self.in_class_scope = prev_in_class;
}
ast::Stmt::TypeAlias(type_alias) => {
// Type alias name
self.add_token(
type_alias.name.range(),
SemanticTokenType::Class,
SemanticTokenModifier::DEFINITION,
);
// Type parameters (Python 3.12+ syntax)
if let Some(type_params) = &type_alias.type_params {
for type_param in &type_params.type_params {
self.visit_type_param(type_param);
}
}
self.visit_expr(&type_alias.value);
}
ast::Stmt::Import(import) => {
for alias in &import.names {
if let Some(asname) = &alias.asname {
@@ -764,49 +747,6 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
self.visit_expr(value);
}
}
ast::Stmt::For(for_stmt) => {
self.in_target_creating_definition = true;
self.visit_expr(&for_stmt.target);
self.in_target_creating_definition = false;
self.visit_expr(&for_stmt.iter);
self.visit_body(&for_stmt.body);
self.visit_body(&for_stmt.orelse);
}
ast::Stmt::With(with_stmt) => {
for item in &with_stmt.items {
self.visit_expr(&item.context_expr);
if let Some(expr) = &item.optional_vars {
self.in_target_creating_definition = true;
self.visit_expr(expr);
self.in_target_creating_definition = false;
}
}
self.visit_body(&with_stmt.body);
}
ast::Stmt::Try(try_stmt) => {
self.visit_body(&try_stmt.body);
for handler in &try_stmt.handlers {
match handler {
ast::ExceptHandler::ExceptHandler(except_handler) => {
if let Some(expr) = &except_handler.type_ {
self.visit_expr(expr);
}
if let Some(name) = &except_handler.name {
self.add_token(
name.range(),
SemanticTokenType::Variable,
SemanticTokenModifier::DEFINITION,
);
}
self.visit_body(&except_handler.body);
}
}
}
self.visit_body(&try_stmt.orelse);
self.visit_body(&try_stmt.finalbody);
}
_ => {
// For all other statement types, let the default visitor handle them
@@ -1364,7 +1304,7 @@ result = check(None)
);
assert_snapshot!(test.to_snapshot(&test.highlight_file()), @r#"
"U" @ 6..7: Class [definition]
"U" @ 6..7: TypeParameter
"str" @ 10..13: Class
"int" @ 16..19: Class
"Test" @ 27..31: Class [definition]
@@ -2465,16 +2405,16 @@ finally:
"1" @ 14..15: Number
"0" @ 18..19: Number
"ValueError" @ 27..37: Class
"ve" @ 41..43: Variable [definition]
"ve" @ 41..43: Variable
"print" @ 49..54: Function
"ve" @ 55..57: Variable
"TypeError" @ 67..76: Class
"RuntimeError" @ 78..90: Class
"re" @ 95..97: Variable [definition]
"re" @ 95..97: Variable
"print" @ 103..108: Function
"re" @ 109..111: Variable
"Exception" @ 120..129: Class
"e" @ 133..134: Variable [definition]
"e" @ 133..134: Variable
"print" @ 140..145: Function
"e" @ 146..147: Variable
"#);
@@ -2520,81 +2460,6 @@ class C:
"#);
}
#[test]
fn test_augmented_assignment() {
let test = SemanticTokenTest::new(
r#"
x = 0
x += 1
"#,
);
let tokens = test.highlight_file();
assert_snapshot!(test.to_snapshot(&tokens), @r#"
"x" @ 1..2: Variable [definition]
"0" @ 5..6: Number
"x" @ 7..8: Variable
"1" @ 12..13: Number
"#);
}
#[test]
fn test_type_alias() {
let test = SemanticTokenTest::new("type MyList[T] = list[T]");
let tokens = test.highlight_file();
assert_snapshot!(test.to_snapshot(&tokens), @r#"
"MyList" @ 5..11: Class [definition]
"T" @ 12..13: TypeParameter [definition]
"list" @ 17..21: Class
"T" @ 22..23: TypeParameter
"#);
}
#[test]
fn test_for_stmt() {
let test = SemanticTokenTest::new(
r#"
for item in []:
print(item)
else:
print(0)
"#,
);
let tokens = test.highlight_file();
assert_snapshot!(test.to_snapshot(&tokens), @r#"
"item" @ 5..9: Variable [definition]
"print" @ 21..26: Function
"item" @ 27..31: Variable
"print" @ 43..48: Function
"0" @ 49..50: Number
"#);
}
#[test]
fn test_with_stmt() {
let test = SemanticTokenTest::new(
r#"
with open("file.txt") as f:
f.read()
"#,
);
let tokens = test.highlight_file();
assert_snapshot!(test.to_snapshot(&tokens), @r#"
"open" @ 6..10: Function
"\"file.txt\"" @ 11..21: String
"f" @ 26..27: Variable [definition]
"f" @ 33..34: Variable
"read" @ 35..39: Method
"#);
}
/// Regression test for <https://github.com/astral-sh/ty/issues/1406>
#[test]
fn test_invalid_kwargs() {

View File

@@ -67,16 +67,6 @@ impl QueryPattern {
symbol_name.contains(&self.original)
}
}
/// Returns true when it is known that this pattern will return `true` for
/// all inputs given to `QueryPattern::is_match_symbol_name`.
///
/// This will never return `true` incorrectly, but it may return `false`
/// incorrectly. That is, it's possible that this query will match all
/// inputs but this still returns `false`.
pub fn will_match_everything(&self) -> bool {
self.re.is_none()
}
}
impl From<&str> for QueryPattern {

View File

@@ -32,7 +32,7 @@ camino = { workspace = true }
colored = { workspace = true }
compact_str = { workspace = true }
drop_bomb = { workspace = true }
get-size2 = { workspace = true, features = ["indexmap"]}
get-size2 = { workspace = true }
indexmap = { workspace = true }
itertools = { workspace = true }
ordermap = { workspace = true }

View File

@@ -98,9 +98,7 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
n: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
o: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
p: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
# error: [invalid-type-form] "Slices are not allowed in type expressions"
# error: [invalid-type-form] "Invalid subscript"
q: [1, 2, 3][1:2],
q: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
@@ -118,7 +116,7 @@ async def outer_async(): # avoid unrelated syntax errors on `yield` and `await`
reveal_type(n) # revealed: Unknown
reveal_type(o) # revealed: Unknown
reveal_type(p) # revealed: int | Unknown
reveal_type(q) # revealed: Unknown
reveal_type(q) # revealed: @Todo(unknown type subscript)
class Mat:
def __init__(self, value: int):

View File

@@ -330,11 +330,10 @@ from other import Literal
# ?
#
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
# error: [invalid-type-form] "Invalid subscript of object of type `_SpecialForm` in type expression"
a1: Literal[26]
def f():
reveal_type(a1) # revealed: Unknown
reveal_type(a1) # revealed: @Todo(unknown type subscript)
```
## Detecting typing_extensions.Literal

View File

@@ -503,11 +503,9 @@ class C[T]():
def f(self: Self):
def b(x: Self):
reveal_type(x) # revealed: Self@f
# revealed: None
reveal_type(generic_context(b))
reveal_type(generic_context(b)) # revealed: None
# revealed: ty_extensions.GenericContext[Self@f]
reveal_type(generic_context(C.f))
reveal_type(generic_context(C.f)) # revealed: tuple[Self@f]
```
Even if the `Self` annotation appears first in the nested function, it is the method that binds
@@ -521,11 +519,9 @@ class C:
def f(self: "C"):
def b(x: Self):
reveal_type(x) # revealed: Self@f
# revealed: None
reveal_type(generic_context(b))
reveal_type(generic_context(b)) # revealed: None
# revealed: None
reveal_type(generic_context(C.f))
reveal_type(generic_context(C.f)) # revealed: None
```
## Non-positional first parameters

View File

@@ -12,8 +12,11 @@ P = ParamSpec("P")
Ts = TypeVarTuple("Ts")
R_co = TypeVar("R_co", covariant=True)
Alias: TypeAlias = int
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
return args
def g() -> TypeGuard[int]: ...

View File

@@ -458,12 +458,12 @@ b: TD | None = f([{"x": 0}, {"x": 1}])
reveal_type(b) # revealed: TD
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
# error: [invalid-key] "Invalid key for TypedDict `TD`: Unknown key "y""
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD`"
c: TD = f([{"y": 0}, {"x": 1}])
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
# error: [invalid-key] "Unknown key "y" for TypedDict `TD`"
# error: [invalid-key] "Invalid key for TypedDict `TD`: Unknown key "y""
# error: [invalid-assignment] "Object of type `Unknown | dict[Unknown | str, Unknown | int]` is not assignable to `TD | None`"
c: TD | None = f([{"y": 0}, {"x": 1}])
```
@@ -482,14 +482,17 @@ class TD2(TypedDict):
x: str
def f(self, dt: dict[str, Any], key: str):
# TODO: This should not error once typed dict assignability is implemented.
# error: [invalid-assignment]
x1: TD = dt.get(key, {})
reveal_type(x1) # revealed: Any
reveal_type(x1) # revealed: TD
x2: TD = dt.get(key, {"x": 0})
reveal_type(x2) # revealed: Any
x3: TD | None = dt.get(key, {})
reveal_type(x3) # revealed: Any
# TODO: This should reveal `Any` once typed dict assignability is implemented.
reveal_type(x3) # revealed: Any | None
x4: TD | None = dt.get(key, {"x": 0})
reveal_type(x4) # revealed: Any

View File

@@ -2208,9 +2208,9 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
```py
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
reveal_type(b"foo".join)
# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
reveal_type(b"foo".endswith)
```

View File

@@ -313,7 +313,8 @@ reveal_type(A() + "foo") # revealed: A
reveal_type("foo" + A()) # revealed: A
reveal_type(A() + b"foo") # revealed: A
reveal_type(b"foo" + A()) # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(A() + ()) # revealed: A
reveal_type(() + A()) # revealed: A

View File

@@ -54,8 +54,10 @@ reveal_type(2**largest_u32) # revealed: int
def variable(x: int):
reveal_type(x**2) # revealed: int
reveal_type(2**x) # revealed: Any
reveal_type(x**x) # revealed: Any
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(2**x) # revealed: int
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
reveal_type(x**x) # revealed: int
```
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but

View File

@@ -200,9 +200,6 @@ isinstance("", t.Any) # error: [invalid-argument-type]
## The builtin `NotImplemented` constant is not callable
<!-- snapshot-diagnostics -->
```py
raise NotImplemented() # error: [call-non-callable]
raise NotImplemented("this module is not implemented yet!!!") # error: [call-non-callable]
NotImplemented() # error: [call-non-callable]
```

View File

@@ -598,9 +598,9 @@ from typing_extensions import Self
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
reveal_type(int.__new__)
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
reveal_type((42).__new__)
class X:

View File

@@ -10,13 +10,13 @@ import pickle
reveal_type(open("")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(open("", "r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(open("", "rb")) # revealed: BufferedReader[_BufferedReaderStream]
reveal_type(open("", "rb")) # revealed: @Todo(`builtins.open` return type)
with open("foo.pickle", "rb") as f:
x = pickle.load(f) # fine
def _(mode: str):
reveal_type(open("", mode)) # revealed: IO[Any]
reveal_type(open("", mode)) # revealed: @Todo(`builtins.open` return type)
```
## `os.fdopen`
@@ -29,7 +29,7 @@ import os
reveal_type(os.fdopen(0)) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(os.fdopen(0, "r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(os.fdopen(0, "rb")) # revealed: BufferedReader[_BufferedReaderStream]
reveal_type(os.fdopen(0, "rb")) # revealed: @Todo(`os.fdopen` return type)
with os.fdopen(0, "rb") as f:
x = pickle.load(f) # fine
@@ -43,9 +43,9 @@ And similarly for `Path.open()`:
from pathlib import Path
import pickle
reveal_type(Path("").open()) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(Path("").open("r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(Path("").open("rb")) # revealed: BufferedReader[_BufferedReaderStream]
reveal_type(Path("").open()) # revealed: @Todo(`Path.open` return type)
reveal_type(Path("").open("r")) # revealed: @Todo(`Path.open` return type)
reveal_type(Path("").open("rb")) # revealed: @Todo(`Path.open` return type)
with Path("foo.pickle").open("rb") as f:
x = pickle.load(f) # fine
@@ -61,7 +61,7 @@ import pickle
reveal_type(NamedTemporaryFile()) # revealed: _TemporaryFileWrapper[bytes]
reveal_type(NamedTemporaryFile("r")) # revealed: _TemporaryFileWrapper[str]
reveal_type(NamedTemporaryFile("rb")) # revealed: _TemporaryFileWrapper[bytes]
reveal_type(NamedTemporaryFile("rb")) # revealed: @Todo(`tempfile.NamedTemporaryFile` return type)
with NamedTemporaryFile("rb") as f:
x = pickle.load(f) # fine

View File

@@ -1,52 +0,0 @@
# Invalid assignment diagnostics
<!-- snapshot-diagnostics -->
## Annotated assignment
```py
x: int = "three" # error: [invalid-assignment]
```
## Unannotated assignment
```py
x: int
x = "three" # error: [invalid-assignment]
```
## Named expression
```py
x: int
(x := "three") # error: [invalid-assignment]
```
## Multiline expressions
```py
# fmt: off
# error: [invalid-assignment]
x: str = (
1 + 2 + (
3 + 4 + 5
)
)
```
## Multiple targets
```py
x: int
y: str
x, y = ("a", "b") # error: [invalid-assignment]
x, y = (0, 0) # error: [invalid-assignment]
```
## Shadowing of classes and functions
See [shadowing.md](./shadowing.md).

View File

@@ -265,7 +265,7 @@ import dict_a
import dict_b
def _(b_person: dict_b.Person):
# error: [invalid-assignment] "Object of type `dict_b.Person` is not assignable to `dict_a.Person`"
# TODO should be error: [invalid-assignment] "Object of type `dict_b.Person` is not assignable to `dict_a.Person`"
person_var: dict_a.Person = b_person
```

View File

@@ -80,7 +80,7 @@ def if_else_isinstance_error(obj: A | B):
elif isinstance(obj, C):
pass
else:
# error: [type-assertion-failure] "Type `B & ~A & ~C` is not equivalent to `Never`"
# error: [type-assertion-failure] "Argument does not have asserted type `Never`"
assert_never(obj)
def if_else_singletons_success(obj: Literal[1, "a"] | None):
@@ -101,7 +101,7 @@ def if_else_singletons_error(obj: Literal[1, "a"] | None):
elif obj is None:
pass
else:
# error: [type-assertion-failure] "Type `Literal["a"]` is not equivalent to `Never`"
# error: [type-assertion-failure] "Argument does not have asserted type `Never`"
assert_never(obj)
def match_singletons_success(obj: Literal[1, "a"] | None):
@@ -125,9 +125,7 @@ def match_singletons_error(obj: Literal[1, "a"] | None):
pass
case _ as obj:
# TODO: We should emit an error here, but the message should
# show the type `Literal["a"]` instead of `@Todo(…)`. We only
# assert on the first part of the message because the `@Todo`
# message is not available in release mode builds.
# error: [type-assertion-failure] "Type `@Todo"
# show the type `Literal["a"]` instead of `@Todo(…)`.
# error: [type-assertion-failure] "Argument does not have asserted type `Never`"
assert_never(obj)
```

View File

@@ -41,11 +41,11 @@ from typing_extensions import assert_type
# Subtype does not count
def _(x: bool):
assert_type(x, int) # error: [type-assertion-failure] "Type `int` does not match asserted type `bool`"
assert_type(x, int) # error: [type-assertion-failure]
def _(a: type[int], b: type[Any]):
assert_type(a, type[Any]) # error: [type-assertion-failure] "Type `type[Any]` does not match asserted type `type[int]`"
assert_type(b, type[int]) # error: [type-assertion-failure] "Type `type[int]` does not match asserted type `type[Any]`"
assert_type(a, type[Any]) # error: [type-assertion-failure]
assert_type(b, type[int]) # error: [type-assertion-failure]
# The expression constructing the type is not taken into account
def _(a: type[int]):

View File

@@ -901,7 +901,7 @@ def color_name_misses_one_variant(color: Color) -> str:
elif color is Color.GREEN:
return "Green"
else:
assert_never(color) # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`"
assert_never(color) # error: [type-assertion-failure] "Argument does not have asserted type `Never`"
class Singleton(Enum):
VALUE = 1
@@ -956,7 +956,7 @@ def color_name_misses_one_variant(color: Color) -> str:
case Color.GREEN:
return "Green"
case _:
assert_never(color) # error: [type-assertion-failure] "Type `Literal[Color.BLUE]` is not equivalent to `Never`"
assert_never(color) # error: [type-assertion-failure] "Argument does not have asserted type `Never`"
class Singleton(Enum):
VALUE = 1

View File

@@ -127,7 +127,7 @@ x = lambda y: y
reveal_type(x.__code__) # revealed: CodeType
reveal_type(x.__name__) # revealed: str
reveal_type(x.__defaults__) # revealed: tuple[Any, ...] | None
reveal_type(x.__annotations__) # revealed: dict[str, Any]
reveal_type(x.__annotations__) # revealed: dict[str, @Todo(Support for `typing.TypeAlias`)]
reveal_type(x.__dict__) # revealed: dict[str, Any]
reveal_type(x.__doc__) # revealed: str | None
reveal_type(x.__kwdefaults__) # revealed: dict[str, Any] | None

View File

@@ -21,14 +21,14 @@ class TypeVarAndParamSpec(Generic[P, T]): ...
class SingleTypeVarTuple(Generic[Unpack[Ts]]): ...
class TypeVarAndTypeVarTuple(Generic[T, Unpack[Ts]]): ...
# revealed: ty_extensions.GenericContext[T@SingleTypevar]
# revealed: tuple[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar))
# revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars]
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))
# revealed: ty_extensions.GenericContext[P@SingleParamSpec]
# revealed: tuple[P@SingleParamSpec]
reveal_type(generic_context(SingleParamSpec))
# revealed: ty_extensions.GenericContext[P@TypeVarAndParamSpec, T@TypeVarAndParamSpec]
# revealed: tuple[P@TypeVarAndParamSpec, T@TypeVarAndParamSpec]
reveal_type(generic_context(TypeVarAndParamSpec))
# TODO: support `TypeVarTuple` properly (these should not reveal `None`)
@@ -66,9 +66,9 @@ class InheritedGeneric(MultipleTypevars[T, S]): ...
class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ...
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...
# revealed: ty_extensions.GenericContext[T@InheritedGeneric, S@InheritedGeneric]
# revealed: tuple[T@InheritedGeneric, S@InheritedGeneric]
reveal_type(generic_context(InheritedGeneric))
# revealed: ty_extensions.GenericContext[T@InheritedGenericPartiallySpecialized]
# revealed: tuple[T@InheritedGenericPartiallySpecialized]
reveal_type(generic_context(InheritedGenericPartiallySpecialized))
# revealed: None
reveal_type(generic_context(InheritedGenericFullySpecialized))
@@ -90,7 +90,7 @@ class OuterClass(Generic[T]):
# revealed: None
reveal_type(generic_context(InnerClassInMethod))
# revealed: ty_extensions.GenericContext[T@OuterClass]
# revealed: tuple[T@OuterClass]
reveal_type(generic_context(OuterClass))
```
@@ -118,11 +118,11 @@ class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ...
# revealed: ty_extensions.GenericContext[T@ExplicitInheritedGeneric, S@ExplicitInheritedGeneric]
# revealed: tuple[T@ExplicitInheritedGeneric, S@ExplicitInheritedGeneric]
reveal_type(generic_context(ExplicitInheritedGeneric))
# revealed: ty_extensions.GenericContext[T@ExplicitInheritedGenericPartiallySpecialized]
# revealed: tuple[T@ExplicitInheritedGenericPartiallySpecialized]
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized))
# revealed: ty_extensions.GenericContext[T@ExplicitInheritedGenericPartiallySpecializedExtraTypevar, S@ExplicitInheritedGenericPartiallySpecializedExtraTypevar]
# revealed: tuple[T@ExplicitInheritedGenericPartiallySpecializedExtraTypevar, S@ExplicitInheritedGenericPartiallySpecializedExtraTypevar]
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar))
```
@@ -594,27 +594,18 @@ class C(Generic[T]):
def generic_method(self, t: T, u: U) -> U:
return u
# revealed: ty_extensions.GenericContext[T@C]
reveal_type(generic_context(C))
# revealed: ty_extensions.GenericContext[Self@method]
reveal_type(generic_context(C.method))
# revealed: ty_extensions.GenericContext[Self@generic_method, U@generic_method]
reveal_type(generic_context(C.generic_method))
# revealed: None
reveal_type(generic_context(C[int]))
# revealed: ty_extensions.GenericContext[Self@method]
reveal_type(generic_context(C[int].method))
# revealed: ty_extensions.GenericContext[Self@generic_method, U@generic_method]
reveal_type(generic_context(C[int].generic_method))
reveal_type(generic_context(C)) # revealed: tuple[T@C]
reveal_type(generic_context(C.method)) # revealed: tuple[Self@method]
reveal_type(generic_context(C.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
reveal_type(generic_context(C[int])) # revealed: None
reveal_type(generic_context(C[int].method)) # revealed: tuple[Self@method]
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
c: C[int] = C[int]()
reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"]
# revealed: None
reveal_type(generic_context(c))
# revealed: ty_extensions.GenericContext[Self@method]
reveal_type(generic_context(c.method))
# revealed: ty_extensions.GenericContext[Self@generic_method, U@generic_method]
reveal_type(generic_context(c.generic_method))
reveal_type(generic_context(c)) # revealed: None
reveal_type(generic_context(c.method)) # revealed: tuple[Self@method]
reveal_type(generic_context(c.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
```
## Specializations propagate

View File

@@ -20,21 +20,17 @@ type TypeVarAndParamSpec[T, **P] = ...
type SingleTypeVarTuple[*Ts] = ...
type TypeVarAndTypeVarTuple[T, *Ts] = ...
# revealed: ty_extensions.GenericContext[T@SingleTypevar]
# revealed: tuple[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar))
# revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars]
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))
# TODO: support `ParamSpec`/`TypeVarTuple` properly
# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleParamSpec))
# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec]
reveal_type(generic_context(TypeVarAndParamSpec))
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleTypeVarTuple))
# revealed: ty_extensions.GenericContext[T@TypeVarAndTypeVarTuple]
reveal_type(generic_context(TypeVarAndTypeVarTuple))
reveal_type(generic_context(SingleParamSpec)) # revealed: tuple[()]
reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: tuple[T@TypeVarAndParamSpec]
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: tuple[()]
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: tuple[T@TypeVarAndTypeVarTuple]
```
You cannot use the same typevar more than once.

View File

@@ -20,21 +20,17 @@ class TypeVarAndParamSpec[T, **P]: ...
class SingleTypeVarTuple[*Ts]: ...
class TypeVarAndTypeVarTuple[T, *Ts]: ...
# revealed: ty_extensions.GenericContext[T@SingleTypevar]
# revealed: tuple[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar))
# revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars]
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))
# TODO: support `ParamSpec`/`TypeVarTuple` properly
# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleParamSpec))
# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec]
reveal_type(generic_context(TypeVarAndParamSpec))
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleTypeVarTuple))
# revealed: ty_extensions.GenericContext[T@TypeVarAndTypeVarTuple]
reveal_type(generic_context(TypeVarAndTypeVarTuple))
reveal_type(generic_context(SingleParamSpec)) # revealed: tuple[()]
reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: tuple[T@TypeVarAndParamSpec]
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: tuple[()]
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: tuple[T@TypeVarAndTypeVarTuple]
```
You cannot use the same typevar more than once.
@@ -53,9 +49,9 @@ class InheritedGeneric[U, V](MultipleTypevars[U, V]): ...
class InheritedGenericPartiallySpecialized[U](MultipleTypevars[U, int]): ...
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...
# revealed: ty_extensions.GenericContext[U@InheritedGeneric, V@InheritedGeneric]
# revealed: tuple[U@InheritedGeneric, V@InheritedGeneric]
reveal_type(generic_context(InheritedGeneric))
# revealed: ty_extensions.GenericContext[U@InheritedGenericPartiallySpecialized]
# revealed: tuple[U@InheritedGenericPartiallySpecialized]
reveal_type(generic_context(InheritedGenericPartiallySpecialized))
# revealed: None
reveal_type(generic_context(InheritedGenericFullySpecialized))
@@ -68,8 +64,7 @@ the inheriting class generic.
```py
class InheritedGenericDefaultSpecialization(MultipleTypevars): ...
# revealed: None
reveal_type(generic_context(InheritedGenericDefaultSpecialization))
reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None
```
You cannot use PEP-695 syntax and the legacy syntax in the same class definition.
@@ -517,27 +512,18 @@ class C[T]:
# TODO: error
def cannot_shadow_class_typevar[T](self, t: T): ...
# revealed: ty_extensions.GenericContext[T@C]
reveal_type(generic_context(C))
# revealed: ty_extensions.GenericContext[Self@method]
reveal_type(generic_context(C.method))
# revealed: ty_extensions.GenericContext[Self@generic_method, U@generic_method]
reveal_type(generic_context(C.generic_method))
# revealed: None
reveal_type(generic_context(C[int]))
# revealed: ty_extensions.GenericContext[Self@method]
reveal_type(generic_context(C[int].method))
# revealed: ty_extensions.GenericContext[Self@generic_method, U@generic_method]
reveal_type(generic_context(C[int].generic_method))
reveal_type(generic_context(C)) # revealed: tuple[T@C]
reveal_type(generic_context(C.method)) # revealed: tuple[Self@method]
reveal_type(generic_context(C.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
reveal_type(generic_context(C[int])) # revealed: None
reveal_type(generic_context(C[int].method)) # revealed: tuple[Self@method]
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
c: C[int] = C[int]()
reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"]
# revealed: None
reveal_type(generic_context(c))
# revealed: ty_extensions.GenericContext[Self@method]
reveal_type(generic_context(c.method))
# revealed: ty_extensions.GenericContext[Self@generic_method, U@generic_method]
reveal_type(generic_context(c.generic_method))
reveal_type(generic_context(c)) # revealed: None
reveal_type(generic_context(c.method)) # revealed: tuple[Self@method]
reveal_type(generic_context(c.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
```
## Specializations propagate

View File

@@ -154,10 +154,8 @@ from ty_extensions import generic_context
legacy.m("string", None) # error: [invalid-argument-type]
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S@m) -> S@m
# revealed: ty_extensions.GenericContext[T@Legacy]
reveal_type(generic_context(Legacy))
# revealed: ty_extensions.GenericContext[Self@m, S@m]
reveal_type(generic_context(legacy.m))
reveal_type(generic_context(Legacy)) # revealed: tuple[T@Legacy]
reveal_type(generic_context(legacy.m)) # revealed: tuple[Self@m, S@m]
```
With PEP 695 syntax, it is clearer that the method uses a separate typevar:

View File

@@ -1,335 +0,0 @@
# Creating a specialization from a constraint set
```toml
[environment]
python-version = "3.12"
```
We create constraint sets to describe which types a set of typevars can specialize to. We have a
`specialize_constrained` method that creates a "best" specialization for a constraint set, which
lets us test this logic in isolation, without having to bring in the rest of the specialization
inference logic.
## Unbounded typevars
An unbounded typevar can specialize to any type. We will specialize the typevar to the least upper
bound of all of the types that satisfy the constraint set.
```py
from typing import Never
from ty_extensions import ConstraintSet, generic_context
# fmt: off
def unbounded[T]():
# revealed: ty_extensions.Specialization[T@unbounded = object]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.never()))
# revealed: ty_extensions.Specialization[T@unbounded = int]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int)))
# revealed: ty_extensions.Specialization[T@unbounded = int]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(bool, T, int)))
# revealed: ty_extensions.Specialization[T@unbounded = bool]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, T, bool)))
# revealed: ty_extensions.Specialization[T@unbounded = Never]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, T, str)))
# revealed: None
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(bool, T, bool) & ConstraintSet.range(Never, T, str)))
# revealed: ty_extensions.Specialization[T@unbounded = int]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, bool)))
# revealed: ty_extensions.Specialization[T@unbounded = Never]
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, str)))
# revealed: None
reveal_type(generic_context(unbounded).specialize_constrained(ConstraintSet.range(bool, T, bool) | ConstraintSet.range(Never, T, str)))
```
## Typevar with an upper bound
If a typevar has an upper bound, then it must specialize to a type that is a subtype of that bound.
```py
from typing import final, Never
from ty_extensions import ConstraintSet, generic_context
class Super: ...
class Base(Super): ...
class Sub(Base): ...
@final
class Unrelated: ...
def bounded[T: Base]():
# revealed: ty_extensions.Specialization[T@bounded = Base]
reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.never()))
# revealed: ty_extensions.Specialization[T@bounded = Base]
reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Super)))
# revealed: ty_extensions.Specialization[T@bounded = Base]
reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Base)))
# revealed: ty_extensions.Specialization[T@bounded = Sub]
reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Sub)))
# revealed: ty_extensions.Specialization[T@bounded = Never]
reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Never, T, Unrelated)))
# revealed: None
reveal_type(generic_context(bounded).specialize_constrained(ConstraintSet.range(Unrelated, T, Unrelated)))
```
If the upper bound is a gradual type, we are free to choose any materialization of the upper bound
that makes the test succeed.
```py
from typing import Any
def bounded_by_gradual[T: Any]():
# revealed: ty_extensions.Specialization[T@bounded_by_gradual = object]
reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.never()))
# revealed: ty_extensions.Specialization[T@bounded_by_gradual = Base]
reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base)))
# revealed: ty_extensions.Specialization[T@bounded_by_gradual = Unrelated]
reveal_type(generic_context(bounded_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated)))
def bounded_by_gradual_list[T: list[Any]]():
# revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = Top[list[Any]]]
reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.never()))
# revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = list[Base]]
reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Base])))
# revealed: ty_extensions.Specialization[T@bounded_by_gradual_list = list[Unrelated]]
reveal_type(generic_context(bounded_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Unrelated])))
```
## Constrained typevar
If a typevar has constraints, then it must specialize to one of those specific types. (Not to a
subtype of one of those types!)
In particular, note that if a constraint set is satisfied by more than one of the typevar's
constraints (i.e., we have no reason to prefer one over the others), then we return `None` to
indicate an ambiguous result. We could, in theory, return _more than one_ specialization, since we
have all of the information necessary to produce this. But it's not clear what we would do with that
information at the moment.
```py
from typing import final, Never
from ty_extensions import ConstraintSet, generic_context
class Super: ...
class Base(Super): ...
class Sub(Base): ...
@final
class Unrelated: ...
def constrained[T: (Base, Unrelated)]():
# revealed: None
reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.never()))
# revealed: ty_extensions.Specialization[T@constrained = Base]
reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Never, T, Base)))
# revealed: ty_extensions.Specialization[T@constrained = Unrelated]
reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Never, T, Unrelated)))
# revealed: ty_extensions.Specialization[T@constrained = Base]
reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Never, T, Super)))
# revealed: None
reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Super, T, Super)))
# revealed: ty_extensions.Specialization[T@constrained = Base]
reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Sub, T, object)))
# revealed: None
reveal_type(generic_context(constrained).specialize_constrained(ConstraintSet.range(Sub, T, Sub)))
```
If any of the constraints is a gradual type, we are free to choose any materialization of that
constraint that makes the test succeed.
TODO: At the moment, we are producing a specialization that shows which particular materialization
that we chose, but really, we should be returning the gradual constraint as the specialization.
```py
from typing import Any
# fmt: off
def constrained_by_gradual[T: (Base, Any)]():
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = object]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.never()))
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Base]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Unrelated]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Never, T, Super)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Super]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Super, T, Super)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = object]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Sub, T, object)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual = Sub]
reveal_type(generic_context(constrained_by_gradual).specialize_constrained(ConstraintSet.range(Sub, T, Sub)))
def constrained_by_two_gradual[T: (Any, Any)]():
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = object]
reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.never()))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Base]
reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Never, T, Base)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Unrelated]
reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Never, T, Unrelated)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Super]
reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Never, T, Super)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Super]
reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Super, T, Super)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = object]
reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Sub, T, object)))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = Any]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual = Sub]
reveal_type(generic_context(constrained_by_two_gradual).specialize_constrained(ConstraintSet.range(Sub, T, Sub)))
def constrained_by_gradual_list[T: (list[Base], list[Any])]():
# revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Base]]
reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.never()))
# revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Base]]
reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Base])))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Unrelated]]
reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Unrelated])))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Super]]
reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(Never, T, list[Super])))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Super]]
reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(list[Super], T, list[Super])))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_gradual_list = list[Sub]]
reveal_type(generic_context(constrained_by_gradual_list).specialize_constrained(ConstraintSet.range(list[Sub], T, list[Sub])))
def constrained_by_two_gradual_lists[T: (list[Any], list[Any])]():
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = Top[list[Any]]]
reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.never()))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Base]]
reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(Never, T, list[Base])))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Unrelated]]
reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(Never, T, list[Unrelated])))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Super]]
reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(Never, T, list[Super])))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Super]]
reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(list[Super], T, list[Super])))
# TODO: revealed: ty_extensions.Specialization[T@constrained_by_gradual = list[Any]]
# revealed: ty_extensions.Specialization[T@constrained_by_two_gradual_lists = list[Sub]]
reveal_type(generic_context(constrained_by_two_gradual_lists).specialize_constrained(ConstraintSet.range(list[Sub], T, list[Sub])))
```
## Mutually constrained typevars
If one typevar is constrained by another, the specialization of one can affect the specialization of
the other.
```py
from typing import final, Never
from ty_extensions import ConstraintSet, generic_context
class Super: ...
class Base(Super): ...
class Sub(Base): ...
@final
class Unrelated: ...
# fmt: off
def mutually_bound[T: Base, U]():
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = object]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.always()))
# revealed: None
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.never()))
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Base]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, U, T)))
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = object]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub)))
# revealed: ty_extensions.Specialization[T@mutually_bound = Sub, U@mutually_bound = Sub]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, T, Sub) & ConstraintSet.range(Never, U, T)))
# revealed: ty_extensions.Specialization[T@mutually_bound = Base, U@mutually_bound = Sub]
reveal_type(generic_context(mutually_bound).specialize_constrained(ConstraintSet.range(Never, U, Sub) & ConstraintSet.range(Never, U, T)))
```
## Nested typevars
A typevar's constraint can _mention_ another typevar without _constraining_ it. In this example, `U`
must be specialized to `list[T]`, but it cannot affect what `T` is specialized to.
```py
from typing import Never
from ty_extensions import ConstraintSet, generic_context
def mentions[T, U]():
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(list[T], U, list[T])
# revealed: ty_extensions.ConstraintSet[((T@mentions ≤ int) ∧ (U@mentions = list[T@mentions]))]
reveal_type(constraints)
# revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]]
reveal_type(generic_context(mentions).specialize_constrained(constraints))
```
If the constraint set contains mutually recursive bounds, specialization inference will not
converge. This test ensures that our cycle detection prevents an endless loop or stack overflow in
this case.
```py
def divergent[T, U]():
constraints = ConstraintSet.range(list[U], T, list[U]) & ConstraintSet.range(list[T], U, list[T])
# revealed: ty_extensions.ConstraintSet[((T@divergent = list[U@divergent]) ∧ (U@divergent = list[T@divergent]))]
reveal_type(constraints)
# revealed: None
reveal_type(generic_context(divergent).specialize_constrained(constraints))
```

View File

@@ -33,11 +33,9 @@ g(None)
We also support unions in type aliases:
```py
from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional, Union, Callable, TypeVar
from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional, Union
from ty_extensions import Unknown
T = TypeVar("T")
IntOrStr = int | str
IntOrStrOrBytes1 = int | str | bytes
IntOrStrOrBytes2 = (int | str) | bytes
@@ -70,12 +68,6 @@ IntOrOptional = int | Optional[str]
OptionalOrInt = Optional[str] | int
IntOrTypeOfStr = int | type[str]
TypeOfStrOrInt = type[str] | int
IntOrCallable = int | Callable[[str], bytes]
CallableOrInt = Callable[[str], bytes] | int
TypeVarOrInt = T | int
IntOrTypeVar = int | T
TypeVarOrNone = T | None
NoneOrTypeVar = None | T
reveal_type(IntOrStr) # revealed: types.UnionType
reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
@@ -109,12 +101,6 @@ reveal_type(IntOrOptional) # revealed: types.UnionType
reveal_type(OptionalOrInt) # revealed: types.UnionType
reveal_type(IntOrTypeOfStr) # revealed: types.UnionType
reveal_type(TypeOfStrOrInt) # revealed: types.UnionType
reveal_type(IntOrCallable) # revealed: types.UnionType
reveal_type(CallableOrInt) # revealed: types.UnionType
reveal_type(TypeVarOrInt) # revealed: types.UnionType
reveal_type(IntOrTypeVar) # revealed: types.UnionType
reveal_type(TypeVarOrNone) # revealed: types.UnionType
reveal_type(NoneOrTypeVar) # revealed: types.UnionType
def _(
int_or_str: IntOrStr,
@@ -149,12 +135,6 @@ def _(
optional_or_int: OptionalOrInt,
int_or_type_of_str: IntOrTypeOfStr,
type_of_str_or_int: TypeOfStrOrInt,
int_or_callable: IntOrCallable,
callable_or_int: CallableOrInt,
type_var_or_int: TypeVarOrInt,
int_or_type_var: IntOrTypeVar,
type_var_or_none: TypeVarOrNone,
none_or_type_var: NoneOrTypeVar,
):
reveal_type(int_or_str) # revealed: int | str
reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes
@@ -188,16 +168,6 @@ def _(
reveal_type(optional_or_int) # revealed: str | None | int
reveal_type(int_or_type_of_str) # revealed: int | type[str]
reveal_type(type_of_str_or_int) # revealed: type[str] | int
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
# TODO should be Unknown | int
reveal_type(type_var_or_int) # revealed: typing.TypeVar | int
# TODO should be int | Unknown
reveal_type(int_or_type_var) # revealed: int | typing.TypeVar
# TODO should be Unknown | None
reveal_type(type_var_or_none) # revealed: typing.TypeVar | None
# TODO should be None | Unknown
reveal_type(none_or_type_var) # revealed: None | typing.TypeVar
```
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -368,190 +338,25 @@ def g(obj: Y):
## Generic types
Implicit type aliases can also be generic:
Implicit type aliases can also refer to generic types:
```py
from typing_extensions import TypeVar, ParamSpec, Callable, Union, Annotated
from typing_extensions import TypeVar
T = TypeVar("T")
U = TypeVar("U")
P = ParamSpec("P")
MyList = list[T]
MyDict = dict[T, U]
MyType = type[T]
IntAndType = tuple[int, T]
Pair = tuple[T, T]
Sum = tuple[T, U]
ListOrTuple = list[T] | tuple[T, ...]
ListOrTupleLegacy = Union[list[T], tuple[T, ...]]
MyCallable = Callable[P, T]
AnnotatedType = Annotated[T, "tag"]
reveal_type(MyList) # revealed: <class 'list[T]'>
reveal_type(MyDict) # revealed: <class 'dict[T, U]'>
reveal_type(MyType) # revealed: GenericAlias
reveal_type(IntAndType) # revealed: <class 'tuple[int, typing.TypeVar]'>
reveal_type(Pair) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
reveal_type(Sum) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
reveal_type(ListOrTuple) # revealed: types.UnionType
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
reveal_type(MyCallable) # revealed: GenericAlias
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
def _(
list_of_ints: MyList[int],
dict_str_to_int: MyDict[str, int],
# TODO: no error here
# error: [invalid-type-form] "`typing.TypeVar` is not a generic class"
subclass_of_int: MyType[int],
int_and_str: IntAndType[str],
pair_of_ints: Pair[int],
int_and_bytes: Sum[int, bytes],
list_or_tuple: ListOrTuple[int],
list_or_tuple_legacy: ListOrTupleLegacy[int],
# TODO: no error here
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[str, bytes]`?"
my_callable: MyCallable[[str, bytes], int],
annotated_int: AnnotatedType[int],
):
def _(my_list: MyList[int]):
# TODO: This should be `list[int]`
reveal_type(list_of_ints) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `dict[str, int]`
reveal_type(dict_str_to_int) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `type[int]`
reveal_type(subclass_of_int) # revealed: Unknown
# TODO: This should be `tuple[int, str]`
reveal_type(int_and_str) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `tuple[int, int]`
reveal_type(pair_of_ints) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `tuple[int, bytes]`
reveal_type(int_and_bytes) # revealed: @Todo(specialized generic alias in type expression)
# TODO: This should be `list[int] | tuple[int, ...]`
reveal_type(my_list) # revealed: @Todo(unknown type subscript)
ListOrTuple = list[T] | tuple[T, ...]
reveal_type(ListOrTuple) # revealed: types.UnionType
def _(list_or_tuple: ListOrTuple[int]):
reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType)
# TODO: This should be `list[int] | tuple[int, ...]`
reveal_type(list_or_tuple_legacy) # revealed: @Todo(Generic specialization of types.UnionType)
# TODO: This should be `(str, bytes) -> int`
reveal_type(my_callable) # revealed: @Todo(Generic specialization of typing.Callable)
# TODO: This should be `int`
reveal_type(annotated_int) # revealed: @Todo(Generic specialization of typing.Annotated)
```
Generic implicit type aliases can be partially specialized:
```py
U = TypeVar("U")
DictStrTo = MyDict[str, U]
reveal_type(DictStrTo) # revealed: GenericAlias
def _(
# TODO: No error here
# error: [invalid-type-form] "Invalid subscript of object of type `GenericAlias` in type expression"
dict_str_to_int: DictStrTo[int],
):
# TODO: This should be `dict[str, int]`
reveal_type(dict_str_to_int) # revealed: Unknown
```
Using specializations of generic implicit type aliases in other implicit type aliases works as
expected:
```py
IntsOrNone = MyList[int] | None
IntsOrStrs = Pair[int] | Pair[str]
ListOfPairs = MyList[Pair[str]]
reveal_type(IntsOrNone) # revealed: UnionType
reveal_type(IntsOrStrs) # revealed: UnionType
reveal_type(ListOfPairs) # revealed: GenericAlias
def _(
# TODO: This should not be an error
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
ints_or_none: IntsOrNone,
# TODO: This should not be an error
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
ints_or_strs: IntsOrStrs,
list_of_pairs: ListOfPairs,
):
# TODO: This should be `list[int] | None`
reveal_type(ints_or_none) # revealed: Unknown
# TODO: This should be `tuple[int, int] | tuple[str, str]`
reveal_type(ints_or_strs) # revealed: Unknown
# TODO: This should be `list[tuple[str, str]]`
reveal_type(list_of_pairs) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
```
If a generic implicit type alias is used unspecialized in a type expression, we treat it as an
`Unknown` specialization:
```py
def _(
my_list: MyList,
my_dict: MyDict,
my_callable: MyCallable,
):
# TODO: Should be `list[Unknown]`
reveal_type(my_list) # revealed: list[T]
# TODO: Should be `dict[Unknown, Unknown]`
reveal_type(my_dict) # revealed: dict[T, U]
# TODO: Should be `(...) -> Unknown`
reveal_type(my_callable) # revealed: (...) -> typing.TypeVar
```
(Generic) implicit type aliases can be used as base classes:
```py
from typing_extensions import Generic
from ty_extensions import reveal_mro
class GenericBase(Generic[T]):
pass
ConcreteBase = GenericBase[int]
class Derived1(ConcreteBase):
pass
# revealed: (<class 'Derived1'>, <class 'GenericBase[int]'>, typing.Generic, <class 'object'>)
reveal_mro(Derived1)
GenericBaseAlias = GenericBase[T]
# TODO: No error here
# error: [non-subscriptable] "Cannot subscript object of type `<class 'GenericBase[T]'>` with no `__class_getitem__` method"
class Derived2(GenericBaseAlias[int]):
pass
```
A generic alias that is already fully specialized cannot be specialized again:
```py
ListOfInts = list[int]
# TODO: this should be an error
def _(doubly_specialized: ListOfInts[int]):
# TODO: this should be `Unknown`
reveal_type(doubly_specialized) # revealed: @Todo(specialized generic alias in type expression)
```
Specializing a generic implicit type alias with an incorrect number of type arguments also results
in an error:
```py
def _(
# TODO: this should be an error
list_too_many_args: MyList[int, str],
# TODO: this should be an error
dict_too_few_args: MyDict[int],
):
# TODO: this should be `Unknown`
reveal_type(list_too_many_args) # revealed: @Todo(specialized generic alias in type expression)
# TODO: this should be `Unknown`
reveal_type(dict_too_few_args) # revealed: @Todo(specialized generic alias in type expression)
```
## `Literal`s
@@ -1108,7 +913,7 @@ from typing import List, Dict
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidList = List[1]
# error: [invalid-type-form] "`typing.List` requires exactly one argument"
# error: [invalid-type-form] "`typing.typing.List` requires exactly one argument"
ListTooManyArgs = List[int, str]
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
@@ -1117,10 +922,10 @@ InvalidDict1 = Dict[1, str]
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidDict2 = Dict[str, 2]
# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 1"
# error: [invalid-type-form] "`typing.typing.Dict` requires exactly two arguments, got 1"
DictTooFewArgs = Dict[str]
# error: [invalid-type-form] "`typing.Dict` requires exactly two arguments, got 3"
# error: [invalid-type-form] "`typing.typing.Dict` requires exactly two arguments, got 3"
DictTooManyArgs = Dict[str, int, float]
def _(
@@ -1139,60 +944,7 @@ def _(
reveal_type(dict_too_many_args) # revealed: dict[Unknown, Unknown]
```
## `Callable[...]`
We support implicit type aliases using `Callable[...]`:
```py
from typing import Callable, Union
CallableNoArgs = Callable[[], None]
BasicCallable = Callable[[int, str], bytes]
GradualCallable = Callable[..., str]
reveal_type(CallableNoArgs) # revealed: GenericAlias
reveal_type(BasicCallable) # revealed: GenericAlias
reveal_type(GradualCallable) # revealed: GenericAlias
def _(
callable_no_args: CallableNoArgs,
basic_callable: BasicCallable,
gradual_callable: GradualCallable,
):
reveal_type(callable_no_args) # revealed: () -> None
reveal_type(basic_callable) # revealed: (int, str, /) -> bytes
reveal_type(gradual_callable) # revealed: (...) -> str
```
Nested callables work as expected:
```py
TakesCallable = Callable[[Callable[[int], str]], bytes]
ReturnsCallable = Callable[[int], Callable[[str], bytes]]
def _(takes_callable: TakesCallable, returns_callable: ReturnsCallable):
reveal_type(takes_callable) # revealed: ((int, /) -> str, /) -> bytes
reveal_type(returns_callable) # revealed: (int, /) -> (str, /) -> bytes
```
Invalid uses result in diagnostics:
```py
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
InvalidCallable1 = Callable[[int]]
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
InvalidCallable2 = Callable[int, str]
reveal_type(InvalidCallable1) # revealed: GenericAlias
reveal_type(InvalidCallable2) # revealed: GenericAlias
def _(invalid_callable1: InvalidCallable1, invalid_callable2: InvalidCallable2):
reveal_type(invalid_callable1) # revealed: (...) -> Unknown
reveal_type(invalid_callable2) # revealed: (...) -> Unknown
```
## Stringified annotations
## Stringified annotations?
From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):
@@ -1222,7 +974,7 @@ We *do* support stringified annotations if they appear in a position where a typ
syntactically expected:
```py
from typing import Union, List, Dict, Annotated, Callable
from typing import Union, List, Dict, Annotated
ListOfInts1 = list["int"]
ListOfInts2 = List["int"]
@@ -1230,7 +982,6 @@ StrOrStyle = Union[str, "Style"]
SubclassOfStyle = type["Style"]
DictStrToStyle = Dict[str, "Style"]
AnnotatedStyle = Annotated["Style", "metadata"]
CallableStyleToStyle = Callable[["Style"], "Style"]
class Style: ...
@@ -1241,7 +992,6 @@ def _(
subclass_of_style: SubclassOfStyle,
dict_str_to_style: DictStrToStyle,
annotated_style: AnnotatedStyle,
callable_style_to_style: CallableStyleToStyle,
):
reveal_type(list_of_ints1) # revealed: list[int]
reveal_type(list_of_ints2) # revealed: list[int]
@@ -1249,7 +999,6 @@ def _(
reveal_type(subclass_of_style) # revealed: type[Style]
reveal_type(dict_str_to_style) # revealed: dict[str, Style]
reveal_type(annotated_style) # revealed: Style
reveal_type(callable_style_to_style) # revealed: (Style, /) -> Style
```
## Recursive

View File

@@ -147,57 +147,6 @@ def _(x: int | str | bytes):
reveal_type(x) # revealed: (int & Unknown) | (str & Unknown) | (bytes & Unknown)
```
## `classinfo` is a `types.UnionType`
Python 3.10 added the ability to use `Union[int, str]` as the second argument to `isinstance()`:
```py
from typing import Union
IntOrStr = Union[int, str]
reveal_type(IntOrStr) # revealed: types.UnionType
def _(x: int | str | bytes | memoryview | range):
if isinstance(x, IntOrStr):
reveal_type(x) # revealed: int | str
elif isinstance(x, Union[bytes, memoryview]):
reveal_type(x) # revealed: bytes | memoryview[int]
else:
reveal_type(x) # revealed: range
def _(x: int | str | None):
if isinstance(x, Union[int, None]):
reveal_type(x) # revealed: int | None
else:
reveal_type(x) # revealed: str
ListStrOrInt = Union[list[str], int]
def _(x: dict[int, str] | ListStrOrInt):
# TODO: this should ideally be an error
if isinstance(x, ListStrOrInt):
# TODO: this should not be narrowed
reveal_type(x) # revealed: list[str] | int
# TODO: this should ideally be an error
if isinstance(x, Union[list[str], int]):
# TODO: this should not be narrowed
reveal_type(x) # revealed: list[str] | int
```
## `Optional` as `classinfo`
```py
from typing import Optional
def _(x: int | str | None):
if isinstance(x, Optional[int]):
reveal_type(x) # revealed: int | None
else:
reveal_type(x) # revealed: str
```
## `classinfo` is a `typing.py` special form
Certain special forms in `typing.py` are aliases to classes elsewhere in the standard library; these
@@ -313,23 +262,6 @@ def _(flag: bool):
reveal_type(x) # revealed: Literal[1, "a"]
```
## Generic aliases are not supported as second argument
The `classinfo` argument cannot be a generic alias:
```py
def _(x: list[str] | list[int] | list[bytes]):
# TODO: Ideally, this would be an error (requires https://github.com/astral-sh/ty/issues/116)
if isinstance(x, list[int]):
# No narrowing here:
reveal_type(x) # revealed: list[str] | list[int] | list[bytes]
# error: [invalid-argument-type] "Invalid second argument to `isinstance`"
if isinstance(x, list[int] | list[str]):
# No narrowing here:
reveal_type(x) # revealed: list[str] | list[int] | list[bytes]
```
## `type[]` types are narrowed as well as class-literal types
```py

View File

@@ -200,26 +200,6 @@ def _(x: type[int | str | bytes]):
reveal_type(x) # revealed: (type[int] & Unknown) | (type[str] & Unknown) | (type[bytes] & Unknown)
```
## `classinfo` is a `types.UnionType`
Python 3.10 added the ability to use `Union[int, str]` as the second argument to `issubclass()`:
```py
from typing import Union
IntOrStr = Union[int, str]
reveal_type(IntOrStr) # revealed: types.UnionType
def f(x: type[int | str | bytes | range]):
if issubclass(x, IntOrStr):
reveal_type(x) # revealed: type[int] | type[str]
elif issubclass(x, Union[bytes, memoryview]):
reveal_type(x) # revealed: type[bytes]
else:
reveal_type(x) # revealed: <class 'range'>
```
## Special cases
### Emit a diagnostic if the first argument is of wrong type

View File

@@ -1,147 +1,8 @@
# PEP 613 type aliases
PEP 613 type aliases are simple assignment statements, annotated with `typing.TypeAlias` to mark
them as a type alias. At runtime, they behave the same as implicit type aliases. Our support for
them is currently the same as for implicit type aliases, but we don't reproduce the full
implicit-type-alias test suite here, just some particularly interesting cases.
## No panics
## Basic
### as `TypeAlias`
```py
from typing import TypeAlias
IntOrStr: TypeAlias = int | str
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
### as `typing.TypeAlias`
```py
import typing
IntOrStr: typing.TypeAlias = int | str
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
## Can be used as value
Because PEP 613 type aliases are just annotated assignments, they can be used as values, like a
legacy type expression (and unlike a PEP 695 type alias). We might prefer this wasn't allowed, but
people do use it.
```py
from typing import TypeAlias
MyExc: TypeAlias = Exception
try:
raise MyExc("error")
except MyExc as e:
reveal_type(e) # revealed: Exception
```
## Can inherit from an alias
```py
from typing import TypeAlias
from ty_extensions import is_subtype_of, static_assert
MyList: TypeAlias = list["int"]
class Foo(MyList): ...
static_assert(is_subtype_of(Foo, list[int]))
```
## Cannot inherit from a stringified alias
```py
from typing import TypeAlias
MyList: TypeAlias = "list[int]"
# error: [invalid-base] "Invalid class base with type `str`"
class Foo(MyList): ...
```
## Unknown type in PEP 604 union
If we run into an unknown type in a PEP 604 union in the right-hand side of a PEP 613 type alias, we
still understand it as a union type, just with an unknown element.
```py
from typing import TypeAlias
from nonexistent import unknown_type # error: [unresolved-import]
MyAlias: TypeAlias = int | unknown_type | str
def _(x: MyAlias):
reveal_type(x) # revealed: int | Unknown | str
```
## Callable type in union
```py
from typing import TypeAlias, Callable
MyAlias: TypeAlias = int | Callable[[str], int]
def _(x: MyAlias):
reveal_type(x) # revealed: int | ((str, /) -> int)
```
## Subscripted generic alias in union
```py
from typing import TypeAlias, TypeVar
T = TypeVar("T")
Alias1: TypeAlias = list[T] | set[T]
MyAlias: TypeAlias = int | Alias1[str]
def _(x: MyAlias):
# TODO: int | list[str] | set[str]
reveal_type(x) # revealed: int | @Todo(Specialization of union type alias)
```
## Imported
`alias.py`:
```py
from typing import TypeAlias
MyAlias: TypeAlias = int | str
```
`main.py`:
```py
from alias import MyAlias
def _(x: MyAlias):
reveal_type(x) # revealed: int | str
```
## String literal in right-hand side
```py
from typing import TypeAlias
IntOrStr: TypeAlias = "int | str"
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
## Cyclic
We do not fully support PEP 613 type aliases yet. For now, just make sure that we don't panic:
```py
from typing import TypeAlias
@@ -157,26 +18,6 @@ def _(rec: RecursiveHomogeneousTuple):
reveal_type(rec) # revealed: tuple[Divergent, ...]
```
## Conditionally imported on Python < 3.10
```toml
[environment]
python-version = "3.9"
```
```py
try:
# error: [unresolved-import]
from typing import TypeAlias
except ImportError:
from typing_extensions import TypeAlias
MyAlias: TypeAlias = int
def _(x: MyAlias):
reveal_type(x) # revealed: int
```
## PEP-613 aliases in stubs are deferred
Although the right-hand side of a PEP-613 alias is a value expression, inference of this value is
@@ -205,31 +46,7 @@ f(stub.B())
class Unrelated: ...
# error: [invalid-argument-type]
# TODO: we should emit `[invalid-argument-type]` here
# (the alias is a `@Todo` because it's imported from another file)
f(Unrelated())
```
## Invalid position
`typing.TypeAlias` must be used as the sole annotation in an annotated assignment. Use in any other
context is an error.
```py
from typing import TypeAlias
# error: [invalid-type-form]
def _(x: TypeAlias):
reveal_type(x) # revealed: Unknown
# error: [invalid-type-form]
y: list[TypeAlias] = []
```
## Right-hand side is required
```py
from typing import TypeAlias
# error: [invalid-type-form]
Empty: TypeAlias
```

View File

@@ -28,7 +28,7 @@ def f() -> None:
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__value__) # revealed: Any
reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`)
```
## Invalid assignment

View File

@@ -24,12 +24,12 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics
```
error[invalid-key]: Unknown key "Retries" for TypedDict `Config`
error[invalid-key]: Invalid key for TypedDict `Config`
--> src/mdtest_snippet.py:7:5
|
6 | def _(config: Config) -> None:
7 | config["Retries"] = 30.0 # error: [invalid-key]
| ------ ^^^^^^^^^ Did you mean "retries"?
| ------ ^^^^^^^^^ Unknown key "Retries" - did you mean "retries"?
| |
| TypedDict `Config`
|

View File

@@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for all elements of a union
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for all elemens of a union
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
---
@@ -16,28 +16,27 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
2 |
3 | class Person(TypedDict):
4 | name: str
5 | phone_number: str
6 |
7 | class Animal(TypedDict):
8 | name: str
9 | legs: int
10 |
11 | def _(being: Person | Animal) -> None:
5 |
6 | class Animal(TypedDict):
7 | name: str
8 | legs: int
9 |
10 | def _(being: Person | Animal) -> None:
11 | # error: [invalid-key]
12 | # error: [invalid-key]
13 | # error: [invalid-key]
14 | being["surname"] = "unknown"
13 | being["surname"] = "unknown"
```
# Diagnostics
```
error[invalid-key]: Unknown key "surname" for TypedDict `Person`
--> src/mdtest_snippet.py:14:5
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:13:5
|
11 | # error: [invalid-key]
12 | # error: [invalid-key]
13 | # error: [invalid-key]
14 | being["surname"] = "unknown"
| ----- ^^^^^^^^^ Did you mean "name"?
13 | being["surname"] = "unknown"
| ----- ^^^^^^^^^ Unknown key "surname" - did you mean "name"?
| |
| TypedDict `Person` in union type `Person | Animal`
|
@@ -46,13 +45,13 @@ info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: Unknown key "surname" for TypedDict `Animal`
--> src/mdtest_snippet.py:14:5
error[invalid-key]: Invalid key for TypedDict `Animal`
--> src/mdtest_snippet.py:13:5
|
11 | # error: [invalid-key]
12 | # error: [invalid-key]
13 | # error: [invalid-key]
14 | being["surname"] = "unknown"
| ----- ^^^^^^^^^ Did you mean "name"?
13 | being["surname"] = "unknown"
| ----- ^^^^^^^^^ Unknown key "surname" - did you mean "name"?
| |
| TypedDict `Animal` in union type `Person | Animal`
|

View File

@@ -16,24 +16,23 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
2 |
3 | class Person(TypedDict):
4 | name: str
5 | phone_number: str
6 |
7 | class Animal(TypedDict):
8 | name: str
9 | legs: int
10 |
11 | def _(being: Person | Animal) -> None:
12 | being["legs"] = 4 # error: [invalid-key]
5 |
6 | class Animal(TypedDict):
7 | name: str
8 | legs: int
9 |
10 | def _(being: Person | Animal) -> None:
11 | being["legs"] = 4 # error: [invalid-key]
```
# Diagnostics
```
error[invalid-key]: Unknown key "legs" for TypedDict `Person`
--> src/mdtest_snippet.py:12:5
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:11:5
|
11 | def _(being: Person | Animal) -> None:
12 | being["legs"] = 4 # error: [invalid-key]
10 | def _(being: Person | Animal) -> None:
11 | being["legs"] = 4 # error: [invalid-key]
| ----- ^^^^^^ Unknown key "legs"
| |
| TypedDict `Person` in union type `Person | Animal`

View File

@@ -1,47 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: builtins.md - Calling builtins - The builtin `NotImplemented` constant is not callable
mdtest path: crates/ty_python_semantic/resources/mdtest/call/builtins.md
---
# Python source files
## mdtest_snippet.py
```
1 | raise NotImplemented() # error: [call-non-callable]
2 | raise NotImplemented("this module is not implemented yet!!!") # error: [call-non-callable]
```
# Diagnostics
```
error[call-non-callable]: `NotImplemented` is not callable
--> src/mdtest_snippet.py:1:7
|
1 | raise NotImplemented() # error: [call-non-callable]
| --------------^^
| |
| Did you mean `NotImplementedError`?
2 | raise NotImplemented("this module is not implemented yet!!!") # error: [call-non-callable]
|
info: rule `call-non-callable` is enabled by default
```
```
error[call-non-callable]: `NotImplemented` is not callable
--> src/mdtest_snippet.py:2:7
|
1 | raise NotImplemented() # error: [call-non-callable]
2 | raise NotImplemented("this module is not implemented yet!!!") # error: [call-non-callable]
| --------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| Did you mean `NotImplementedError`?
|
info: rule `call-non-callable` is enabled by default
```

View File

@@ -1,31 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Annotated assignment
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | x: int = "three" # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
--> src/mdtest_snippet.py:1:4
|
1 | x: int = "three" # error: [invalid-assignment]
| --- ^^^^^^^ Incompatible value of type `Literal["three"]`
| |
| Declared type
|
info: rule `invalid-assignment` is enabled by default
```

View File

@@ -1,44 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Multiline expressions
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | # fmt: off
2 |
3 | # error: [invalid-assignment]
4 | x: str = (
5 | 1 + 2 + (
6 | 3 + 4 + 5
7 | )
8 | )
```
# Diagnostics
```
error[invalid-assignment]: Object of type `Literal[15]` is not assignable to `str`
--> src/mdtest_snippet.py:4:4
|
3 | # error: [invalid-assignment]
4 | x: str = (
| ____---___^
| | |
| | Declared type
5 | | 1 + 2 + (
6 | | 3 + 4 + 5
7 | | )
8 | | )
| |_^ Incompatible value of type `Literal[15]`
|
info: rule `invalid-assignment` is enabled by default
```

View File

@@ -1,55 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Multiple targets
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | x: int
2 | y: str
3 |
4 | x, y = ("a", "b") # error: [invalid-assignment]
5 |
6 | x, y = (0, 0) # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Object of type `tuple[Literal["a"], Literal["b"]]` is not assignable to `int`
--> src/mdtest_snippet.py:4:1
|
2 | y: str
3 |
4 | x, y = ("a", "b") # error: [invalid-assignment]
| - ^^^^^^^^^^ Incompatible value of type `tuple[Literal["a"], Literal["b"]]`
| |
| Declared type `int`
5 |
6 | x, y = (0, 0) # error: [invalid-assignment]
|
info: rule `invalid-assignment` is enabled by default
```
```
error[invalid-assignment]: Object of type `tuple[Literal[0], Literal[0]]` is not assignable to `str`
--> src/mdtest_snippet.py:6:4
|
4 | x, y = ("a", "b") # error: [invalid-assignment]
5 |
6 | x, y = (0, 0) # error: [invalid-assignment]
| - ^^^^^^ Incompatible value of type `tuple[Literal[0], Literal[0]]`
| |
| Declared type `str`
|
info: rule `invalid-assignment` is enabled by default
```

View File

@@ -1,35 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Named expression
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | x: int
2 |
3 | (x := "three") # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
--> src/mdtest_snippet.py:3:2
|
1 | x: int
2 |
3 | (x := "three") # error: [invalid-assignment]
| - ^^^^^^^ Incompatible value of type `Literal["three"]`
| |
| Declared type `int`
|
info: rule `invalid-assignment` is enabled by default
```

View File

@@ -1,33 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Unannotated assignment
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
---
# Python source files
## mdtest_snippet.py
```
1 | x: int
2 | x = "three" # error: [invalid-assignment]
```
# Diagnostics
```
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
--> src/mdtest_snippet.py:2:1
|
1 | x: int
2 | x = "three" # error: [invalid-assignment]
| - ^^^^^^^ Incompatible value of type `Literal["three"]`
| |
| Declared type `int`
|
info: rule `invalid-assignment` is enabled by default
```

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