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
116 changed files with 1634 additions and 5478 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@e5c5f5b2d762af91b28490537fe0077334165693"
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@e5c5f5b2d762af91b28490537fe0077334165693"
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

@@ -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

@@ -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

@@ -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

@@ -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}])
```

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

@@ -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

@@ -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

@@ -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

@@ -145,7 +145,7 @@ def unbounded_unconstrained[T, U](t: T, u: U) -> None:
reveal_type(is_assignable_to(T, object))
static_assert(is_assignable_to(T, object))
# revealed: ty_extensions.ConstraintSet[(T@unbounded_unconstrained ≤ Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Super))
@@ -165,15 +165,15 @@ def unbounded_unconstrained[T, U](t: T, u: U) -> None:
reveal_type(is_assignable_to(U, object))
static_assert(is_assignable_to(U, object))
# revealed: ty_extensions.ConstraintSet[(U@unbounded_unconstrained ≤ Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, Super))
static_assert(not is_assignable_to(U, Super))
# revealed: ty_extensions.ConstraintSet[(T@unbounded_unconstrained ≤ U@unbounded_unconstrained)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, U))
static_assert(not is_assignable_to(T, U))
# revealed: ty_extensions.ConstraintSet[(U@unbounded_unconstrained ≤ T@unbounded_unconstrained)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, T))
static_assert(not is_assignable_to(U, T))
@@ -185,15 +185,15 @@ def unbounded_unconstrained[T, U](t: T, u: U) -> None:
reveal_type(is_subtype_of(T, object))
static_assert(is_subtype_of(T, object))
# revealed: ty_extensions.ConstraintSet[(T@unbounded_unconstrained ≤ Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[(T@unbounded_unconstrained = Never)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[(T@unbounded_unconstrained = object)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
@@ -205,15 +205,15 @@ def unbounded_unconstrained[T, U](t: T, u: U) -> None:
reveal_type(is_subtype_of(U, object))
static_assert(is_subtype_of(U, object))
# revealed: ty_extensions.ConstraintSet[(U@unbounded_unconstrained ≤ Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, Super))
static_assert(not is_subtype_of(U, Super))
# revealed: ty_extensions.ConstraintSet[(T@unbounded_unconstrained ≤ U@unbounded_unconstrained)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, U))
static_assert(not is_subtype_of(T, U))
# revealed: ty_extensions.ConstraintSet[(U@unbounded_unconstrained ≤ T@unbounded_unconstrained)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, T))
static_assert(not is_subtype_of(U, T))
```
@@ -229,31 +229,31 @@ from typing import Any
from typing_extensions import final
def bounded[T: Super](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[(T@bounded ≤ Super)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[(T@bounded ≤ Super)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded ≤ Super)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[(T@bounded ≤ Sub)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Sub))
# revealed: ty_extensions.ConstraintSet[(T@bounded = Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super, T))
static_assert(not is_assignable_to(Super, T))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@bounded ≤ Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Sub, T))
static_assert(not is_assignable_to(Sub, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded = Never)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
@@ -261,19 +261,19 @@ def bounded[T: Super](t: T) -> None:
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded ≤ Super)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, Super))
static_assert(is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[(T@bounded ≤ Sub)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[(T@bounded = Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@bounded ≤ Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Sub, T))
static_assert(not is_subtype_of(Sub, T))
@@ -286,67 +286,23 @@ def bounded_by_gradual[T: Any](t: T) -> None:
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded_by_gradual ≤ Super)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[(Super ≤ T@bounded_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super, T))
static_assert(not is_assignable_to(Super, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded_by_gradual ≤ Sub)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Sub))
static_assert(is_assignable_to(T, Sub))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@bounded_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Sub, T))
static_assert(not is_assignable_to(Sub, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded_by_gradual = Never)]
reveal_type(is_subtype_of(T, Any))
static_assert(is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[(T@bounded_by_gradual = object)]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded_by_gradual ≤ Super)]
reveal_type(is_subtype_of(T, Super))
static_assert(is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[(Super ≤ T@bounded_by_gradual)]
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded_by_gradual ≤ Sub)]
reveal_type(is_subtype_of(T, Sub))
static_assert(is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@bounded_by_gradual)]
reveal_type(is_subtype_of(Sub, T))
static_assert(not is_subtype_of(Sub, T))
@final
class FinalClass: ...
def bounded_final[T: FinalClass](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[(T@bounded_final ≤ FinalClass)]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[(T@bounded_final ≤ FinalClass)]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded_final ≤ FinalClass)]
reveal_type(is_assignable_to(T, FinalClass))
static_assert(is_assignable_to(T, FinalClass))
# revealed: ty_extensions.ConstraintSet[(T@bounded_final = FinalClass)]
reveal_type(is_assignable_to(FinalClass, T))
static_assert(not is_assignable_to(FinalClass, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded_final = Never)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
@@ -354,11 +310,55 @@ def bounded_final[T: FinalClass](t: T) -> None:
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@bounded_final ≤ FinalClass)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Sub, T))
static_assert(not is_subtype_of(Sub, T))
@final
class FinalClass: ...
def bounded_final[T: FinalClass](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, FinalClass))
static_assert(is_assignable_to(T, FinalClass))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(FinalClass, T))
static_assert(not is_assignable_to(FinalClass, T))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, FinalClass))
static_assert(is_subtype_of(T, FinalClass))
# revealed: ty_extensions.ConstraintSet[(T@bounded_final = FinalClass)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(FinalClass, T))
static_assert(not is_subtype_of(FinalClass, T))
```
@@ -370,36 +370,36 @@ typevars to `Never` in addition to that final class.
```py
def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[((T@two_bounded ≤ U@two_bounded) ∧ (U@two_bounded ≤ Super))]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, U))
static_assert(not is_assignable_to(T, U))
# revealed: ty_extensions.ConstraintSet[((U@two_bounded ≤ Super) ∧ (U@two_bounded ≤ T@two_bounded ≤ Super))]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, T))
static_assert(not is_assignable_to(U, T))
# revealed: ty_extensions.ConstraintSet[((T@two_bounded ≤ U@two_bounded) ∧ (U@two_bounded ≤ Super))]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, U))
static_assert(not is_subtype_of(T, U))
# revealed: ty_extensions.ConstraintSet[((U@two_bounded ≤ Super) ∧ (U@two_bounded ≤ T@two_bounded ≤ Super))]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, T))
static_assert(not is_subtype_of(U, T))
def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[((T@two_final_bounded ≤ U@two_final_bounded) ∧ (U@two_final_bounded ≤ FinalClass))]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, U))
static_assert(not is_assignable_to(T, U))
# revealed: ty_extensions.ConstraintSet[((U@two_final_bounded ≤ FinalClass) ∧ (U@two_final_bounded ≤ T@two_final_bounded ≤ FinalClass))]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(U, T))
static_assert(not is_assignable_to(U, T))
# revealed: ty_extensions.ConstraintSet[((T@two_final_bounded ≤ U@two_final_bounded) ∧ (U@two_final_bounded ≤ FinalClass))]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, U))
static_assert(not is_subtype_of(T, U))
# revealed: ty_extensions.ConstraintSet[((U@two_final_bounded ≤ FinalClass) ∧ (U@two_final_bounded ≤ T@two_final_bounded ≤ FinalClass))]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(U, T))
static_assert(not is_subtype_of(U, T))
```
@@ -412,11 +412,11 @@ intersection of all of its constraints is a subtype of the typevar.
from ty_extensions import Intersection
def constrained[T: (Base, Unrelated)](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Super))
static_assert(not is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Base))
static_assert(not is_assignable_to(T, Base))
@@ -424,27 +424,27 @@ def constrained[T: (Base, Unrelated)](t: T) -> None:
reveal_type(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Sub))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Unrelated))
static_assert(not is_assignable_to(T, Unrelated))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base) (T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
static_assert(is_assignable_to(T, Any))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base) (T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super | Unrelated))
static_assert(is_assignable_to(T, Super | Unrelated))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base) (T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Base | Unrelated))
static_assert(is_assignable_to(T, Base | Unrelated))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Sub | Unrelated))
static_assert(not is_assignable_to(T, Sub | Unrelated))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base) (T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
@@ -452,7 +452,7 @@ def constrained[T: (Base, Unrelated)](t: T) -> None:
reveal_type(is_assignable_to(Super, T))
static_assert(not is_assignable_to(Super, T))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Unrelated, T))
static_assert(not is_assignable_to(Unrelated, T))
@@ -460,15 +460,15 @@ def constrained[T: (Base, Unrelated)](t: T) -> None:
reveal_type(is_assignable_to(Super | Unrelated, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base) (T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Base))
@@ -476,7 +476,7 @@ def constrained[T: (Base, Unrelated)](t: T) -> None:
reveal_type(is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Unrelated))
static_assert(not is_subtype_of(T, Unrelated))
@@ -484,15 +484,15 @@ def constrained[T: (Base, Unrelated)](t: T) -> None:
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base) (T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, Super | Unrelated))
static_assert(is_subtype_of(T, Super | Unrelated))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base) (T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, Base | Unrelated))
static_assert(is_subtype_of(T, Base | Unrelated))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub | Unrelated))
static_assert(not is_subtype_of(T, Sub | Unrelated))
@@ -504,7 +504,7 @@ def constrained[T: (Base, Unrelated)](t: T) -> None:
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Unrelated, T))
@@ -512,24 +512,24 @@ def constrained[T: (Base, Unrelated)](t: T) -> None:
reveal_type(is_subtype_of(Super | Unrelated, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
# revealed: ty_extensions.ConstraintSet[(T@constrained = Base) (T@constrained = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(Intersection[Base, Unrelated], T))
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Super)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super))
static_assert(is_assignable_to(T, Super))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Base)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Base))
static_assert(is_assignable_to(T, Base))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Sub)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Sub))
static_assert(not is_assignable_to(T, Sub))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Unrelated))
static_assert(not is_assignable_to(T, Unrelated))
@@ -541,19 +541,19 @@ def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
reveal_type(is_assignable_to(T, Super | Any))
static_assert(is_assignable_to(T, Super | Any))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Super | Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Super | Unrelated))
static_assert(is_assignable_to(T, Super | Unrelated))
# revealed: ty_extensions.ConstraintSet[(Super ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super, T))
static_assert(not is_assignable_to(Super, T))
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Base, T))
static_assert(is_assignable_to(Base, T))
# revealed: ty_extensions.ConstraintSet[(Unrelated ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Unrelated, T))
static_assert(not is_assignable_to(Unrelated, T))
@@ -561,19 +561,19 @@ def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
reveal_type(is_assignable_to(Any, T))
static_assert(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[(Super ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super | Any, T))
static_assert(not is_assignable_to(Super | Any, T))
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Base | Any, T))
static_assert(is_assignable_to(Base | Any, T))
# revealed: ty_extensions.ConstraintSet[(Super | Unrelated ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Super | Unrelated, T))
static_assert(not is_assignable_to(Super | Unrelated, T))
# revealed: ty_extensions.ConstraintSet[(Base & Unrelated ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[Base, Unrelated], T))
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
@@ -581,69 +581,69 @@ def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
reveal_type(is_assignable_to(Intersection[Base, Any], T))
static_assert(is_assignable_to(Intersection[Base, Any], T))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super))
static_assert(is_subtype_of(T, Super))
static_assert(not is_subtype_of(T, Super))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Base)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Base))
static_assert(is_subtype_of(T, Base))
static_assert(not is_subtype_of(T, Base))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Sub)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Sub))
static_assert(not is_subtype_of(T, Sub))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Unrelated))
static_assert(not is_subtype_of(T, Unrelated))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual = Never)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
static_assert(not is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Super)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super | Any))
static_assert(is_subtype_of(T, Super | Any))
static_assert(not is_subtype_of(T, Super | Any))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual ≤ Super | Unrelated)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Super | Unrelated))
static_assert(is_subtype_of(T, Super | Unrelated))
static_assert(not is_subtype_of(T, Super | Unrelated))
# revealed: ty_extensions.ConstraintSet[(Super ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super, T))
static_assert(not is_subtype_of(Super, T))
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Base, T))
static_assert(is_subtype_of(Base, T))
static_assert(not is_subtype_of(Base, T))
# revealed: ty_extensions.ConstraintSet[(Unrelated ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Unrelated, T))
static_assert(not is_subtype_of(Unrelated, T))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual = object)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
static_assert(not is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual = object)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super | Any, T))
static_assert(not is_subtype_of(Super | Any, T))
# revealed: ty_extensions.ConstraintSet[(T@constrained_by_gradual = object)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Base | Any, T))
static_assert(not is_subtype_of(Base | Any, T))
# revealed: ty_extensions.ConstraintSet[(Super | Unrelated ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Super | Unrelated, T))
static_assert(not is_subtype_of(Super | Unrelated, T))
# revealed: ty_extensions.ConstraintSet[(Base & Unrelated ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Intersection[Base, Unrelated], T))
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@constrained_by_gradual)]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Intersection[Base, Any], T))
static_assert(is_subtype_of(Intersection[Base, Any], T))
static_assert(not is_subtype_of(Intersection[Base, Any], T))
```
Two distinct fully static typevars are not subtypes of each other, even if they have the same
@@ -694,19 +694,19 @@ A bound or constrained typevar is a subtype of itself in a union:
```py
def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[(T@union ≤ Base)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, T | None))
static_assert(is_assignable_to(T, T | None))
# revealed: ty_extensions.ConstraintSet[(U@union = Base) (U@union = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(U, U | None))
static_assert(is_assignable_to(U, U | None))
# revealed: ty_extensions.ConstraintSet[(T@union ≤ Base)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, T | None))
static_assert(is_subtype_of(T, T | None))
# revealed: ty_extensions.ConstraintSet[(U@union = Base) (U@union = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(U, U | None))
static_assert(is_subtype_of(U, U | None))
```
@@ -715,11 +715,11 @@ A bound or constrained typevar in a union with a dynamic type is assignable to t
```py
def union_with_dynamic[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[(T@union_with_dynamic ≤ Base)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T | Any, T))
static_assert(is_assignable_to(T | Any, T))
# revealed: ty_extensions.ConstraintSet[(U@union_with_dynamic = Base) (U@union_with_dynamic = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(U | Any, U))
static_assert(is_assignable_to(U | Any, U))
@@ -740,19 +740,19 @@ from ty_extensions import Intersection, Not, is_disjoint_from
class A: ...
def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
# revealed: ty_extensions.ConstraintSet[(T@inter ≤ Base)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[T, Unrelated], T))
static_assert(is_assignable_to(Intersection[T, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[(T@inter ≤ Base)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(Intersection[T, Unrelated], T))
static_assert(is_subtype_of(Intersection[T, Unrelated], T))
# revealed: ty_extensions.ConstraintSet[(U@inter = Base) (U@inter = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Intersection[U, A], U))
static_assert(is_assignable_to(Intersection[U, A], U))
# revealed: ty_extensions.ConstraintSet[(U@inter = Base) (U@inter = Unrelated)]
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(Intersection[U, A], U))
static_assert(is_subtype_of(Intersection[U, A], U))

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: T@_ | int
# TODO should be int | Unknown
reveal_type(int_or_type_var) # revealed: int | T@_
# TODO should be Unknown | None
reveal_type(type_var_or_none) # revealed: T@_ | None
# TODO should be None | Unknown
reveal_type(none_or_type_var) # revealed: None | T@_
```
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -368,191 +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"]
# TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below)
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'>
reveal_type(MyDict) # revealed: <class 'dict[typing.TypeVar, typing.TypeVar]'>
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[typing.TypeVar]
# TODO: Should be `dict[Unknown, Unknown]`
reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar]
# 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[typing.TypeVar]'>` 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
@@ -1140,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):
@@ -1223,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"]
@@ -1231,7 +982,6 @@ StrOrStyle = Union[str, "Style"]
SubclassOfStyle = type["Style"]
DictStrToStyle = Dict[str, "Style"]
AnnotatedStyle = Annotated["Style", "metadata"]
CallableStyleToStyle = Callable[["Style"], "Style"]
class Style: ...
@@ -1242,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]
@@ -1250,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

@@ -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

@@ -30,13 +30,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics
```
error[invalid-key]: Unknown key "surname" for TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:13:5
|
11 | # error: [invalid-key]
12 | # error: [invalid-key]
13 | being["surname"] = "unknown"
| ----- ^^^^^^^^^ Did you mean "name"?
| ----- ^^^^^^^^^ Unknown key "surname" - did you mean "name"?
| |
| TypedDict `Person` in union type `Person | Animal`
|
@@ -45,13 +45,13 @@ info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: Unknown key "surname" for TypedDict `Animal`
error[invalid-key]: Invalid key for TypedDict `Animal`
--> src/mdtest_snippet.py:13:5
|
11 | # error: [invalid-key]
12 | # error: [invalid-key]
13 | being["surname"] = "unknown"
| ----- ^^^^^^^^^ Did you mean "name"?
| ----- ^^^^^^^^^ Unknown key "surname" - did you mean "name"?
| |
| TypedDict `Animal` in union type `Person | Animal`
|

View File

@@ -28,7 +28,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
# Diagnostics
```
error[invalid-key]: Unknown key "legs" for TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:11:5
|
10 | def _(being: Person | Animal) -> None:

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
```

View File

@@ -26,9 +26,7 @@ error[invalid-assignment]: Implicit shadowing of class `C`
1 | class C: ...
2 |
3 | C = 1 # error: [invalid-assignment]
| - ^ Incompatible value of type `Literal[1]`
| |
| Declared type `<class 'C'>`
| ^
|
info: Annotate to make it explicit if this is intentional
info: rule `invalid-assignment` is enabled by default

View File

@@ -26,9 +26,7 @@ error[invalid-assignment]: Implicit shadowing of function `f`
1 | def f(): ...
2 |
3 | f = 1 # error: [invalid-assignment]
| - ^ Incompatible value of type `Literal[1]`
| |
| Declared type `def f() -> Unknown`
| ^
|
info: Annotate to make it explicit if this is intentional
info: rule `invalid-assignment` is enabled by default

View File

@@ -57,12 +57,12 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
# Diagnostics
```
error[invalid-key]: Unknown key "naem" for TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:8:5
|
7 | def access_invalid_literal_string_key(person: Person):
8 | person["naem"] # error: [invalid-key]
| ------ ^^^^^^ Did you mean "name"?
| ------ ^^^^^^ Unknown key "naem" - did you mean "name"?
| |
| TypedDict `Person`
9 |
@@ -73,7 +73,7 @@ info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: Unknown key "naem" for TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:13:5
|
12 | def access_invalid_key(person: Person):
@@ -130,12 +130,12 @@ info: rule `invalid-assignment` is enabled by default
```
```
error[invalid-key]: Unknown key "naem" for TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:22:5
|
21 | def write_to_non_existing_key(person: Person):
22 | person["naem"] = "Alice" # error: [invalid-key]
| ------ ^^^^^^ Did you mean "name"?
| ------ ^^^^^^ Unknown key "naem" - did you mean "name"?
| |
| TypedDict `Person`
23 |
@@ -160,7 +160,7 @@ info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: Unknown key "unknown" for TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:29:21
|
27 | def create_with_invalid_string_key():
@@ -178,7 +178,7 @@ info: rule `invalid-key` is enabled by default
```
```
error[invalid-key]: Unknown key "unknown" for TypedDict `Person`
error[invalid-key]: Invalid key for TypedDict `Person`
--> src/mdtest_snippet.py:32:11
|
31 | # error: [invalid-key]

View File

@@ -59,10 +59,10 @@ In a non-stub file, there's no special treatment of ellipsis literals. An ellips
be assigned if `EllipsisType` is actually assignable to the annotated type.
```py
# error: [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
# error: 7 [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
def f(x: int = ...) -> None: ...
# error: [invalid-assignment] "Object of type `EllipsisType` is not assignable to `int`"
# error: 1 [invalid-assignment] "Object of type `EllipsisType` is not assignable to `int`"
a: int = ...
b = ...
reveal_type(b) # revealed: EllipsisType
@@ -73,6 +73,6 @@ reveal_type(b) # revealed: EllipsisType
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
```pyi
# error: [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
# error: 7 [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
def f(x: int = Ellipsis) -> None: ...
```

View File

@@ -189,40 +189,3 @@ a = 10 + 4 # ty: ignore[division-by-zer]
# error: [division-by-zero]
a = 10 / 0 # ty: ignore[lint:division-by-zero]
```
## Suppression of specific diagnostics
In this section, we make sure that specific diagnostics can be suppressed in various forms that
users might expect to work.
### Invalid assignment
An invalid assignment can be suppressed in the following ways:
```py
# fmt: off
x1: str = 1 + 2 + 3 # ty: ignore
x2: str = ( # ty: ignore
1 + 2 + 3
)
x4: str = (
1 + 2 + 3
) # ty: ignore
```
It can *not* be suppressed by putting the `# ty: ignore` on the inner expression. The range targeted
by the suppression comment needs to overlap with one of the boundaries of the value range (the outer
parentheses in this case):
```py
# fmt: off
# error: [invalid-assignment]
x4: str = (
# error: [unused-ignore-comment]
1 + 2 + 3 # ty: ignore
)
```

View File

@@ -45,25 +45,29 @@ def even_given_unsatisfiable_constraints():
## Type variables
The interesting case is typevars. The other typing relationships all "punt" on the question when
considering a typevar, by translating the desired relationship into a constraint set.
The interesting case is typevars. The other typing relationships (TODO: will) all "punt" on the
question when considering a typevar, by translating the desired relationship into a constraint set.
```py
from typing import Any
from ty_extensions import is_assignable_to, is_subtype_of
def assignability[T]():
# revealed: ty_extensions.ConstraintSet[(T@assignability ≤ bool)]
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ bool]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, bool))
# revealed: ty_extensions.ConstraintSet[(T@assignability ≤ int)]
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ int]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, int))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, object))
def subtyping[T]():
# revealed: ty_extensions.ConstraintSet[(T@subtyping ≤ bool)]
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ bool]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, bool))
# revealed: ty_extensions.ConstraintSet[(T@subtyping ≤ int)]
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ int]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, int))
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_subtype_of(T, object))
@@ -84,37 +88,49 @@ class Contravariant[T]:
pass
def assignability[T]():
# aka [T@assignability ≤ object], which is always satisfiable
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(T, Any))
# aka [Never ≤ T@assignability], which is always satisfiable
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(is_assignable_to(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@assignability ≤ Covariant[object])]
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Covariant[object]]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Covariant[Any]))
# revealed: ty_extensions.ConstraintSet[(Covariant[Never] ≤ T@assignability)]
# TODO: revealed: ty_extensions.ConstraintSet[Covariant[Never] ≤ T@assignability]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Covariant[Any], T))
# revealed: ty_extensions.ConstraintSet[(T@assignability ≤ Contravariant[Never])]
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Contravariant[Never]]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(T, Contravariant[Any]))
# revealed: ty_extensions.ConstraintSet[(Contravariant[object] ≤ T@assignability)]
# TODO: revealed: ty_extensions.ConstraintSet[Contravariant[object] ≤ T@assignability]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_assignable_to(Contravariant[Any], T))
def subtyping[T]():
# revealed: ty_extensions.ConstraintSet[(T@subtyping = Never)]
# aka [T@assignability ≤ object], which is always satisfiable
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Any))
# revealed: ty_extensions.ConstraintSet[(T@subtyping = object)]
# aka [Never ≤ T@assignability], which is always satisfiable
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Any, T))
# revealed: ty_extensions.ConstraintSet[(T@subtyping ≤ Covariant[Never])]
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Covariant[Never]]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Covariant[Any]))
# revealed: ty_extensions.ConstraintSet[(Covariant[object] ≤ T@subtyping)]
# TODO: revealed: ty_extensions.ConstraintSet[Covariant[object] ≤ T@subtyping]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Covariant[Any], T))
# revealed: ty_extensions.ConstraintSet[(T@subtyping ≤ Contravariant[object])]
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Contravariant[object]]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(T, Contravariant[Any]))
# revealed: ty_extensions.ConstraintSet[(Contravariant[Never] ≤ T@subtyping)]
# TODO: revealed: ty_extensions.ConstraintSet[Contravariant[Never] ≤ T@subtyping]
# revealed: ty_extensions.ConstraintSet[never]
reveal_type(is_subtype_of(Contravariant[Any], T))
```
@@ -157,10 +173,7 @@ def given_constraints[T]():
static_assert(given_str.implies_subtype_of(T, str))
```
This might require propagating constraints from other typevars. (Note that we perform the test
twice, with different variable orderings. Our BDD implementation uses the Salsa IDs of each typevar
as part of the variable ordering. Reversing the typevar order helps us verify that we don't have any
BDD logic that is dependent on which variable ordering we end up with.)
This might require propagating constraints from other typevars.
```py
def mutually_constrained[T, U]():
@@ -170,19 +183,6 @@ def mutually_constrained[T, U]():
static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not given_int.implies_subtype_of(T, str))
# If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well.
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(T, int))
static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not given_int.implies_subtype_of(T, str))
def mutually_constrained[U, T]():
# If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well.
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(T, int))
static_assert(not given_int.implies_subtype_of(T, bool))
static_assert(not given_int.implies_subtype_of(T, str))
# If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well.
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(T, int))
@@ -236,22 +236,6 @@ def mutually_constrained[T, U]():
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str]))
# If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Covariant[T] ≤ Covariant[int]).
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str]))
# Repeat the test with a different typevar ordering
def mutually_constrained[U, T]():
# If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Covariant[T] ≤ Covariant[int]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool]))
static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str]))
# If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Covariant[T] ≤ Covariant[int]).
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
@@ -297,22 +281,6 @@ def mutually_constrained[T, U]():
static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T]))
# If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Contravariant[int] ≤ Contravariant[T]).
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T]))
# Repeat the test with a different typevar ordering
def mutually_constrained[U, T]():
# If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Contravariant[int] ≤ Contravariant[T]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T]))
static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T]))
# If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore
# (Contravariant[int] ≤ Contravariant[T]).
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
@@ -370,25 +338,6 @@ def mutually_constrained[T, U]():
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str]))
# If (T = U ∧ U = int), then (T = int) must be true as well. That is an equality constraint, so
# even though T is invariant, it does imply that (Invariant[T] ≤ Invariant[int]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(int, U, int)
static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int]))
static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(not given_int.implies_subtype_of(Invariant[bool], Invariant[T]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str]))
static_assert(not given_int.implies_subtype_of(Invariant[str], Invariant[T]))
# Repeat the test with a different typevar ordering
def mutually_constrained[U, T]():
# If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well. But because T is invariant, that
# does _not_ imply that (Invariant[T] ≤ Invariant[int]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool]))
static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str]))
# If (T = U ∧ U = int), then (T = int) must be true as well. That is an equality constraint, so
# even though T is invariant, it does imply that (Invariant[T] ≤ Invariant[int]).
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(int, U, int)

View File

@@ -1248,10 +1248,14 @@ def identity[T](t: T) -> T:
static_assert(is_assignable_to(TypeOf[identity], Callable[[int], int]))
static_assert(is_assignable_to(TypeOf[identity], Callable[[str], str]))
# TODO: no error
# error: [static-assert-error]
static_assert(not is_assignable_to(TypeOf[identity], Callable[[str], int]))
static_assert(is_assignable_to(CallableTypeOf[identity], Callable[[int], int]))
static_assert(is_assignable_to(CallableTypeOf[identity], Callable[[str], str]))
# TODO: no error
# error: [static-assert-error]
static_assert(not is_assignable_to(CallableTypeOf[identity], Callable[[str], int]))
```

View File

@@ -2221,11 +2221,23 @@ from ty_extensions import CallableTypeOf, TypeOf, is_subtype_of, static_assert
def identity[T](t: T) -> T:
return t
# TODO: Confusingly, these are not the same results as the corresponding checks in
# is_assignable_to.md, even though all of these types are fully static. We have some heuristics that
# currently conflict with each other, that we are in the process of removing with the constraint set
# work.
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(TypeOf[identity], Callable[[int], int]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(TypeOf[identity], Callable[[str], str]))
static_assert(not is_subtype_of(TypeOf[identity], Callable[[str], int]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(CallableTypeOf[identity], Callable[[int], int]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(CallableTypeOf[identity], Callable[[str], str]))
static_assert(not is_subtype_of(CallableTypeOf[identity], Callable[[str], int]))
```

View File

@@ -29,7 +29,7 @@ alice: Person = {"name": "Alice", "age": 30}
reveal_type(alice["name"]) # revealed: str
reveal_type(alice["age"]) # revealed: int | None
# error: [invalid-key] "Unknown key "non_existing" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(alice["non_existing"]) # revealed: Unknown
```
@@ -41,7 +41,7 @@ bob = Person(name="Bob", age=25)
reveal_type(bob["name"]) # revealed: str
reveal_type(bob["age"]) # revealed: int | None
# error: [invalid-key] " key "non_existing" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(bob["non_existing"]) # revealed: Unknown
```
@@ -81,7 +81,7 @@ def _():
CAPITALIZED_NAME = "Name"
# error: [invalid-key] "Unknown key "Name" for TypedDict `Person` - did you mean "name"?"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "Name" - did you mean "name"?"
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
dave: Person = {CAPITALIZED_NAME: "Dave", "age": 20}
@@ -112,10 +112,10 @@ eve2b = Person(age=22)
reveal_type(eve2a) # revealed: Person
reveal_type(eve2b) # revealed: Person
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
eve3b = Person(name="Eve", age=25, extra=True)
reveal_type(eve3a) # revealed: Person
@@ -169,10 +169,10 @@ bob["name"] = None
Assignments to non-existing keys are disallowed:
```py
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
alice["extra"] = True
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
bob["extra"] = True
```
@@ -197,10 +197,10 @@ alice: Person = {"inner": {"name": "Alice", "age": 30}}
reveal_type(alice["inner"]["name"]) # revealed: str
reveal_type(alice["inner"]["age"]) # revealed: int | None
# error: [invalid-key] "Unknown key "non_existing" for TypedDict `Inner`"
# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "non_existing""
reveal_type(alice["inner"]["non_existing"]) # revealed: Unknown
# error: [invalid-key] "Unknown key "extra" for TypedDict `Inner`"
# error: [invalid-key] "Invalid key for TypedDict `Inner`: Unknown key "extra""
alice: Person = {"inner": {"name": "Alice", "age": 30, "extra": 1}}
```
@@ -289,16 +289,16 @@ a_person = {"name": None, "age": 30}
All of these have an extra field that is not defined in the `TypedDict`:
```py
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
alice4: Person = {"name": "Alice", "age": 30, "extra": True}
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
Person(name="Alice", age=30, extra=True)
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
Person({"name": "Alice", "age": 30, "extra": True})
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
# error: [invalid-argument-type]
accepts_person({"name": "Alice", "age": 30, "extra": True})
@@ -307,10 +307,10 @@ accepts_person({"name": "Alice", "age": 30, "extra": True})
house.owner = {"name": "Alice", "age": 30, "extra": True}
a_person: Person
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
a_person = {"name": "Alice", "age": 30, "extra": True}
# error: [invalid-key] "Unknown key "extra" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
(a_person := {"name": "Alice", "age": 30, "extra": True})
```
@@ -351,7 +351,7 @@ user2 = User({"name": "Bob"})
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `User`: value of type `None`"
user3 = User({"name": None, "age": 25})
# error: [invalid-key] "Unknown key "extra" for TypedDict `User`"
# error: [invalid-key] "Invalid key for TypedDict `User`: Unknown key "extra""
user4 = User({"name": "Charlie", "age": 30, "extra": True})
```
@@ -388,7 +388,7 @@ invalid = OptionalPerson(name=123)
Extra fields are still not allowed, even with `total=False`:
```py
# error: [invalid-key] "Unknown key "extra" for TypedDict `OptionalPerson`"
# error: [invalid-key] "Invalid key for TypedDict `OptionalPerson`: Unknown key "extra""
invalid_extra = OptionalPerson(name="George", extra=True)
```
@@ -550,7 +550,7 @@ def _(
reveal_type(person[union_of_keys]) # revealed: int | None | str
# error: [invalid-key] "Unknown key "non_existing" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "non_existing""
reveal_type(person["non_existing"]) # revealed: Unknown
# error: [invalid-key] "TypedDict `Person` can only be subscripted with a string literal key, got key of type `str`"
@@ -563,7 +563,7 @@ def _(
# TODO: A type of `int | None | Unknown` might be better here. The `str` is mixed in
# because `Animal.__getitem__` can only return `str`.
# error: [invalid-key] "Unknown key "age" for TypedDict `Animal`"
# error: [invalid-key] "Invalid key for TypedDict `Animal`"
reveal_type(being["age"]) # revealed: int | None | str
```
@@ -589,7 +589,7 @@ def _(person: Person):
person["name"] = "Alice"
person["age"] = 30
# error: [invalid-key] "Unknown key "naem" for TypedDict `Person` - did you mean "name"?"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "naem" - did you mean "name"?"
person["naem"] = "Alice"
def _(person: Person):
@@ -613,7 +613,7 @@ def _(being: Person | Animal):
# error: [invalid-assignment] "Invalid assignment to key "name" with declared type `str` on TypedDict `Animal`: value of type `Literal[1]`"
being["name"] = 1
# error: [invalid-key] "Unknown key "surname" for TypedDict `Animal` - did you mean "name"?"
# error: [invalid-key] "Invalid key for TypedDict `Animal`: Unknown key "surname" - did you mean "name"?"
being["surname"] = "unknown"
def _(centaur: Intersection[Person, Animal]):
@@ -621,7 +621,7 @@ def _(centaur: Intersection[Person, Animal]):
centaur["age"] = 100
centaur["legs"] = 4
# error: [invalid-key] "Unknown key "unknown" for TypedDict `Person`"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "unknown""
centaur["unknown"] = "value"
def _(person: Person, union_of_keys: Literal["name", "age"], unknown_value: Any):
@@ -724,7 +724,7 @@ def _(p: Person) -> None:
reveal_type(p.setdefault("name", "Alice")) # revealed: str
reveal_type(p.setdefault("extra", "default")) # revealed: str
# error: [invalid-key] "Unknown key "extraz" for TypedDict `Person` - did you mean "extra"?"
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?"
reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown
```

View File

@@ -1,7 +1,3 @@
use crate::{Db, Program, PythonVersionWithSource};
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use std::fmt::Write;
/// Suggest a name from `existing_names` that is similar to `wrong_name`.
pub(crate) fn did_you_mean<S: AsRef<str>, T: AsRef<str>>(
existing_names: impl Iterator<Item = S>,
@@ -28,6 +24,10 @@ pub(crate) fn did_you_mean<S: AsRef<str>, T: AsRef<str>>(
.map(|(id, _)| id)
}
use crate::{Db, Program, PythonVersionWithSource};
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use std::fmt::Write;
/// Add a subdiagnostic to `diagnostic` that explains why a certain Python version was inferred.
///
/// ty can infer the Python version from various sources, such as command-line arguments,

View File

@@ -116,10 +116,6 @@ impl LintMetadata {
self.documentation_lines().join("\n")
}
pub fn documentation_url(&self) -> String {
lint_documentation_url(self.name())
}
pub fn default_level(&self) -> Level {
self.default_level
}
@@ -137,10 +133,6 @@ impl LintMetadata {
}
}
pub fn lint_documentation_url(lint_name: LintName) -> String {
format!("https://ty.dev/rules#{lint_name}")
}
#[doc(hidden)]
pub const fn lint_metadata_defaults() -> LintMetadata {
LintMetadata {

View File

@@ -298,7 +298,6 @@ impl<'a> CheckSuppressionsContext<'a> {
let id = DiagnosticId::Lint(lint.name());
let mut diag = Diagnostic::new(id, severity, "");
diag.set_documentation_url(Some(lint.documentation_url()));
let span = Span::from(self.file).with_range(range);
diag.annotate(Annotation::primary(span).message(message));
self.diagnostics.push(diag);

View File

@@ -1310,7 +1310,7 @@ impl<'db> Type<'db> {
self.filter_union(db, |elem| {
!elem
.when_disjoint_from(db, target, inferable)
.satisfied_by_all_typevars(db, inferable)
.is_always_satisfied(db)
})
}
@@ -1625,7 +1625,7 @@ impl<'db> Type<'db> {
/// See [`TypeRelation::Subtyping`] for more details.
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.when_subtype_of(db, target, InferableTypeVars::None)
.satisfied_by_all_typevars(db, InferableTypeVars::None)
.is_always_satisfied(db)
}
fn when_subtype_of(
@@ -1661,7 +1661,7 @@ impl<'db> Type<'db> {
/// See [`TypeRelation::Assignability`] for more details.
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
self.when_assignable_to(db, target, InferableTypeVars::None)
.satisfied_by_all_typevars(db, InferableTypeVars::None)
.is_always_satisfied(db)
}
fn when_assignable_to(
@@ -1679,7 +1679,7 @@ impl<'db> Type<'db> {
#[salsa::tracked(cycle_initial=is_redundant_with_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool {
self.has_relation_to(db, other, InferableTypeVars::None, TypeRelation::Redundancy)
.satisfied_by_all_typevars(db, InferableTypeVars::None)
.is_always_satisfied(db)
}
fn has_relation_to(
@@ -1726,29 +1726,6 @@ impl<'db> Type<'db> {
}
match (self, target) {
// Two identical typevars must always solve to the same type, so they are always
// subtypes of each other and assignable to each other.
//
// Note that this is not handled by the early return at the beginning of this method,
// since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive.
(Type::TypeVar(lhs_bound_typevar), Type::TypeVar(rhs_bound_typevar))
if lhs_bound_typevar.is_same_typevar_as(db, rhs_bound_typevar) =>
{
ConstraintSet::from(true)
}
// A typevar satisfies a relation when...it satisfies the relation. Yes that's a
// tautology! We're moving the caller's subtyping/assignability requirement into a
// constraint set. If the typevar has an upper bound or constraints, then the relation
// only has to hold when the typevar has a valid specialization (i.e., one that
// satisfies the upper bound/constraints).
(Type::TypeVar(bound_typevar), _) => {
ConstraintSet::constrain_typevar(db, bound_typevar, Type::Never, target, relation)
}
(_, Type::TypeVar(bound_typevar)) => {
ConstraintSet::constrain_typevar(db, bound_typevar, self, Type::object(), relation)
}
// Everything is a subtype of `object`.
(_, Type::NominalInstance(instance)) if instance.is_object() => {
ConstraintSet::from(true)
@@ -1852,6 +1829,130 @@ impl<'db> Type<'db> {
},
}),
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
// 2. `T` is a constrained TypeVar and all of `T`'s constraints are subtypes of `S`.
//
// However, there is one exception to this general rule: for any given typevar `T`,
// `T` will always be a subtype of any union containing `T`.
// A similar rule applies in reverse to intersection types.
(Type::TypeVar(bound_typevar), Type::Union(union))
if !bound_typevar.is_inferable(db, inferable)
&& union.elements(db).contains(&self) =>
{
ConstraintSet::from(true)
}
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
if !bound_typevar.is_inferable(db, inferable)
&& intersection.positive(db).contains(&target) =>
{
ConstraintSet::from(true)
}
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
if !bound_typevar.is_inferable(db, inferable)
&& intersection.negative(db).contains(&target) =>
{
ConstraintSet::from(false)
}
// Two identical typevars must always solve to the same type, so they are always
// subtypes of each other and assignable to each other.
//
// Note that this is not handled by the early return at the beginning of this method,
// since subtyping between a TypeVar and an arbitrary other type cannot be guaranteed to be reflexive.
(Type::TypeVar(lhs_bound_typevar), Type::TypeVar(rhs_bound_typevar))
if !lhs_bound_typevar.is_inferable(db, inferable)
&& lhs_bound_typevar.is_same_typevar_as(db, rhs_bound_typevar) =>
{
ConstraintSet::from(true)
}
// A fully static typevar is a subtype of its upper bound, and to something similar to
// the union of its constraints. An unbound, unconstrained, fully static typevar has an
// implicit upper bound of `object` (which is handled above).
(Type::TypeVar(bound_typevar), _)
if !bound_typevar.is_inferable(db, inferable)
&& bound_typevar.typevar(db).bound_or_constraints(db).is_some() =>
{
match bound_typevar.typevar(db).bound_or_constraints(db) {
None => unreachable!(),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound
.has_relation_to_impl(
db,
target,
inferable,
relation,
relation_visitor,
disjointness_visitor,
),
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
constraints.elements(db).iter().when_all(db, |constraint| {
constraint.has_relation_to_impl(
db,
target,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
}
}
// If the typevar is constrained, there must be multiple constraints, and the typevar
// might be specialized to any one of them. However, the constraints do not have to be
// disjoint, which means an lhs type might be a subtype of all of the constraints.
(_, Type::TypeVar(bound_typevar))
if !bound_typevar.is_inferable(db, inferable)
&& !bound_typevar
.typevar(db)
.constraints(db)
.when_some_and(|constraints| {
constraints.iter().when_all(db, |constraint| {
self.has_relation_to_impl(
db,
*constraint,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
})
.is_never_satisfied(db) =>
{
// TODO: The repetition here isn't great, but we really need the fallthrough logic,
// where this arm only engages if it returns true (or in the world of constraints,
// not false). Once we're using real constraint sets instead of bool, we should be
// able to simplify the typevar logic.
bound_typevar
.typevar(db)
.constraints(db)
.when_some_and(|constraints| {
constraints.iter().when_all(db, |constraint| {
self.has_relation_to_impl(
db,
*constraint,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
})
}
(Type::TypeVar(bound_typevar), _) if bound_typevar.is_inferable(db, inferable) => {
// The implicit lower bound of a typevar is `Never`, which means
// that it is always assignable to any other type.
// TODO: record the unification constraints
ConstraintSet::from(true)
}
// `Never` is the bottom type, the empty set.
(_, Type::Never) => ConstraintSet::from(false),
@@ -1943,6 +2044,55 @@ impl<'db> Type<'db> {
})
}
// Other than the special cases checked above, no other types are a subtype of a
// typevar, since there's no guarantee what type the typevar will be specialized to.
// (If the typevar is bounded, it might be specialized to a smaller type than the
// bound. This is true even if the bound is a final class, since the typevar can still
// be specialized to `Never`.)
(_, Type::TypeVar(bound_typevar)) if !bound_typevar.is_inferable(db, inferable) => {
ConstraintSet::from(false)
}
(_, Type::TypeVar(typevar))
if typevar.is_inferable(db, inferable)
&& relation.is_assignability()
&& typevar.typevar(db).upper_bound(db).is_none_or(|bound| {
!self
.has_relation_to_impl(
db,
bound,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
.is_never_satisfied(db)
}) =>
{
// TODO: record the unification constraints
typevar.typevar(db).upper_bound(db).when_none_or(|bound| {
self.has_relation_to_impl(
db,
bound,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
// TODO: Infer specializations here
(_, Type::TypeVar(bound_typevar)) if bound_typevar.is_inferable(db, inferable) => {
ConstraintSet::from(false)
}
(Type::TypeVar(bound_typevar), _) => {
// All inferable cases should have been handled above
assert!(!bound_typevar.is_inferable(db, inferable));
ConstraintSet::from(false)
}
(Type::TypedDict(_), _) => {
// TODO: Implement assignability and subtyping for TypedDict
ConstraintSet::from(relation.is_assignability())
@@ -2334,7 +2484,7 @@ impl<'db> Type<'db> {
disjointness_visitor,
),
(Type::KnownInstance(left), right) => left.instance_fallback(db).has_relation_to_impl(
(Type::KnownInstance(left), right) => left.has_relation_to_impl(
db,
right,
inferable,
@@ -2414,7 +2564,7 @@ impl<'db> Type<'db> {
/// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
self.when_equivalent_to(db, other, InferableTypeVars::None)
.satisfied_by_all_typevars(db, InferableTypeVars::None)
.is_always_satisfied(db)
}
fn when_equivalent_to(
@@ -2478,6 +2628,10 @@ impl<'db> Type<'db> {
first.is_equivalent_to_impl(db, second, inferable, visitor)
}
(Type::KnownInstance(first), Type::KnownInstance(second)) => {
ConstraintSet::from(first.is_equivalent_to(db, second))
}
(Type::Union(first), Type::Union(second)) => {
first.is_equivalent_to_impl(db, second, inferable, visitor)
}
@@ -2541,7 +2695,7 @@ impl<'db> Type<'db> {
/// `false` answers in some cases.
pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool {
self.when_disjoint_from(db, other, InferableTypeVars::None)
.satisfied_by_all_typevars(db, InferableTypeVars::None)
.is_always_satisfied(db)
}
fn when_disjoint_from(
@@ -4283,14 +4437,6 @@ impl<'db> Type<'db> {
))
.into()
}
Type::KnownInstance(KnownInstanceType::GenericContext(tracked))
if name == "specialize_constrained" =>
{
Place::bound(Type::KnownBoundMethod(
KnownBoundMethodType::GenericContextSpecializeConstrained(tracked),
))
.into()
}
Type::ClassLiteral(class)
if name == "__get__" && class.is_known(db, KnownClass::FunctionType) =>
@@ -4807,7 +4953,7 @@ impl<'db> Type<'db> {
Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => {
let constraints = tracked_set.constraints(db);
Truthiness::from(constraints.satisfied_by_all_typevars(db, InferableTypeVars::None))
Truthiness::from(constraints.is_always_satisfied(db))
}
Type::FunctionLiteral(_)
@@ -6570,14 +6716,6 @@ impl<'db> Type<'db> {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::ConstraintSet],
fallback_type: Type::unknown(),
}),
KnownInstanceType::GenericContext(__call__) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::GenericContext],
fallback_type: Type::unknown(),
}),
KnownInstanceType::Specialization(__call__) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Specialization],
fallback_type: Type::unknown(),
}),
KnownInstanceType::SubscriptedProtocol(_) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec_inline![
InvalidTypeExpression::Protocol
@@ -6613,7 +6751,6 @@ impl<'db> Type<'db> {
Ok(ty.inner(db).to_meta_type(db))
}
KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)),
},
Type::SpecialForm(special_form) => match special_form {
@@ -7153,7 +7290,6 @@ impl<'db> Type<'db> {
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_)
)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
@@ -7313,8 +7449,7 @@ impl<'db> Type<'db> {
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_),
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
)
| Type::DataclassDecorator(_)
| Type::DataclassTransformer(_)
@@ -7846,14 +7981,6 @@ pub enum KnownInstanceType<'db> {
/// `ty_extensions.ConstraintSet`.
ConstraintSet(TrackedConstraintSet<'db>),
/// A generic context, which is exposed in mdtests as an instance of
/// `ty_extensions.GenericContext`.
GenericContext(GenericContext<'db>),
/// A specialization, which is exposed in mdtests as an instance of
/// `ty_extensions.Specialization`.
Specialization(Specialization<'db>),
/// A single instance of `types.UnionType`, which stores the left- and
/// right-hand sides of a PEP 604 union.
UnionType(InternedTypes<'db>),
@@ -7867,9 +7994,6 @@ pub enum KnownInstanceType<'db> {
/// An instance of `typing.GenericAlias` representing a `type[...]` expression.
TypeGenericAlias(InternedType<'db>),
/// An instance of `typing.GenericAlias` representing a `Callable[...]` expression.
Callable(CallableType<'db>),
/// An identity callable created with `typing.NewType(name, base)`, which behaves like a
/// subtype of `base` in type expressions. See the `struct NewType` payload for an example.
NewType(NewType<'db>),
@@ -7891,10 +8015,7 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
KnownInstanceType::TypeAliasType(type_alias) => {
visitor.visit_type_alias_type(db, type_alias);
}
KnownInstanceType::Deprecated(_)
| KnownInstanceType::ConstraintSet(_)
| KnownInstanceType::GenericContext(_)
| KnownInstanceType::Specialization(_) => {
KnownInstanceType::Deprecated(_) | KnownInstanceType::ConstraintSet(_) => {
// Nothing to visit
}
KnownInstanceType::Field(field) => {
@@ -7912,9 +8033,6 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
| KnownInstanceType::TypeGenericAlias(ty) => {
visitor.visit_type(db, ty.inner(db));
}
KnownInstanceType::Callable(callable) => {
visitor.visit_callable_type(db, callable);
}
KnownInstanceType::NewType(newtype) => {
if let ClassType::Generic(generic_alias) = newtype.base_class_type(db) {
visitor.visit_generic_alias_type(db, generic_alias);
@@ -7947,23 +8065,23 @@ impl<'db> KnownInstanceType<'db> {
Self::TypeAliasType(type_alias) => {
Self::TypeAliasType(type_alias.normalized_impl(db, visitor))
}
Self::Deprecated(deprecated) => {
// Nothing to normalize
Self::Deprecated(deprecated)
}
Self::Field(field) => Self::Field(field.normalized_impl(db, visitor)),
Self::ConstraintSet(set) => {
// Nothing to normalize
Self::ConstraintSet(set)
}
Self::UnionType(list) => Self::UnionType(list.normalized_impl(db, visitor)),
Self::Literal(ty) => Self::Literal(ty.normalized_impl(db, visitor)),
Self::Annotated(ty) => Self::Annotated(ty.normalized_impl(db, visitor)),
Self::TypeGenericAlias(ty) => Self::TypeGenericAlias(ty.normalized_impl(db, visitor)),
Self::Callable(callable) => Self::Callable(callable.normalized_impl(db, visitor)),
Self::NewType(newtype) => Self::NewType(
newtype
.map_base_class_type(db, |class_type| class_type.normalized_impl(db, visitor)),
),
Self::Deprecated(_)
| Self::ConstraintSet(_)
| Self::GenericContext(_)
| Self::Specialization(_) => {
// Nothing to normalize
self
}
}
}
@@ -7981,13 +8099,10 @@ impl<'db> KnownInstanceType<'db> {
Self::Deprecated(_) => KnownClass::Deprecated,
Self::Field(_) => KnownClass::Field,
Self::ConstraintSet(_) => KnownClass::ConstraintSet,
Self::GenericContext(_) => KnownClass::GenericContext,
Self::Specialization(_) => KnownClass::Specialization,
Self::UnionType(_) => KnownClass::UnionType,
Self::Literal(_)
| Self::Annotated(_)
| Self::TypeGenericAlias(_)
| Self::Callable(_) => KnownClass::GenericAlias,
Self::Literal(_) | Self::Annotated(_) | Self::TypeGenericAlias(_) => {
KnownClass::GenericAlias
}
Self::NewType(_) => KnownClass::NewType,
}
}
@@ -8010,6 +8125,78 @@ impl<'db> KnownInstanceType<'db> {
self.class(db).is_subclass_of(db, class)
}
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
match (self, other) {
(Self::TypeVar(self_typevar), Self::TypeVar(other_typevar)) => {
self_typevar.is_same_typevar_as(db, other_typevar)
}
(
Self::SubscriptedProtocol(_)
| Self::SubscriptedGeneric(_)
| Self::TypeVar(_)
| Self::TypeAliasType(_)
| Self::Deprecated(_)
| Self::Field(_)
| Self::ConstraintSet(_)
| Self::UnionType(_)
| Self::Literal(_)
| Self::Annotated(_)
| Self::TypeGenericAlias(_)
| Self::NewType(_),
Self::SubscriptedProtocol(_)
| Self::SubscriptedGeneric(_)
| Self::TypeVar(_)
| Self::TypeAliasType(_)
| Self::Deprecated(_)
| Self::Field(_)
| Self::ConstraintSet(_)
| Self::UnionType(_)
| Self::Literal(_)
| Self::Annotated(_)
| Self::TypeGenericAlias(_)
| Self::NewType(_),
) => self == other,
}
}
fn has_relation_to_impl(
self,
db: &'db dyn Db,
target: Type<'db>,
inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> {
match (self, target) {
(Self::TypeVar(self_typevar), Type::KnownInstance(Self::TypeVar(other_typevar))) => {
ConstraintSet::from(self_typevar.is_same_typevar_as(db, other_typevar))
}
(
Self::SubscriptedProtocol(_)
| Self::SubscriptedGeneric(_)
| Self::TypeVar(_)
| Self::TypeAliasType(_)
| Self::Deprecated(_)
| Self::Field(_)
| Self::ConstraintSet(_)
| Self::UnionType(_)
| Self::Literal(_)
| Self::Annotated(_)
| Self::TypeGenericAlias(_)
| Self::NewType(_),
_,
) => self.instance_fallback(db).has_relation_to_impl(
db,
target,
inferable,
relation,
relation_visitor,
disjointness_visitor,
),
}
}
/// Return the repr of the symbol at runtime
fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
struct KnownInstanceRepr<'db> {
@@ -8061,38 +8248,19 @@ impl<'db> KnownInstanceType<'db> {
Ok(())
}
KnownInstanceType::ConstraintSet(tracked_set) => {
let constraints = tracked_set
.constraints(self.db)
.limit_to_valid_specializations(self.db);
let constraints = tracked_set.constraints(self.db);
write!(
f,
"ty_extensions.ConstraintSet[{}]",
constraints.display(self.db)
)
}
KnownInstanceType::GenericContext(generic_context) => {
write!(
f,
"ty_extensions.GenericContext{}",
generic_context.display_full(self.db)
)
}
KnownInstanceType::Specialization(specialization) => {
// Normalize for consistent output across CI platforms
write!(
f,
"ty_extensions.Specialization{}",
specialization.normalized(self.db).display_full(self.db)
)
}
KnownInstanceType::UnionType(_) => f.write_str("types.UnionType"),
KnownInstanceType::Literal(_) => f.write_str("<typing.Literal special form>"),
KnownInstanceType::Annotated(_) => {
f.write_str("<typing.Annotated special form>")
}
KnownInstanceType::TypeGenericAlias(_) | KnownInstanceType::Callable(_) => {
f.write_str("GenericAlias")
}
KnownInstanceType::TypeGenericAlias(_) => f.write_str("GenericAlias"),
KnownInstanceType::NewType(declaration) => {
write!(f, "<NewType pseudo-class '{}'>", declaration.name(self.db))
}
@@ -8331,10 +8499,6 @@ enum InvalidTypeExpression<'db> {
Field,
/// Same for `ty_extensions.ConstraintSet`
ConstraintSet,
/// Same for `ty_extensions.GenericContext`
GenericContext,
/// Same for `ty_extensions.Specialization`
Specialization,
/// Same for `typing.TypedDict`
TypedDict,
/// Type qualifiers are always invalid in *type expressions*,
@@ -8387,12 +8551,6 @@ impl<'db> InvalidTypeExpression<'db> {
InvalidTypeExpression::ConstraintSet => {
f.write_str("`ty_extensions.ConstraintSet` is not allowed in type expressions")
}
InvalidTypeExpression::GenericContext => {
f.write_str("`ty_extensions.GenericContext` is not allowed in type expressions")
}
InvalidTypeExpression::Specialization => {
f.write_str("`ty_extensions.GenericContext` is not allowed in type expressions")
}
InvalidTypeExpression::TypedDict => {
f.write_str(
"The special form `typing.TypedDict` is not allowed in type expressions. \
@@ -8648,6 +8806,12 @@ impl<'db> TypeVarInstance<'db> {
BoundTypeVarInstance::new(db, self, BindingContext::Definition(binding_context))
}
/// Returns whether two typevars represent the same logical typevar, regardless of e.g.
/// differences in their bounds or constraints due to materialization.
pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool {
self.identity(db) == other.identity(db)
}
pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name {
self.identity(db).name(db)
}
@@ -10848,9 +11012,6 @@ pub enum KnownBoundMethodType<'db> {
ConstraintSetImpliesSubtypeOf(TrackedConstraintSet<'db>),
ConstraintSetSatisfies(TrackedConstraintSet<'db>),
ConstraintSetSatisfiedByAllTypeVars(TrackedConstraintSet<'db>),
// GenericContext methods
GenericContextSpecializeConstrained(GenericContext<'db>),
}
pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
@@ -10880,8 +11041,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_) => {}
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {}
}
}
@@ -10957,10 +11117,6 @@ impl<'db> KnownBoundMethodType<'db> {
| (
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
)
| (
KnownBoundMethodType::GenericContextSpecializeConstrained(_),
KnownBoundMethodType::GenericContextSpecializeConstrained(_),
) => ConstraintSet::from(true),
(
@@ -10975,8 +11131,7 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_),
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_)
@@ -10988,8 +11143,7 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_),
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
) => ConstraintSet::from(false),
}
}
@@ -11054,11 +11208,6 @@ impl<'db> KnownBoundMethodType<'db> {
.constraints(db)
.iff(db, right_constraints.constraints(db)),
(
KnownBoundMethodType::GenericContextSpecializeConstrained(left_generic_context),
KnownBoundMethodType::GenericContextSpecializeConstrained(right_generic_context),
) => ConstraintSet::from(left_generic_context == right_generic_context),
(
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
@@ -11071,8 +11220,7 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_),
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
KnownBoundMethodType::FunctionTypeDunderGet(_)
| KnownBoundMethodType::FunctionTypeDunderCall(_)
| KnownBoundMethodType::PropertyDunderGet(_)
@@ -11084,8 +11232,7 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_),
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_),
) => ConstraintSet::from(false),
}
}
@@ -11111,8 +11258,7 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_) => self,
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => self,
}
}
@@ -11130,8 +11276,7 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
| KnownBoundMethodType::ConstraintSetSatisfies(_)
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_)
| KnownBoundMethodType::GenericContextSpecializeConstrained(_) => {
| KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => {
KnownClass::ConstraintSet
}
}
@@ -11293,19 +11438,6 @@ impl<'db> KnownBoundMethodType<'db> {
Some(KnownClass::Bool.to_instance(db)),
)))
}
KnownBoundMethodType::GenericContextSpecializeConstrained(_) => {
Either::Right(std::iter::once(Signature::new(
Parameters::new([Parameter::positional_only(Some(Name::new_static(
"constraints",
)))
.with_annotated_type(KnownClass::ConstraintSet.to_instance(db))]),
Some(UnionType::from_elements(
db,
[KnownClass::Specialization.to_instance(db), Type::none(db)],
)),
)))
}
}
}
}
@@ -12491,14 +12623,39 @@ static_assertions::assert_eq_size!(Type, [u8; 16]);
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::db::tests::{TestDbBuilder, setup_db};
use crate::place::{typing_extensions_symbol, typing_symbol};
use crate::db::tests::{TestDb, TestDbBuilder, setup_db};
use crate::place::{ConsideredDefinitions, symbol, typing_extensions_symbol, typing_symbol};
use crate::semantic_index::FileScopeId;
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithWritableSystem as _;
use ruff_python_ast::PythonVersion;
use test_case::test_case;
#[track_caller]
pub(crate) fn get_symbol<'db>(
db: &'db TestDb,
file_name: &str,
scopes: &[&str],
symbol_name: &str,
) -> Place<'db> {
let file = system_path_to_file(db, file_name).expect("file to exist");
let module = parsed_module(db, file).load(db);
let index = semantic_index(db, file);
let mut file_scope_id = FileScopeId::global();
let mut scope = file_scope_id.to_scope_id(db, file);
for expected_scope_name in scopes {
file_scope_id = index
.child_scopes(file_scope_id)
.next()
.unwrap_or_else(|| panic!("scope of {expected_scope_name}"))
.0;
scope = file_scope_id.to_scope_id(db, file);
assert_eq!(scope.name(db, &module), *expected_scope_name);
}
symbol(db, scope, symbol_name, ConsideredDefinitions::EndOfScope).place
}
/// Explicitly test for Python version <3.13 and >=3.13, to ensure that
/// the fallback to `typing_extensions` is working correctly.
/// See [`KnownClass::canonical_module`] for more information.
@@ -12649,6 +12806,40 @@ pub(crate) mod tests {
assert_eq!(intersection.display(&db).to_string(), "Never");
}
#[test]
fn lazy_eager_typevar_equivalence() {
let mut db = setup_db();
db.write_dedented(
"/src/a.py",
r#"
def f[T = int](): ...
"#,
)
.unwrap();
let lazy_ty = get_symbol(&db, "/src/a.py", &["f"], "T").expect_type();
let Type::KnownInstance(KnownInstanceType::TypeVar(lazy_typevar)) = lazy_ty else {
panic!("unexpected type {}", lazy_ty.display(&db));
};
assert_eq!(
lazy_typevar._default(&db),
Some(TypeVarDefaultEvaluation::Lazy)
);
let eager_ty = lazy_ty.normalized(&db);
let Type::KnownInstance(KnownInstanceType::TypeVar(eager_typevar)) = eager_ty else {
panic!("unexpected type {}", eager_ty.display(&db));
};
assert!(matches!(
eager_typevar._default(&db),
Some(TypeVarDefaultEvaluation::Eager(_))
));
assert!(lazy_ty.is_equivalent_to(&db, eager_ty));
assert!(lazy_ty.is_assignable_to(&db, eager_ty));
assert!(eager_ty.is_assignable_to(&db, lazy_ty));
}
#[test]
fn type_alias_variance() {
use crate::db::tests::TestDb;

View File

@@ -396,7 +396,7 @@ impl<'db> BoundSuperType<'db> {
let mut key_builder = UnionBuilder::new(db);
let mut value_builder = UnionBuilder::new(db);
for (name, field) in td.items(db) {
key_builder = key_builder.add(Type::string_literal(db, name));
key_builder = key_builder.add(Type::string_literal(db, &name));
value_builder = value_builder.add(field.declared_ty);
}
return delegate_to(

View File

@@ -782,12 +782,6 @@ impl<'db> Bindings<'db> {
Some(KnownFunction::GenericContext) => {
if let [Some(ty)] = overload.parameter_types() {
let wrap_generic_context = |generic_context| {
Type::KnownInstance(KnownInstanceType::GenericContext(
generic_context,
))
};
let function_generic_context = |function: FunctionType<'db>| {
let union = UnionType::from_elements(
db,
@@ -796,7 +790,7 @@ impl<'db> Bindings<'db> {
.overloads
.iter()
.filter_map(|signature| signature.generic_context)
.map(wrap_generic_context),
.map(|generic_context| generic_context.as_tuple(db)),
);
if union.is_never() {
Type::none(db)
@@ -810,7 +804,7 @@ impl<'db> Bindings<'db> {
overload.set_return_type(match ty {
Type::ClassLiteral(class) => class
.generic_context(db)
.map(wrap_generic_context)
.map(|generic_context| generic_context.as_tuple(db))
.unwrap_or_else(|| Type::none(db)),
Type::FunctionLiteral(function) => {
@@ -825,7 +819,7 @@ impl<'db> Bindings<'db> {
TypeAliasType::PEP695(alias),
)) => alias
.generic_context(db)
.map(wrap_generic_context)
.map(|generic_context| generic_context.as_tuple(db))
.unwrap_or_else(|| Type::none(db)),
_ => Type::none(db),
@@ -1274,28 +1268,6 @@ impl<'db> Bindings<'db> {
overload.set_return_type(Type::BooleanLiteral(result));
}
Type::KnownBoundMethod(
KnownBoundMethodType::GenericContextSpecializeConstrained(generic_context),
) => {
let [Some(constraints)] = overload.parameter_types() else {
continue;
};
let Type::KnownInstance(KnownInstanceType::ConstraintSet(constraints)) =
constraints
else {
continue;
};
let specialization =
generic_context.specialize_constrained(db, constraints.constraints(db));
let result = match specialization {
Ok(specialization) => Type::KnownInstance(
KnownInstanceType::Specialization(specialization),
),
Err(()) => Type::none(db),
};
overload.set_return_type(result);
}
Type::ClassLiteral(class) => match class.known(db) {
Some(KnownClass::Bool) => match overload.parameter_types() {
[Some(arg)] => overload.set_return_type(arg.bool(db).into_type(db)),
@@ -1658,7 +1630,7 @@ impl<'db> CallableBinding<'db> {
.unwrap_or(Type::unknown());
if argument_type
.when_assignable_to(db, parameter_type, overload.inferable_typevars)
.satisfied_by_all_typevars(db, overload.inferable_typevars)
.is_always_satisfied(db)
{
is_argument_assignable_to_any_overload = true;
break 'overload;
@@ -1888,7 +1860,7 @@ impl<'db> CallableBinding<'db> {
current_parameter_type,
overload.inferable_typevars,
)
.satisfied_by_all_typevars(db, overload.inferable_typevars)
.is_always_satisfied(db)
{
participating_parameter_indexes.insert(parameter_index);
}
@@ -2011,7 +1983,7 @@ impl<'db> CallableBinding<'db> {
first_overload_return_type,
overload.inferable_typevars,
)
.satisfied_by_all_typevars(db, overload.inferable_typevars)
.is_always_satisfied(db)
})
} else {
// No matching overload
@@ -2809,21 +2781,15 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
.zip(self.call_expression_tcx.annotation);
self.inferable_typevars = generic_context.inferable_typevars(self.db);
let valid_specializations = generic_context.valid_specializations(self.db);
let mut constraints = ConstraintSet::from(true);
let mut builder = SpecializationBuilder::new(self.db, self.inferable_typevars);
// Prefer the declared type of generic classes.
let preferred_type_mappings = return_with_tcx.and_then(|(return_ty, tcx)| {
tcx.filter_union(self.db, |ty| ty.class_specialization(self.db).is_some())
.class_specialization(self.db)?;
let return_type_mappings =
return_ty.when_assignable_to(self.db, tcx, self.inferable_typevars);
if return_type_mappings.is_never_satisfied(self.db) {
return None;
}
constraints.intersect(self.db, return_type_mappings);
Some(return_type_mappings)
builder.infer(return_ty, tcx).ok()?;
Some(builder.type_mappings().clone())
});
// For a given type variable, we track the variance of any assignments to that type variable
@@ -2843,11 +2809,22 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
continue;
};
let argument_type = variadic_argument_type.unwrap_or(argument_type);
let specialization_result = builder.infer_map(
expected_type,
argument_type,
variadic_argument_type.unwrap_or(argument_type),
|(identity, variance, inferred_ty)| {
// Avoid widening the inferred type if it is already assignable to the
// preferred declared type.
if preferred_type_mappings
.as_ref()
.and_then(|types| types.get(&identity))
.is_some_and(|preferred_ty| {
inferred_ty.is_assignable_to(self.db, *preferred_ty)
})
{
return None;
}
variance_in_arguments
.entry(identity)
.and_modify(|current| *current = current.join(variance))
@@ -2863,44 +2840,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
argument_index: adjusted_argument_index,
});
}
let argument_constraints = expected_type.when_assignable_to(
self.db,
variadic_argument_type.unwrap_or(argument_type),
self.inferable_typevars,
);
if argument_constraints.is_never_satisfied(self.db) {
// This argument is never assignable to its parameter, without considering any
// typevars. This will be caught by `check_argument_types` later.
continue;
}
let valid_argument_constraints =
argument_constraints.and(self.db, || valid_specializations);
if valid_argument_constraints.is_never_satisfied(self.db) {
// There are specializations that make this argument assignable to its
// parameter, but none of them are _valid_ specializations.
// XXX: Figure out which typevars are violated and create a nice
// SpecializationError.
continue;
}
// Avoid widening the inferred type if it is already assignable to the
// preferred declared type.
// XXX: Because constraint sets are ANDed together this might not be needed? AND
// should prefer the tighter specialization.
// XXX: Determine typevar variance per argument
eprintln!(
"--> arg {} {} {}",
argument_index,
argument_constraints.display(self.db),
valid_argument_constraints.display(self.db),
);
constraints.intersect(self.db, valid_argument_constraints);
}
}
eprintln!("--> constraints {}", constraints.display(self.db));
// Attempt to promote any literal types assigned to the specialization.
let maybe_promote = |identity, typevar, ty: Type<'db>| {
@@ -2952,27 +2893,15 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
};
// Build the specialization first without inferring the complete type context.
let Ok(isolated_specialization) =
generic_context.specialize_constrained_mapped(self.db, constraints, maybe_promote)
else {
// XXX: better error
eprintln!("--> X1");
return;
};
// XXX: maybe_promote
let isolated_specialization = builder
.mapped(generic_context, maybe_promote)
.build(generic_context);
let isolated_return_ty = self
.return_ty
.apply_specialization(self.db, isolated_specialization);
eprintln!("--> spec {}", isolated_specialization.display_full(self.db));
eprintln!("--> rty {}", isolated_return_ty.display(self.db));
let mut try_infer_tcx = || {
let (return_ty, call_expression_tcx) = return_with_tcx?;
eprintln!(
"--> infer tcx {} {}",
return_ty.display(self.db),
call_expression_tcx.display(self.db)
);
// A type variable is not a useful type-context for expression inference, and applying it
// to the return type can lead to confusing unions in nested generic calls.
@@ -2983,31 +2912,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
// If the return type is already assignable to the annotated type, we ignore the rest of
// the type context and prefer the narrower inferred type.
if isolated_return_ty.is_assignable_to(self.db, call_expression_tcx) {
eprintln!("--> X2");
return None;
}
// TODO: Ideally we would infer the annotated type _before_ the arguments if this call is part of an
// annotated assignment, to closer match the order of any unions written in the type annotation.
let return_constraints = return_ty
.when_assignable_to(self.db, call_expression_tcx, self.inferable_typevars)
.and(self.db, || valid_specializations);
if return_constraints.is_never_satisfied(self.db) {
eprintln!("--> X3");
return None;
}
builder.infer(return_ty, call_expression_tcx).ok()?;
// Otherwise, build the specialization again after inferring the complete type context.
let Ok(specialization) = generic_context.specialize_constrained_mapped(
self.db,
constraints.and(self.db, || return_constraints),
maybe_promote,
) else {
// XXX: better return
eprintln!("--> X4");
return None;
};
// XXX: maybe_promote
let specialization = builder
.mapped(generic_context, maybe_promote)
.build(generic_context);
let return_ty = return_ty.apply_specialization(self.db, specialization);
Some((Some(specialization), return_ty))
@@ -3027,40 +2942,19 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
let parameters = self.signature.parameters();
let parameter = &parameters[parameter_index];
if let Some(mut expected_ty) = parameter.annotated_type() {
eprintln!(
"--> before {:?} {} {}",
adjusted_argument_index,
argument_type.display(self.db),
expected_ty.display(self.db),
);
if let Some(specialization) = self.specialization {
argument_type = argument_type.apply_specialization(self.db, specialization);
expected_ty = expected_ty.apply_specialization(self.db, specialization);
eprintln!(
"--> after {:?} {} {}",
adjusted_argument_index,
argument_type.display(self.db),
expected_ty.display(self.db),
);
eprintln!(" {:?}", argument_type);
eprintln!(" {:?}", expected_ty);
}
// This is one of the few places where we want to check if there's _any_ specialization
// where assignability holds; normally we want to check that assignability holds for
// _all_ specializations.
// TODO: Soon we will go further, and build the actual specializations from the
// constraint set that we get from this assignability check, instead of inferring and
// building them in an earlier separate step.
let when =
argument_type.when_assignable_to(self.db, expected_ty, self.inferable_typevars);
eprintln!("===> check argument");
eprintln!(" --> arg {}", argument_type.display(self.db));
eprintln!(" --> param {}", expected_ty.display(self.db));
eprintln!(" --> when {}", when.display(self.db));
eprintln!(
" --> sat {}",
when.satisfied_by_all_typevars(self.db, self.inferable_typevars)
);
if !argument_type
if argument_type
.when_assignable_to(self.db, expected_ty, self.inferable_typevars)
.satisfied_by_all_typevars(self.db, self.inferable_typevars)
.is_never_satisfied(self.db)
{
let positional = matches!(argument, Argument::Positional | Argument::Synthetic)
&& !parameter.is_variadic();
@@ -3142,8 +3036,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
if let Type::TypedDict(typed_dict) = argument_type {
for (argument_type, parameter_index) in typed_dict
.items(self.db)
.values()
.map(|field| field.declared_ty)
.iter()
.map(|(_, field)| field.declared_ty)
.zip(&self.argument_matches[argument_index].parameters)
{
self.check_argument_type(
@@ -3194,7 +3088,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
KnownClass::Str.to_instance(self.db),
self.inferable_typevars,
)
.satisfied_by_all_typevars(self.db, self.inferable_typevars)
.is_always_satisfied(self.db)
{
self.errors.push(BindingError::InvalidKeyType {
argument_index: adjusted_argument_index,

View File

@@ -7,6 +7,7 @@ use super::{
SpecialFormType, SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase,
function::FunctionType, infer_expression_type, infer_unpack_types,
};
use crate::FxOrderMap;
use crate::module_resolver::KnownModule;
use crate::place::TypeOrigin;
use crate::semantic_index::definition::{Definition, DefinitionState};
@@ -127,7 +128,7 @@ fn try_metaclass_cycle_initial<'db>(
}
/// A category of classes with code generation capabilities (with synthesized methods).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)]
#[derive(Clone, Copy, Debug, PartialEq, salsa::Update, get_size2::GetSize)]
pub(crate) enum CodeGeneratorKind<'db> {
/// Classes decorated with `@dataclass` or similar dataclass-like decorators
DataclassLike(Option<DataclassTransformerParams<'db>>),
@@ -516,7 +517,7 @@ impl<'db> ClassType<'db> {
/// Return `true` if `other` is present in this class's MRO.
pub(super) fn is_subclass_of(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
self.when_subclass_of(db, other, InferableTypeVars::None)
.satisfied_by_all_typevars(db, InferableTypeVars::None)
.is_always_satisfied(db)
}
pub(super) fn when_subclass_of(
@@ -1252,7 +1253,7 @@ impl MethodDecorator {
}
/// Kind-specific metadata for different types of fields
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FieldKind<'db> {
/// `NamedTuple` field metadata
NamedTuple { default_ty: Option<Type<'db>> },
@@ -1280,7 +1281,7 @@ pub(crate) enum FieldKind<'db> {
}
/// Metadata regarding a dataclass field/attribute or a `TypedDict` "item" / key-value pair.
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Field<'db> {
/// The declared type of the field
pub(crate) declared_ty: Type<'db>,
@@ -2328,8 +2329,7 @@ impl<'db> ClassLiteral<'db> {
|| kw_only.unwrap_or(has_dataclass_param(DataclassFlags::KW_ONLY));
// Use the alias name if provided, otherwise use the field name
let parameter_name =
Name::new(alias.map(|alias| &**alias).unwrap_or(&**field_name));
let parameter_name = alias.map(Name::new).unwrap_or(field_name);
let mut parameter = if is_kw_only {
Parameter::keyword_only(parameter_name)
@@ -2595,7 +2595,7 @@ impl<'db> ClassLiteral<'db> {
(CodeGeneratorKind::TypedDict, "get") => {
let overloads = self
.fields(db, specialization, field_policy)
.iter()
.into_iter()
.flat_map(|(name, field)| {
let key_type =
Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
@@ -2824,13 +2824,12 @@ impl<'db> ClassLiteral<'db> {
/// Returns a list of all annotated attributes defined in this class, or any of its superclasses.
///
/// See [`ClassLiteral::own_fields`] for more details.
#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)]
pub(crate) fn fields(
self,
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
field_policy: CodeGeneratorKind<'db>,
) -> FxIndexMap<Name, Field<'db>> {
field_policy: CodeGeneratorKind,
) -> FxOrderMap<Name, Field<'db>> {
if field_policy == CodeGeneratorKind::NamedTuple {
// NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the
// fields of this class only.
@@ -2878,8 +2877,8 @@ impl<'db> ClassLiteral<'db> {
db: &'db dyn Db,
specialization: Option<Specialization<'db>>,
field_policy: CodeGeneratorKind,
) -> FxIndexMap<Name, Field<'db>> {
let mut attributes = FxIndexMap::default();
) -> FxOrderMap<Name, Field<'db>> {
let mut attributes = FxOrderMap::default();
let class_body_scope = self.body_scope(db);
let table = place_table(db, class_body_scope);
@@ -3957,8 +3956,6 @@ pub enum KnownClass {
Path,
// ty_extensions
ConstraintSet,
GenericContext,
Specialization,
}
impl KnownClass {
@@ -4062,8 +4059,6 @@ impl KnownClass {
| Self::NamedTupleFallback
| Self::NamedTupleLike
| Self::ConstraintSet
| Self::GenericContext
| Self::Specialization
| Self::ProtocolMeta
| Self::TypedDictFallback => Some(Truthiness::Ambiguous),
@@ -4147,8 +4142,6 @@ impl KnownClass {
| KnownClass::NamedTupleFallback
| KnownClass::NamedTupleLike
| KnownClass::ConstraintSet
| KnownClass::GenericContext
| KnownClass::Specialization
| KnownClass::TypedDictFallback
| KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
@@ -4232,8 +4225,6 @@ impl KnownClass {
| KnownClass::NamedTupleFallback
| KnownClass::NamedTupleLike
| KnownClass::ConstraintSet
| KnownClass::GenericContext
| KnownClass::Specialization
| KnownClass::TypedDictFallback
| KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
@@ -4317,8 +4308,6 @@ impl KnownClass {
| KnownClass::NamedTupleLike
| KnownClass::NamedTupleFallback
| KnownClass::ConstraintSet
| KnownClass::GenericContext
| KnownClass::Specialization
| KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
| KnownClass::Template
@@ -4413,8 +4402,6 @@ impl KnownClass {
| Self::InitVar
| Self::NamedTupleFallback
| Self::ConstraintSet
| Self::GenericContext
| Self::Specialization
| Self::TypedDictFallback
| Self::BuiltinFunctionType
| Self::ProtocolMeta
@@ -4504,8 +4491,6 @@ impl KnownClass {
| KnownClass::Template
| KnownClass::Path
| KnownClass::ConstraintSet
| KnownClass::GenericContext
| KnownClass::Specialization
| KnownClass::InitVar => false,
KnownClass::NamedTupleFallback | KnownClass::TypedDictFallback => true,
}
@@ -4614,8 +4599,6 @@ impl KnownClass {
Self::NamedTupleFallback => "NamedTupleFallback",
Self::NamedTupleLike => "NamedTupleLike",
Self::ConstraintSet => "ConstraintSet",
Self::GenericContext => "GenericContext",
Self::Specialization => "Specialization",
Self::TypedDictFallback => "TypedDictFallback",
Self::Template => "Template",
Self::Path => "Path",
@@ -4927,10 +4910,7 @@ impl KnownClass {
| Self::OrderedDict => KnownModule::Collections,
Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses,
Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals,
Self::NamedTupleLike
| Self::ConstraintSet
| Self::GenericContext
| Self::Specialization => KnownModule::TyExtensions,
Self::NamedTupleLike | Self::ConstraintSet => KnownModule::TyExtensions,
Self::Template => KnownModule::Templatelib,
Self::Path => KnownModule::Pathlib,
}
@@ -5013,8 +4993,6 @@ impl KnownClass {
| Self::NamedTupleFallback
| Self::NamedTupleLike
| Self::ConstraintSet
| Self::GenericContext
| Self::Specialization
| Self::TypedDictFallback
| Self::BuiltinFunctionType
| Self::ProtocolMeta
@@ -5103,8 +5081,6 @@ impl KnownClass {
| Self::NamedTupleFallback
| Self::NamedTupleLike
| Self::ConstraintSet
| Self::GenericContext
| Self::Specialization
| Self::TypedDictFallback
| Self::BuiltinFunctionType
| Self::ProtocolMeta
@@ -5208,8 +5184,6 @@ impl KnownClass {
"NamedTupleFallback" => &[Self::NamedTupleFallback],
"NamedTupleLike" => &[Self::NamedTupleLike],
"ConstraintSet" => &[Self::ConstraintSet],
"GenericContext" => &[Self::GenericContext],
"Specialization" => &[Self::Specialization],
"TypedDictFallback" => &[Self::TypedDictFallback],
"Template" => &[Self::Template],
"Path" => &[Self::Path],
@@ -5287,8 +5261,6 @@ impl KnownClass {
| Self::ExtensionsTypeVar
| Self::NamedTupleLike
| Self::ConstraintSet
| Self::GenericContext
| Self::Specialization
| Self::Awaitable
| Self::Generator
| Self::Template

View File

@@ -174,9 +174,6 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::Deprecated(_)
| KnownInstanceType::Field(_)
| KnownInstanceType::ConstraintSet(_)
| KnownInstanceType::Callable(_)
| KnownInstanceType::GenericContext(_)
| KnownInstanceType::Specialization(_)
| KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_)
// A class inheriting from a newtype would make intuitive sense, but newtype

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ use ruff_text_size::{Ranged, TextRange};
use super::{Type, TypeCheckDiagnostics, binding_type};
use crate::lint::{LintSource, lint_documentation_url};
use crate::lint::LintSource;
use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::semantic_index;
use crate::types::function::FunctionDecorators;
@@ -103,7 +103,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> {
}
pub(super) fn is_lint_enabled(&self, lint: &'static LintMetadata) -> bool {
LintDiagnosticGuardBuilder::severity_and_source(self, LintId::of(lint)).is_some()
LintDiagnosticGuardBuilder::severity_and_source(self, lint).is_some()
}
/// Optionally return a builder for a lint diagnostic guard.
@@ -395,7 +395,7 @@ impl Drop for LintDiagnosticGuard<'_, '_> {
/// when the diagnostic is disabled or suppressed (among other reasons).
pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> {
ctx: &'ctx InferContext<'db, 'ctx>,
id: LintId,
id: DiagnosticId,
severity: Severity,
source: LintSource,
primary_span: Span,
@@ -404,7 +404,7 @@ pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> {
impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
fn severity_and_source(
ctx: &'ctx InferContext<'db, 'ctx>,
lint: LintId,
lint: &'static LintMetadata,
) -> Option<(Severity, LintSource)> {
// The comment below was copied from the original
// implementation of diagnostic reporting. The code
@@ -420,9 +420,10 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
if !ctx.db.should_check_file(ctx.file) {
return None;
}
let lint_id = LintId::of(lint);
// Skip over diagnostics if the rule
// is disabled.
let (severity, source) = ctx.db.rule_selection(ctx.file).get(lint)?;
let (severity, source) = ctx.db.rule_selection(ctx.file).get(lint_id)?;
// If we're not in type checking mode,
// we can bail now.
if ctx.is_in_no_type_check() {
@@ -442,20 +443,20 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
lint: &'static LintMetadata,
range: TextRange,
) -> Option<LintDiagnosticGuardBuilder<'db, 'ctx>> {
let lint_id = LintId::of(lint);
let (severity, source) = Self::severity_and_source(ctx, lint_id)?;
let (severity, source) = Self::severity_and_source(ctx, lint)?;
let suppressions = suppressions(ctx.db(), ctx.file());
let lint_id = LintId::of(lint);
if let Some(suppression) = suppressions.find_suppression(range, lint_id) {
ctx.diagnostics.borrow_mut().mark_used(suppression.id());
return None;
}
let id = DiagnosticId::Lint(lint.name());
let primary_span = Span::from(ctx.file()).with_range(range);
Some(LintDiagnosticGuardBuilder {
ctx,
id: lint_id,
id,
severity,
source,
primary_span,
@@ -476,8 +477,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
self,
message: impl std::fmt::Display,
) -> LintDiagnosticGuard<'db, 'ctx> {
let mut diag = Diagnostic::new(DiagnosticId::Lint(self.id.name()), self.severity, message);
diag.set_documentation_url(Some(self.id.documentation_url()));
let mut diag = Diagnostic::new(self.id, self.severity, message);
// This is why `LintDiagnosticGuard::set_primary_message` exists.
// We add the primary annotation here (because it's required), but
// the optional message can be added later. We could accept it here
@@ -629,15 +629,10 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> {
self,
message: impl std::fmt::Display,
) -> DiagnosticGuard<'db, 'ctx> {
let mut diag = Diagnostic::new(self.id, self.severity, message);
if let DiagnosticId::Lint(lint_name) = diag.id() {
diag.set_documentation_url(Some(lint_documentation_url(lint_name)));
}
let diag = Some(Diagnostic::new(self.id, self.severity, message));
DiagnosticGuard {
ctx: self.ctx,
diag: Some(diag),
diag,
}
}
}

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