Compare commits

..

1 Commits

Author SHA1 Message Date
Zanie Blue
e1c6e2e26a Add test case for missing VIRTUAL_ENV 2025-12-17 15:27:47 -06:00
130 changed files with 1331 additions and 5453 deletions

View File

@@ -4,6 +4,5 @@
# Enable off-by-default rules.
[rules]
possibly-unresolved-reference = "warn"
possibly-missing-import = "warn"
unused-ignore-comment = "warn"
division-by-zero = "warn"

View File

@@ -67,7 +67,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@2e1816eac09c90140b1ba51d19afc5f59da460f5"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
ecosystem-analyzer \
--repository ruff \

View File

@@ -52,7 +52,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@2e1816eac09c90140b1ba51d19afc5f59da460f5"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
ecosystem-analyzer \
--verbose \

View File

@@ -1,54 +1,5 @@
# Changelog
## 0.14.10
Released on 2025-12-18.
### Preview features
- [formatter] Fluent formatting of method chains ([#21369](https://github.com/astral-sh/ruff/pull/21369))
- [formatter] Keep lambda parameters on one line and parenthesize the body if it expands ([#21385](https://github.com/astral-sh/ruff/pull/21385))
- \[`flake8-implicit-str-concat`\] New rule to prevent implicit string concatenation in collections (`ISC004`) ([#21972](https://github.com/astral-sh/ruff/pull/21972))
- \[`flake8-use-pathlib`\] Make fixes unsafe when types change in compound statements (`PTH104`, `PTH105`, `PTH109`, `PTH115`) ([#22009](https://github.com/astral-sh/ruff/pull/22009))
- \[`refurb`\] Extend support for `Path.open` (`FURB101`, `FURB103`) ([#21080](https://github.com/astral-sh/ruff/pull/21080))
### Bug fixes
- \[`pyupgrade`\] Fix parsing named Unicode escape sequences (`UP032`) ([#21901](https://github.com/astral-sh/ruff/pull/21901))
### Rule changes
- \[`eradicate`\] Ignore `ruff:disable` and `ruff:enable` comments in `ERA001` ([#22038](https://github.com/astral-sh/ruff/pull/22038))
- \[`flake8-pytest-style`\] Allow `match` and `check` keyword arguments without an expected exception type (`PT010`) ([#21964](https://github.com/astral-sh/ruff/pull/21964))
- [syntax-errors] Annotated name cannot be global ([#20868](https://github.com/astral-sh/ruff/pull/20868))
### Documentation
- Add `uv` and `ty` to the Ruff README ([#21996](https://github.com/astral-sh/ruff/pull/21996))
- Document known lambda formatting deviations from Black ([#21954](https://github.com/astral-sh/ruff/pull/21954))
- Update `setup.md` ([#22024](https://github.com/astral-sh/ruff/pull/22024))
- \[`flake8-bandit`\] Fix broken link (`S704`) ([#22039](https://github.com/astral-sh/ruff/pull/22039))
### Other changes
- Fix playground Share button showing "Copied!" before clipboard copy completes ([#21942](https://github.com/astral-sh/ruff/pull/21942))
### Contributors
- [@dylwil3](https://github.com/dylwil3)
- [@charliecloudberry](https://github.com/charliecloudberry)
- [@charliermarsh](https://github.com/charliermarsh)
- [@chirizxc](https://github.com/chirizxc)
- [@ntBre](https://github.com/ntBre)
- [@zanieb](https://github.com/zanieb)
- [@amyreese](https://github.com/amyreese)
- [@hauntsaninja](https://github.com/hauntsaninja)
- [@11happy](https://github.com/11happy)
- [@mahiro72](https://github.com/mahiro72)
- [@MichaReiser](https://github.com/MichaReiser)
- [@phongddo](https://github.com/phongddo)
- [@PeterJCLaw](https://github.com/PeterJCLaw)
## 0.14.9
Released on 2025-12-11.

29
Cargo.lock generated
View File

@@ -1004,6 +1004,27 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dir-test"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62c013fe825864f3e4593f36426c1fa7a74f5603f13ca8d1af7a990c1cd94a79"
dependencies = [
"dir-test-macros",
]
[[package]]
name = "dir-test-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d42f54d7b4a6bc2400fe5b338e35d1a335787585375322f49c5d5fe7b243da7e"
dependencies = [
"glob",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dirs"
version = "6.0.0"
@@ -2887,7 +2908,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.10"
version = "0.14.9"
dependencies = [
"anyhow",
"argfile",
@@ -3145,7 +3166,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.10"
version = "0.14.9"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3504,7 +3525,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.10"
version = "0.14.9"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -4492,7 +4513,7 @@ dependencies = [
"camino",
"colored 3.0.0",
"compact_str",
"datatest-stable",
"dir-test",
"drop_bomb",
"get-size2",
"glob",

View File

@@ -82,6 +82,7 @@ criterion = { version = "0.7.0", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
datatest-stable = { version = "0.3.3" }
dir-test = { version = "0.4.0" }
dunce = { version = "1.0.5" }
drop_bomb = { version = "0.1.5" }
etcetera = { version = "0.11.0" }

View File

@@ -150,8 +150,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.10/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.10/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.9/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.9/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -184,7 +184,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.10
rev: v0.14.9
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -4,7 +4,6 @@ extend-exclude = [
"crates/ty_vendored/vendor/**/*",
"**/resources/**/*",
"**/snapshots/**/*",
"crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs",
# Completion tests tend to have a lot of incomplete
# words naturally. It's annoying to have to make all
# of them actually words. So just ignore typos here.

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.10"
version = "0.14.9"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -56,7 +56,10 @@ impl TestSystem {
/// Removes an environment variable override, making it appear as not set.
pub fn remove_env_var(&self, name: impl Into<String>) {
self.env_overrides.lock().unwrap().insert(name.into(), None);
self.env_overrides
.lock()
.unwrap()
.insert(name.into(), None);
}
/// Returns the [`InMemorySystem`].

View File

@@ -119,7 +119,7 @@ fn generate_markdown() -> String {
let _ = writeln!(
&mut output,
r#"<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of '{level}'."><code>{level}</code></a> ·
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of '{level}'."><code>{level}</code></a> ·
{status_text} ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/{file}#L{line}" target="_blank">View source</a>

View File

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

View File

@@ -1,66 +0,0 @@
facts = (
"Lobsters have blue blood.",
"The liver is the only human organ that can fully regenerate itself.",
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon.",
)
facts = [
"Lobsters have blue blood.",
"The liver is the only human organ that can fully regenerate itself.",
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon.",
]
facts = {
"Lobsters have blue blood.",
"The liver is the only human organ that can fully regenerate itself.",
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon.",
}
facts = {
(
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon."
),
}
facts = (
"Octopuses have three hearts."
# Missing comma here.
"Honey never spoils.",
)
facts = [
"Octopuses have three hearts."
# Missing comma here.
"Honey never spoils.",
]
facts = {
"Octopuses have three hearts."
# Missing comma here.
"Honey never spoils.",
}
facts = (
(
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon."
),
)
facts = [
(
"Clarinets are made almost entirely out of wood from the mpingo tree."
"In 1971, astronaut Alan Shepard played golf on the moon."
),
]
facts = (
"Lobsters have blue blood.\n"
"The liver is the only human organ that can fully regenerate itself.\n"
"Clarinets are made almost entirely out of wood from the mpingo tree.\n"
"In 1971, astronaut Alan Shepard played golf on the moon.\n"
)

View File

@@ -9,15 +9,3 @@ def test_ok():
def test_error():
with pytest.raises(UnicodeError):
pass
def test_match_only():
with pytest.raises(match="some error message"):
pass
def test_check_only():
with pytest.raises(check=lambda e: True):
pass
def test_match_and_check():
with pytest.raises(match="some error message", check=lambda e: True):
pass

View File

@@ -136,38 +136,4 @@ os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
# See: https://github.com/astral-sh/ruff/issues/21794
import sys
if os.rename("pth1.py", "pth1.py.bak"):
print("rename: truthy")
else:
print("rename: falsey")
if os.replace("pth1.py.bak", "pth1.py"):
print("replace: truthy")
else:
print("replace: falsey")
try:
for _ in os.getcwd():
print("getcwd: iterable")
break
except TypeError as e:
print("getcwd: not iterable")
try:
for _ in os.getcwdb():
print("getcwdb: iterable")
break
except TypeError as e:
print("getcwdb: not iterable")
try:
for _ in os.readlink(sys.executable):
print("readlink: iterable")
break
except TypeError as e:
print("readlink: not iterable")
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)

View File

@@ -138,6 +138,5 @@ with open("file.txt", encoding="utf-8") as f:
with open("file.txt", encoding="utf-8") as f:
contents = process_contents(f.read())
with open("file1.txt", encoding="utf-8") as f:
with open("file.txt", encoding="utf-8") as f:
contents: str = process_contents(f.read())

View File

@@ -1,8 +0,0 @@
from pathlib import Path
with Path("file.txt").open() as f:
contents = f.read()
with Path("file.txt").open("r") as f:
contents = f.read()

View File

@@ -1,26 +0,0 @@
from pathlib import Path
with Path("file.txt").open("w") as f:
f.write("test")
with Path("file.txt").open("wb") as f:
f.write(b"test")
with Path("file.txt").open(mode="w") as f:
f.write("test")
with Path("file.txt").open("w", encoding="utf8") as f:
f.write("test")
with Path("file.txt").open("w", errors="ignore") as f:
f.write("test")
with Path(foo()).open("w") as f:
f.write("test")
p = Path("file.txt")
with p.open("w") as f:
f.write("test")
with Path("foo", "bar", "baz").open("w") as f:
f.write("test")

View File

@@ -86,26 +86,3 @@ def f():
# Multiple codes but none are used
# ruff: disable[E741, F401, F841]
print("hello")
def f():
# Unknown rule codes
# ruff: disable[YF829]
# ruff: disable[F841, RQW320]
value = 0
# ruff: enable[F841, RQW320]
# ruff: enable[YF829]
def f():
# External rule codes should be ignored
# ruff: disable[TK421]
print("hello")
# ruff: enable[TK421]
def f():
# Empty or missing rule codes
# ruff: disable
# ruff: disable[]
print("hello")

View File

@@ -214,13 +214,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
range: _,
node_index: _,
}) => {
if checker.is_rule_enabled(Rule::ImplicitStringConcatenationInCollectionLiteral) {
flake8_implicit_str_concat::rules::implicit_string_concatenation_in_collection_literal(
checker,
expr,
elts,
);
}
if ctx.is_store() {
let check_too_many_expressions =
checker.is_rule_enabled(Rule::ExpressionsInStarAssignment);
@@ -1336,13 +1329,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
}
}
Expr::Set(set) => {
if checker.is_rule_enabled(Rule::ImplicitStringConcatenationInCollectionLiteral) {
flake8_implicit_str_concat::rules::implicit_string_concatenation_in_collection_literal(
checker,
expr,
&set.elts,
);
}
if checker.is_rule_enabled(Rule::DuplicateValue) {
flake8_bugbear::rules::duplicate_value(checker, set);
}

View File

@@ -454,7 +454,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8ImplicitStrConcat, "001") => rules::flake8_implicit_str_concat::rules::SingleLineImplicitStringConcatenation,
(Flake8ImplicitStrConcat, "002") => rules::flake8_implicit_str_concat::rules::MultiLineImplicitStringConcatenation,
(Flake8ImplicitStrConcat, "003") => rules::flake8_implicit_str_concat::rules::ExplicitStringConcatenation,
(Flake8ImplicitStrConcat, "004") => rules::flake8_implicit_str_concat::rules::ImplicitStringConcatenationInCollectionLiteral,
// flake8-print
(Flake8Print, "1") => rules::flake8_print::rules::Print,
@@ -1064,8 +1063,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,
(Ruff, "102") => rules::ruff::rules::InvalidRuleCode,
(Ruff, "103") => rules::ruff::rules::InvalidSuppressionComment,
(Ruff, "104") => rules::ruff::rules::UnmatchedSuppressionComment,
(Ruff, "200") => rules::ruff::rules::InvalidPyprojectToml,
#[cfg(any(feature = "test-rules", test))]

View File

@@ -22,7 +22,6 @@ static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
# Case-sensitive
pyright
| pyrefly
| ruff\s*:\s*(disable|enable)
| mypy:
| type:\s*ignore
| SPDX-License-Identifier:
@@ -149,8 +148,6 @@ mod tests {
assert!(!comment_contains_code("# 123", &[]));
assert!(!comment_contains_code("# 123.1", &[]));
assert!(!comment_contains_code("# 1, 2, 3", &[]));
assert!(!comment_contains_code("# ruff: disable[E501]", &[]));
assert!(!comment_contains_code("#ruff:enable[E501, F84]", &[]));
assert!(!comment_contains_code(
"# pylint: disable=redefined-outer-name",
&[]

View File

@@ -70,7 +70,7 @@ fn is_open_call(func: &Expr, semantic: &SemanticModel) -> bool {
}
/// Returns `true` if an expression resolves to a call to `pathlib.Path.open`.
pub(crate) fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
return false;
};

View File

@@ -18,7 +18,7 @@ mod async_zero_sleep;
mod blocking_http_call;
mod blocking_http_call_httpx;
mod blocking_input;
pub(crate) mod blocking_open_call;
mod blocking_open_call;
mod blocking_path_methods;
mod blocking_process_invocation;
mod blocking_sleep;

View File

@@ -12,7 +12,7 @@ use crate::{checkers::ast::Checker, settings::LinterSettings};
/// Checks for non-literal strings being passed to [`markupsafe.Markup`][markupsafe-markup].
///
/// ## Why is this bad?
/// [`markupsafe.Markup`][markupsafe-markup] does not perform any escaping, so passing dynamic
/// [`markupsafe.Markup`] does not perform any escaping, so passing dynamic
/// content, like f-strings, variables or interpolated strings will potentially
/// lead to XSS vulnerabilities.
///

View File

@@ -32,10 +32,6 @@ mod tests {
Path::new("ISC_syntax_error_2.py")
)]
#[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"))]
#[test_case(
Rule::ImplicitStringConcatenationInCollectionLiteral,
Path::new("ISC004.py")
)]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,103 +0,0 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::token::parenthesized_range;
use ruff_python_ast::{Expr, StringLike};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for implicitly concatenated strings inside list, tuple, and set literals.
///
/// ## Why is this bad?
/// In collection literals, implicit string concatenation is often the result of
/// a missing comma between elements, which can silently merge items together.
///
/// ## Example
/// ```python
/// facts = (
/// "Lobsters have blue blood.",
/// "The liver is the only human organ that can fully regenerate itself.",
/// "Clarinets are made almost entirely out of wood from the mpingo tree."
/// "In 1971, astronaut Alan Shepard played golf on the moon.",
/// )
/// ```
///
/// Instead, you likely intended:
/// ```python
/// facts = (
/// "Lobsters have blue blood.",
/// "The liver is the only human organ that can fully regenerate itself.",
/// "Clarinets are made almost entirely out of wood from the mpingo tree.",
/// "In 1971, astronaut Alan Shepard played golf on the moon.",
/// )
/// ```
///
/// If the concatenation is intentional, wrap it in parentheses to make it
/// explicit:
/// ```python
/// facts = (
/// "Lobsters have blue blood.",
/// "The liver is the only human organ that can fully regenerate itself.",
/// (
/// "Clarinets are made almost entirely out of wood from the mpingo tree."
/// "In 1971, astronaut Alan Shepard played golf on the moon."
/// ),
/// )
/// ```
///
/// ## Fix safety
/// The fix is safe in that it does not change the semantics of your code.
/// However, the issue is that you may often want to change semantics
/// by adding a missing comma.
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.10")]
pub(crate) struct ImplicitStringConcatenationInCollectionLiteral;
impl Violation for ImplicitStringConcatenationInCollectionLiteral {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;
#[derive_message_formats]
fn message(&self) -> String {
"Unparenthesized implicit string concatenation in collection".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Wrap implicitly concatenated strings in parentheses".to_string())
}
}
/// ISC004
pub(crate) fn implicit_string_concatenation_in_collection_literal(
checker: &Checker,
expr: &Expr,
elements: &[Expr],
) {
for element in elements {
let Ok(string_like) = StringLike::try_from(element) else {
continue;
};
if !string_like.is_implicit_concatenated() {
continue;
}
if parenthesized_range(
string_like.as_expression_ref(),
expr.into(),
checker.tokens(),
)
.is_some()
{
continue;
}
let mut diagnostic = checker.report_diagnostic(
ImplicitStringConcatenationInCollectionLiteral,
string_like.range(),
);
diagnostic.help("Did you forget a comma?");
diagnostic.set_fix(Fix::unsafe_edits(
Edit::insertion("(".to_string(), string_like.range().start()),
[Edit::insertion(")".to_string(), string_like.range().end())],
));
}
}

View File

@@ -1,7 +1,5 @@
pub(crate) use collection_literal::*;
pub(crate) use explicit::*;
pub(crate) use implicit::*;
mod collection_literal;
mod explicit;
mod implicit;

View File

@@ -1,149 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
---
ISC004 [*] Unparenthesized implicit string concatenation in collection
--> ISC004.py:4:5
|
2 | "Lobsters have blue blood.",
3 | "The liver is the only human organ that can fully regenerate itself.",
4 | / "Clarinets are made almost entirely out of wood from the mpingo tree."
5 | | "In 1971, astronaut Alan Shepard played golf on the moon.",
| |______________________________________________________________^
6 | )
|
help: Wrap implicitly concatenated strings in parentheses
help: Did you forget a comma?
1 | facts = (
2 | "Lobsters have blue blood.",
3 | "The liver is the only human organ that can fully regenerate itself.",
- "Clarinets are made almost entirely out of wood from the mpingo tree."
- "In 1971, astronaut Alan Shepard played golf on the moon.",
4 + ("Clarinets are made almost entirely out of wood from the mpingo tree."
5 + "In 1971, astronaut Alan Shepard played golf on the moon."),
6 | )
7 |
8 | facts = [
note: This is an unsafe fix and may change runtime behavior
ISC004 [*] Unparenthesized implicit string concatenation in collection
--> ISC004.py:11:5
|
9 | "Lobsters have blue blood.",
10 | "The liver is the only human organ that can fully regenerate itself.",
11 | / "Clarinets are made almost entirely out of wood from the mpingo tree."
12 | | "In 1971, astronaut Alan Shepard played golf on the moon.",
| |______________________________________________________________^
13 | ]
|
help: Wrap implicitly concatenated strings in parentheses
help: Did you forget a comma?
8 | facts = [
9 | "Lobsters have blue blood.",
10 | "The liver is the only human organ that can fully regenerate itself.",
- "Clarinets are made almost entirely out of wood from the mpingo tree."
- "In 1971, astronaut Alan Shepard played golf on the moon.",
11 + ("Clarinets are made almost entirely out of wood from the mpingo tree."
12 + "In 1971, astronaut Alan Shepard played golf on the moon."),
13 | ]
14 |
15 | facts = {
note: This is an unsafe fix and may change runtime behavior
ISC004 [*] Unparenthesized implicit string concatenation in collection
--> ISC004.py:18:5
|
16 | "Lobsters have blue blood.",
17 | "The liver is the only human organ that can fully regenerate itself.",
18 | / "Clarinets are made almost entirely out of wood from the mpingo tree."
19 | | "In 1971, astronaut Alan Shepard played golf on the moon.",
| |______________________________________________________________^
20 | }
|
help: Wrap implicitly concatenated strings in parentheses
help: Did you forget a comma?
15 | facts = {
16 | "Lobsters have blue blood.",
17 | "The liver is the only human organ that can fully regenerate itself.",
- "Clarinets are made almost entirely out of wood from the mpingo tree."
- "In 1971, astronaut Alan Shepard played golf on the moon.",
18 + ("Clarinets are made almost entirely out of wood from the mpingo tree."
19 + "In 1971, astronaut Alan Shepard played golf on the moon."),
20 | }
21 |
22 | facts = {
note: This is an unsafe fix and may change runtime behavior
ISC004 [*] Unparenthesized implicit string concatenation in collection
--> ISC004.py:30:5
|
29 | facts = (
30 | / "Octopuses have three hearts."
31 | | # Missing comma here.
32 | | "Honey never spoils.",
| |_________________________^
33 | )
|
help: Wrap implicitly concatenated strings in parentheses
help: Did you forget a comma?
27 | }
28 |
29 | facts = (
- "Octopuses have three hearts."
30 + ("Octopuses have three hearts."
31 | # Missing comma here.
- "Honey never spoils.",
32 + "Honey never spoils."),
33 | )
34 |
35 | facts = [
note: This is an unsafe fix and may change runtime behavior
ISC004 [*] Unparenthesized implicit string concatenation in collection
--> ISC004.py:36:5
|
35 | facts = [
36 | / "Octopuses have three hearts."
37 | | # Missing comma here.
38 | | "Honey never spoils.",
| |_________________________^
39 | ]
|
help: Wrap implicitly concatenated strings in parentheses
help: Did you forget a comma?
33 | )
34 |
35 | facts = [
- "Octopuses have three hearts."
36 + ("Octopuses have three hearts."
37 | # Missing comma here.
- "Honey never spoils.",
38 + "Honey never spoils."),
39 | ]
40 |
41 | facts = {
note: This is an unsafe fix and may change runtime behavior
ISC004 [*] Unparenthesized implicit string concatenation in collection
--> ISC004.py:42:5
|
41 | facts = {
42 | / "Octopuses have three hearts."
43 | | # Missing comma here.
44 | | "Honey never spoils.",
| |_________________________^
45 | }
|
help: Wrap implicitly concatenated strings in parentheses
help: Did you forget a comma?
39 | ]
40 |
41 | facts = {
- "Octopuses have three hearts."
42 + ("Octopuses have three hearts."
43 | # Missing comma here.
- "Honey never spoils.",
44 + "Honey never spoils."),
45 | }
46 |
47 | facts = (
note: This is an unsafe fix and may change runtime behavior

View File

@@ -125,9 +125,6 @@ impl Violation for PytestRaisesTooBroad {
/// ## Why is this bad?
/// `pytest.raises` expects to receive an expected exception as its first
/// argument. If omitted, the `pytest.raises` call will fail at runtime.
/// The rule will also accept calls without an expected exception but with
/// `match` and/or `check` keyword arguments, which are also valid after
/// pytest version 8.4.0.
///
/// ## Example
/// ```python
@@ -184,8 +181,6 @@ pub(crate) fn raises_call(checker: &Checker, call: &ast::ExprCall) {
.arguments
.find_argument("expected_exception", 0)
.is_none()
&& call.arguments.find_keyword("match").is_none()
&& call.arguments.find_keyword("check").is_none()
{
checker.report_diagnostic(PytestRaisesWithoutException, call.func.range());
}

View File

@@ -210,7 +210,6 @@ pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, positio
/// Returns `true` if the given call is a top-level expression in its statement.
/// This means the call's return value is not used, so return type changes don't matter.
pub(crate) fn is_top_level_expression_in_statement(checker: &Checker) -> bool {
pub(crate) fn is_top_level_expression_call(checker: &Checker) -> bool {
checker.semantic().current_expression_parent().is_none()
&& checker.semantic().current_statement().is_expr_stmt()
}

View File

@@ -6,7 +6,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_getcwd_enabled;
use crate::rules::flake8_use_pathlib::helpers::is_top_level_expression_in_statement;
use crate::rules::flake8_use_pathlib::helpers::is_top_level_expression_call;
use crate::{FixAvailability, Violation};
/// ## What it does
@@ -89,7 +89,7 @@ pub(crate) fn os_getcwd(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// Unsafe when the fix would delete comments or change a used return value
let applicability = if checker.comment_ranges().intersects(range)
|| !is_top_level_expression_in_statement(checker)
|| !is_top_level_expression_call(checker)
{
Applicability::Unsafe
} else {

View File

@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_readlink_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
is_top_level_expression_in_statement,
is_top_level_expression_call,
};
use crate::{FixAvailability, Violation};
@@ -86,7 +86,7 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
return;
}
let applicability = if !is_top_level_expression_in_statement(checker) {
let applicability = if !is_top_level_expression_call(checker) {
// Unsafe because the return type changes (str/bytes -> Path)
Applicability::Unsafe
} else {

View File

@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_rename_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default, is_top_level_expression_in_statement,
is_keyword_only_argument_non_default, is_top_level_expression_call,
};
use crate::{FixAvailability, Violation};
@@ -92,7 +92,7 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
);
// Unsafe when the fix would delete comments or change a used return value
let applicability = if !is_top_level_expression_in_statement(checker) {
let applicability = if !is_top_level_expression_call(checker) {
// Unsafe because the return type changes (None -> Path)
Applicability::Unsafe
} else {

View File

@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_replace_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
is_keyword_only_argument_non_default, is_top_level_expression_in_statement,
is_keyword_only_argument_non_default, is_top_level_expression_call,
};
use crate::{FixAvailability, Violation};
@@ -95,7 +95,7 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
);
// Unsafe when the fix would delete comments or change a used return value
let applicability = if !is_top_level_expression_in_statement(checker) {
let applicability = if !is_top_level_expression_call(checker) {
// Unsafe because the return type changes (None -> Path)
Applicability::Unsafe
} else {

View File

@@ -567,64 +567,5 @@ PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^^^^^^^
140 |
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
help: Replace with `Path(...).samefile()`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:144:4
|
142 | import sys
143 |
144 | if os.rename("pth1.py", "pth1.py.bak"):
| ^^^^^^^^^
145 | print("rename: truthy")
146 | else:
|
help: Replace with `Path(...).rename(...)`
PTH105 `os.replace()` should be replaced by `Path.replace()`
--> full_name.py:149:4
|
147 | print("rename: falsey")
148 |
149 | if os.replace("pth1.py.bak", "pth1.py"):
| ^^^^^^^^^^
150 | print("replace: truthy")
151 | else:
|
help: Replace with `Path(...).replace(...)`
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:155:14
|
154 | try:
155 | for _ in os.getcwd():
| ^^^^^^^^^
156 | print("getcwd: iterable")
157 | break
|
help: Replace with `Path.cwd()`
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:162:14
|
161 | try:
162 | for _ in os.getcwdb():
| ^^^^^^^^^^
163 | print("getcwdb: iterable")
164 | break
|
help: Replace with `Path.cwd()`
PTH115 `os.readlink()` should be replaced by `Path.readlink()`
--> full_name.py:169:14
|
168 | try:
169 | for _ in os.readlink(sys.executable):
| ^^^^^^^^^^^
170 | print("readlink: iterable")
171 | break
|
help: Replace with `Path(...).readlink()`

View File

@@ -1037,142 +1037,5 @@ PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
138 |
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
| ^^^^^^^^^^^^^^^^
140 |
141 | # See: https://github.com/astral-sh/ruff/issues/21794
|
help: Replace with `Path(...).samefile()`
PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
--> full_name.py:144:4
|
142 | import sys
143 |
144 | if os.rename("pth1.py", "pth1.py.bak"):
| ^^^^^^^^^
145 | print("rename: truthy")
146 | else:
|
help: Replace with `Path(...).rename(...)`
140 |
141 | # See: https://github.com/astral-sh/ruff/issues/21794
142 | import sys
143 + import pathlib
144 |
- if os.rename("pth1.py", "pth1.py.bak"):
145 + if pathlib.Path("pth1.py").rename("pth1.py.bak"):
146 | print("rename: truthy")
147 | else:
148 | print("rename: falsey")
note: This is an unsafe fix and may change runtime behavior
PTH105 [*] `os.replace()` should be replaced by `Path.replace()`
--> full_name.py:149:4
|
147 | print("rename: falsey")
148 |
149 | if os.replace("pth1.py.bak", "pth1.py"):
| ^^^^^^^^^^
150 | print("replace: truthy")
151 | else:
|
help: Replace with `Path(...).replace(...)`
140 |
141 | # See: https://github.com/astral-sh/ruff/issues/21794
142 | import sys
143 + import pathlib
144 |
145 | if os.rename("pth1.py", "pth1.py.bak"):
146 | print("rename: truthy")
147 | else:
148 | print("rename: falsey")
149 |
- if os.replace("pth1.py.bak", "pth1.py"):
150 + if pathlib.Path("pth1.py.bak").replace("pth1.py"):
151 | print("replace: truthy")
152 | else:
153 | print("replace: falsey")
note: This is an unsafe fix and may change runtime behavior
PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:155:14
|
154 | try:
155 | for _ in os.getcwd():
| ^^^^^^^^^
156 | print("getcwd: iterable")
157 | break
|
help: Replace with `Path.cwd()`
140 |
141 | # See: https://github.com/astral-sh/ruff/issues/21794
142 | import sys
143 + import pathlib
144 |
145 | if os.rename("pth1.py", "pth1.py.bak"):
146 | print("rename: truthy")
--------------------------------------------------------------------------------
153 | print("replace: falsey")
154 |
155 | try:
- for _ in os.getcwd():
156 + for _ in pathlib.Path.cwd():
157 | print("getcwd: iterable")
158 | break
159 | except TypeError as e:
note: This is an unsafe fix and may change runtime behavior
PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:162:14
|
161 | try:
162 | for _ in os.getcwdb():
| ^^^^^^^^^^
163 | print("getcwdb: iterable")
164 | break
|
help: Replace with `Path.cwd()`
140 |
141 | # See: https://github.com/astral-sh/ruff/issues/21794
142 | import sys
143 + import pathlib
144 |
145 | if os.rename("pth1.py", "pth1.py.bak"):
146 | print("rename: truthy")
--------------------------------------------------------------------------------
160 | print("getcwd: not iterable")
161 |
162 | try:
- for _ in os.getcwdb():
163 + for _ in pathlib.Path.cwd():
164 | print("getcwdb: iterable")
165 | break
166 | except TypeError as e:
note: This is an unsafe fix and may change runtime behavior
PTH115 [*] `os.readlink()` should be replaced by `Path.readlink()`
--> full_name.py:169:14
|
168 | try:
169 | for _ in os.readlink(sys.executable):
| ^^^^^^^^^^^
170 | print("readlink: iterable")
171 | break
|
help: Replace with `Path(...).readlink()`
140 |
141 | # See: https://github.com/astral-sh/ruff/issues/21794
142 | import sys
143 + import pathlib
144 |
145 | if os.rename("pth1.py", "pth1.py.bak"):
146 | print("rename: truthy")
--------------------------------------------------------------------------------
167 | print("getcwdb: not iterable")
168 |
169 | try:
- for _ in os.readlink(sys.executable):
170 + for _ in pathlib.Path(sys.executable).readlink():
171 | print("readlink: iterable")
172 | break
173 | except TypeError as e:
note: This is an unsafe fix and may change runtime behavior

View File

@@ -3,11 +3,10 @@ use std::borrow::Cow;
use ruff_python_ast::PythonVersion;
use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range};
use ruff_python_codegen::Generator;
use ruff_python_semantic::{ResolvedReference, SemanticModel};
use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::rules::flake8_async::rules::blocking_open_call::is_open_call_from_pathlib;
use crate::{Applicability, Edit, Fix};
/// Format a code snippet to call `name.method()`.
@@ -120,13 +119,14 @@ impl OpenMode {
pub(super) struct FileOpen<'a> {
/// With item where the open happens, we use it for the reporting range.
pub(super) item: &'a ast::WithItem,
/// Filename expression used as the first argument in `open`, we use it in the diagnostic message.
pub(super) filename: &'a Expr,
/// The file open mode.
pub(super) mode: OpenMode,
/// The file open keywords.
pub(super) keywords: Vec<&'a ast::Keyword>,
/// We only check `open` operations whose file handles are used exactly once.
pub(super) reference: &'a ResolvedReference,
pub(super) argument: OpenArgument<'a>,
}
impl FileOpen<'_> {
@@ -137,45 +137,6 @@ impl FileOpen<'_> {
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum OpenArgument<'a> {
/// The filename argument to `open`, e.g. "foo.txt" in:
///
/// ```py
/// f = open("foo.txt")
/// ```
Builtin { filename: &'a Expr },
/// The `Path` receiver of a `pathlib.Path.open` call, e.g. the `p` in the
/// context manager in:
///
/// ```py
/// p = Path("foo.txt")
/// with p.open() as f: ...
/// ```
///
/// or `Path("foo.txt")` in
///
/// ```py
/// with Path("foo.txt").open() as f: ...
/// ```
Pathlib { path: &'a Expr },
}
impl OpenArgument<'_> {
pub(super) fn display<'src>(&self, source: &'src str) -> &'src str {
&source[self.range()]
}
}
impl Ranged for OpenArgument<'_> {
fn range(&self) -> TextRange {
match self {
OpenArgument::Builtin { filename } => filename.range(),
OpenArgument::Pathlib { path } => path.range(),
}
}
}
/// Find and return all `open` operations in the given `with` statement.
pub(super) fn find_file_opens<'a>(
with: &'a ast::StmtWith,
@@ -185,65 +146,10 @@ pub(super) fn find_file_opens<'a>(
) -> Vec<FileOpen<'a>> {
with.items
.iter()
.filter_map(|item| {
find_file_open(item, with, semantic, read_mode, python_version)
.or_else(|| find_path_open(item, with, semantic, read_mode, python_version))
})
.filter_map(|item| find_file_open(item, with, semantic, read_mode, python_version))
.collect()
}
fn resolve_file_open<'a>(
item: &'a ast::WithItem,
with: &'a ast::StmtWith,
semantic: &'a SemanticModel<'a>,
read_mode: bool,
mode: OpenMode,
keywords: Vec<&'a ast::Keyword>,
argument: OpenArgument<'a>,
) -> Option<FileOpen<'a>> {
match mode {
OpenMode::ReadText | OpenMode::ReadBytes => {
if !read_mode {
return None;
}
}
OpenMode::WriteText | OpenMode::WriteBytes => {
if read_mode {
return None;
}
}
}
if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() {
return None;
}
let var = item.optional_vars.as_deref()?.as_name_expr()?;
let scope = semantic.current_scope();
let binding = scope.get_all(var.id.as_str()).find_map(|id| {
let b = semantic.binding(id);
(b.range() == var.range()).then_some(b)
})?;
let references: Vec<&ResolvedReference> = binding
.references
.iter()
.map(|id| semantic.reference(*id))
.filter(|reference| with.range().contains_range(reference.range()))
.collect();
let [reference] = references.as_slice() else {
return None;
};
Some(FileOpen {
item,
mode,
keywords,
reference,
argument,
})
}
/// Find `open` operation in the given `with` item.
fn find_file_open<'a>(
item: &'a ast::WithItem,
@@ -259,6 +165,8 @@ fn find_file_open<'a>(
..
} = item.context_expr.as_call_expr()?;
let var = item.optional_vars.as_deref()?.as_name_expr()?;
// Ignore calls with `*args` and `**kwargs`. In the exact case of `open(*filename, mode="w")`,
// it could be a match; but in all other cases, the call _could_ contain unsupported keyword
// arguments, like `buffering`.
@@ -279,57 +187,58 @@ fn find_file_open<'a>(
let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?;
let mode = kw_mode.unwrap_or(pos_mode);
resolve_file_open(
item,
with,
semantic,
read_mode,
mode,
keywords,
OpenArgument::Builtin { filename },
)
}
fn find_path_open<'a>(
item: &'a ast::WithItem,
with: &'a ast::StmtWith,
semantic: &'a SemanticModel<'a>,
read_mode: bool,
python_version: PythonVersion,
) -> Option<FileOpen<'a>> {
let ast::ExprCall {
func,
arguments: ast::Arguments { args, keywords, .. },
..
} = item.context_expr.as_call_expr()?;
if args.iter().any(Expr::is_starred_expr)
|| keywords.iter().any(|keyword| keyword.arg.is_none())
{
match mode {
OpenMode::ReadText | OpenMode::ReadBytes => {
if !read_mode {
return None;
}
}
OpenMode::WriteText | OpenMode::WriteBytes => {
if read_mode {
return None;
}
}
}
// Path.read_bytes and Path.write_bytes do not support any kwargs.
if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() {
return None;
}
if !is_open_call_from_pathlib(func, semantic) {
// Now we need to find what is this variable bound to...
let scope = semantic.current_scope();
let bindings: Vec<BindingId> = scope.get_all(var.id.as_str()).collect();
let binding = bindings
.iter()
.map(|id| semantic.binding(*id))
// We might have many bindings with the same name, but we only care
// for the one we are looking at right now.
.find(|binding| binding.range() == var.range())?;
// Since many references can share the same binding, we can limit our attention span
// exclusively to the body of the current `with` statement.
let references: Vec<&ResolvedReference> = binding
.references
.iter()
.map(|id| semantic.reference(*id))
.filter(|reference| with.range().contains_range(reference.range()))
.collect();
// And even with all these restrictions, if the file handle gets used not exactly once,
// it doesn't fit the bill.
let [reference] = references.as_slice() else {
return None;
}
let attr = func.as_attribute_expr()?;
let mode = if args.is_empty() {
OpenMode::ReadText
} else {
match_open_mode(args.first()?)?
};
let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?;
let mode = kw_mode.unwrap_or(mode);
resolve_file_open(
Some(FileOpen {
item,
with,
semantic,
read_mode,
filename,
mode,
keywords,
OpenArgument::Pathlib {
path: attr.value.as_ref(),
},
)
reference,
})
}
/// Match positional arguments. Return expression for the file name and open mode.

View File

@@ -15,8 +15,7 @@ mod tests {
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
#[test_case(Rule::ReadWholeFile, Path::new("FURB101_0.py"))]
#[test_case(Rule::ReadWholeFile, Path::new("FURB101_1.py"))]
#[test_case(Rule::ReadWholeFile, Path::new("FURB101.py"))]
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
#[test_case(Rule::IfExpInsteadOfOrOperator, Path::new("FURB110.py"))]
#[test_case(Rule::ReimplementedOperator, Path::new("FURB118.py"))]
@@ -47,8 +46,7 @@ mod tests {
#[test_case(Rule::MetaClassABCMeta, Path::new("FURB180.py"))]
#[test_case(Rule::HashlibDigestHex, Path::new("FURB181.py"))]
#[test_case(Rule::ListReverseCopy, Path::new("FURB187.py"))]
#[test_case(Rule::WriteWholeFile, Path::new("FURB103_0.py"))]
#[test_case(Rule::WriteWholeFile, Path::new("FURB103_1.py"))]
#[test_case(Rule::WriteWholeFile, Path::new("FURB103.py"))]
#[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))]
#[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))]
#[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))]
@@ -67,7 +65,7 @@ mod tests {
#[test]
fn write_whole_file_python_39() -> Result<()> {
let diagnostics = test_path(
Path::new("refurb/FURB103_0.py"),
Path::new("refurb/FURB103.py"),
&settings::LinterSettings::for_rule(Rule::WriteWholeFile)
.with_target_version(PythonVersion::PY39),
)?;

View File

@@ -10,7 +10,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
use crate::importer::ImportRequest;
use crate::rules::refurb::helpers::{FileOpen, OpenArgument, find_file_opens};
use crate::rules::refurb::helpers::{FileOpen, find_file_opens};
use crate::{FixAvailability, Violation};
/// ## What it does
@@ -42,41 +42,27 @@ use crate::{FixAvailability, Violation};
/// - [Python documentation: `Path.read_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.read_text)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.1.2")]
pub(crate) struct ReadWholeFile<'a> {
pub(crate) struct ReadWholeFile {
filename: SourceCodeSnippet,
suggestion: SourceCodeSnippet,
argument: OpenArgument<'a>,
}
impl Violation for ReadWholeFile<'_> {
impl Violation for ReadWholeFile {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let filename = self.filename.truncated_display();
let suggestion = self.suggestion.truncated_display();
match self.argument {
OpenArgument::Pathlib { .. } => {
format!(
"`Path.open()` followed by `read()` can be replaced by `{filename}.{suggestion}`"
)
}
OpenArgument::Builtin { .. } => {
format!("`open` and `read` should be replaced by `Path({filename}).{suggestion}`")
}
}
format!("`open` and `read` should be replaced by `Path({filename}).{suggestion}`")
}
fn fix_title(&self) -> Option<String> {
let filename = self.filename.truncated_display();
let suggestion = self.suggestion.truncated_display();
match self.argument {
OpenArgument::Pathlib { .. } => Some(format!("Replace with `{filename}.{suggestion}`")),
OpenArgument::Builtin { .. } => {
Some(format!("Replace with `Path({filename}).{suggestion}`"))
}
}
Some(format!(
"Replace with `Path({}).{}`",
self.filename.truncated_display(),
self.suggestion.truncated_display(),
))
}
}
@@ -128,13 +114,13 @@ impl<'a> Visitor<'a> for ReadMatcher<'a, '_> {
.position(|open| open.is_ref(read_from))
{
let open = self.candidates.remove(open);
let filename_display = open.argument.display(self.checker.source());
let suggestion = make_suggestion(&open, self.checker.generator());
let mut diagnostic = self.checker.report_diagnostic(
ReadWholeFile {
filename: SourceCodeSnippet::from_str(filename_display),
filename: SourceCodeSnippet::from_str(
&self.checker.generator().expr(open.filename),
),
suggestion: SourceCodeSnippet::from_str(&suggestion),
argument: open.argument,
},
open.item.range(),
);
@@ -202,6 +188,8 @@ fn generate_fix(
let locator = checker.locator();
let filename_code = locator.slice(open.filename.range());
let (import_edit, binding) = checker
.importer()
.get_or_import_symbol(
@@ -218,15 +206,10 @@ fn generate_fix(
[Stmt::Assign(ast::StmtAssign { targets, value, .. })] if value.range() == expr.range() => {
match targets.as_slice() {
[Expr::Name(name)] => {
let target = match open.argument {
OpenArgument::Builtin { filename } => {
let filename_code = locator.slice(filename.range());
format!("{binding}({filename_code})")
}
OpenArgument::Pathlib { path } => locator.slice(path.range()).to_string(),
};
format!("{name} = {target}.{suggestion}", name = name.id)
format!(
"{name} = {binding}({filename_code}).{suggestion}",
name = name.id
)
}
_ => return None,
}
@@ -240,16 +223,8 @@ fn generate_fix(
}),
] if value.range() == expr.range() => match target.as_ref() {
Expr::Name(name) => {
let target = match open.argument {
OpenArgument::Builtin { filename } => {
let filename_code = locator.slice(filename.range());
format!("{binding}({filename_code})")
}
OpenArgument::Pathlib { path } => locator.slice(path.range()).to_string(),
};
format!(
"{var}: {ann} = {target}.{suggestion}",
"{var}: {ann} = {binding}({filename_code}).{suggestion}",
var = name.id,
ann = locator.slice(annotation.range())
)

View File

@@ -176,7 +176,7 @@ fn match_consecutive_appends<'a>(
let suite = if semantic.at_top_level() {
// If the statement is at the top level, we should go to the parent module.
// Module is available in the definitions list.
EnclosingSuite::new(semantic.definitions.python_ast()?, stmt.into())?
EnclosingSuite::new(semantic.definitions.python_ast()?, stmt)?
} else {
// Otherwise, go to the parent, and take its body as a sequence of siblings.
semantic

View File

@@ -9,7 +9,7 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
use crate::importer::ImportRequest;
use crate::rules::refurb::helpers::{FileOpen, OpenArgument, find_file_opens};
use crate::rules::refurb::helpers::{FileOpen, find_file_opens};
use crate::{FixAvailability, Locator, Violation};
/// ## What it does
@@ -42,40 +42,26 @@ use crate::{FixAvailability, Locator, Violation};
/// - [Python documentation: `Path.write_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_text)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "v0.3.6")]
pub(crate) struct WriteWholeFile<'a> {
pub(crate) struct WriteWholeFile {
filename: SourceCodeSnippet,
suggestion: SourceCodeSnippet,
argument: OpenArgument<'a>,
}
impl Violation for WriteWholeFile<'_> {
impl Violation for WriteWholeFile {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let filename = self.filename.truncated_display();
let suggestion = self.suggestion.truncated_display();
match self.argument {
OpenArgument::Pathlib { .. } => {
format!(
"`Path.open()` followed by `write()` can be replaced by `{filename}.{suggestion}`"
)
}
OpenArgument::Builtin { .. } => {
format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`")
}
}
format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`")
}
fn fix_title(&self) -> Option<String> {
let filename = self.filename.truncated_display();
let suggestion = self.suggestion.truncated_display();
match self.argument {
OpenArgument::Pathlib { .. } => Some(format!("Replace with `{filename}.{suggestion}`")),
OpenArgument::Builtin { .. } => {
Some(format!("Replace with `Path({filename}).{suggestion}`"))
}
}
Some(format!(
"Replace with `Path({}).{}`",
self.filename.truncated_display(),
self.suggestion.truncated_display(),
))
}
}
@@ -139,15 +125,16 @@ impl<'a> Visitor<'a> for WriteMatcher<'a, '_> {
.position(|open| open.is_ref(write_to))
{
let open = self.candidates.remove(open);
if self.loop_counter == 0 {
let filename_display = open.argument.display(self.checker.source());
let suggestion = make_suggestion(&open, content, self.checker.locator());
let mut diagnostic = self.checker.report_diagnostic(
WriteWholeFile {
filename: SourceCodeSnippet::from_str(filename_display),
filename: SourceCodeSnippet::from_str(
&self.checker.generator().expr(open.filename),
),
suggestion: SourceCodeSnippet::from_str(&suggestion),
argument: open.argument,
},
open.item.range(),
);
@@ -211,6 +198,7 @@ fn generate_fix(
}
let locator = checker.locator();
let filename_code = locator.slice(open.filename.range());
let (import_edit, binding) = checker
.importer()
@@ -221,15 +209,7 @@ fn generate_fix(
)
.ok()?;
let target = match open.argument {
OpenArgument::Builtin { filename } => {
let filename_code = locator.slice(filename.range());
format!("{binding}({filename_code})")
}
OpenArgument::Pathlib { path } => locator.slice(path.range()).to_string(),
};
let replacement = format!("{target}.{suggestion}");
let replacement = format!("{binding}({filename_code}).{suggestion}");
let applicability = if checker.comment_ranges().intersects(with_stmt.range()) {
Applicability::Unsafe

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text()`
--> FURB101_0.py:12:6
--> FURB101.py:12:6
|
11 | # FURB101
12 | with open("file.txt") as f:
@@ -26,7 +26,7 @@ help: Replace with `Path("file.txt").read_text()`
16 | with open("file.txt", "rb") as f:
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_bytes()`
--> FURB101_0.py:16:6
--> FURB101.py:16:6
|
15 | # FURB101
16 | with open("file.txt", "rb") as f:
@@ -50,7 +50,7 @@ help: Replace with `Path("file.txt").read_bytes()`
20 | with open("file.txt", mode="rb") as f:
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_bytes()`
--> FURB101_0.py:20:6
--> FURB101.py:20:6
|
19 | # FURB101
20 | with open("file.txt", mode="rb") as f:
@@ -74,7 +74,7 @@ help: Replace with `Path("file.txt").read_bytes()`
24 | with open("file.txt", encoding="utf8") as f:
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf8")`
--> FURB101_0.py:24:6
--> FURB101.py:24:6
|
23 | # FURB101
24 | with open("file.txt", encoding="utf8") as f:
@@ -98,7 +98,7 @@ help: Replace with `Path("file.txt").read_text(encoding="utf8")`
28 | with open("file.txt", errors="ignore") as f:
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(errors="ignore")`
--> FURB101_0.py:28:6
--> FURB101.py:28:6
|
27 | # FURB101
28 | with open("file.txt", errors="ignore") as f:
@@ -122,7 +122,7 @@ help: Replace with `Path("file.txt").read_text(errors="ignore")`
32 | with open("file.txt", mode="r") as f: # noqa: FURB120
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text()`
--> FURB101_0.py:32:6
--> FURB101.py:32:6
|
31 | # FURB101
32 | with open("file.txt", mode="r") as f: # noqa: FURB120
@@ -147,7 +147,7 @@ help: Replace with `Path("file.txt").read_text()`
note: This is an unsafe fix and may change runtime behavior
FURB101 `open` and `read` should be replaced by `Path(foo()).read_bytes()`
--> FURB101_0.py:36:6
--> FURB101.py:36:6
|
35 | # FURB101
36 | with open(foo(), "rb") as f:
@@ -158,7 +158,7 @@ FURB101 `open` and `read` should be replaced by `Path(foo()).read_bytes()`
help: Replace with `Path(foo()).read_bytes()`
FURB101 `open` and `read` should be replaced by `Path("a.txt").read_text()`
--> FURB101_0.py:44:6
--> FURB101.py:44:6
|
43 | # FURB101
44 | with open("a.txt") as a, open("b.txt", "rb") as b:
@@ -169,7 +169,7 @@ FURB101 `open` and `read` should be replaced by `Path("a.txt").read_text()`
help: Replace with `Path("a.txt").read_text()`
FURB101 `open` and `read` should be replaced by `Path("b.txt").read_bytes()`
--> FURB101_0.py:44:26
--> FURB101.py:44:26
|
43 | # FURB101
44 | with open("a.txt") as a, open("b.txt", "rb") as b:
@@ -180,7 +180,7 @@ FURB101 `open` and `read` should be replaced by `Path("b.txt").read_bytes()`
help: Replace with `Path("b.txt").read_bytes()`
FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()`
--> FURB101_0.py:49:18
--> FURB101.py:49:18
|
48 | # FURB101
49 | with foo() as a, open("file.txt") as b, foo() as c:
@@ -191,7 +191,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()`
help: Replace with `Path("file.txt").read_text()`
FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")`
--> FURB101_0.py:130:6
--> FURB101.py:130:6
|
129 | # FURB101
130 | with open("file.txt", encoding="utf-8") as f:
@@ -215,7 +215,7 @@ help: Replace with `Path("file.txt").read_text(encoding="utf-8")`
134 | with open("file.txt", encoding="utf-8") as f:
FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")`
--> FURB101_0.py:134:6
--> FURB101.py:134:6
|
133 | # FURB101 but no fix because it would remove the assignment to `x`
134 | with open("file.txt", encoding="utf-8") as f:
@@ -225,7 +225,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(enco
help: Replace with `Path("file.txt").read_text(encoding="utf-8")`
FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")`
--> FURB101_0.py:138:6
--> FURB101.py:138:6
|
137 | # FURB101 but no fix because it would remove the `process_contents` call
138 | with open("file.txt", encoding="utf-8") as f:
@@ -234,13 +234,13 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(enco
|
help: Replace with `Path("file.txt").read_text(encoding="utf-8")`
FURB101 `open` and `read` should be replaced by `Path("file1.txt").read_text(encoding="utf-8")`
--> FURB101_0.py:141:6
FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")`
--> FURB101.py:141:6
|
139 | contents = process_contents(f.read())
140 |
141 | with open("file1.txt", encoding="utf-8") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
141 | with open("file.txt", encoding="utf-8") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
142 | contents: str = process_contents(f.read())
|
help: Replace with `Path("file1.txt").read_text(encoding="utf-8")`
help: Replace with `Path("file.txt").read_text(encoding="utf-8")`

View File

@@ -1,39 +0,0 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB101 [*] `Path.open()` followed by `read()` can be replaced by `Path("file.txt").read_text()`
--> FURB101_1.py:4:6
|
2 | from pathlib import Path
3 |
4 | with Path("file.txt").open() as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 | contents = f.read()
|
help: Replace with `Path("file.txt").read_text()`
1 |
2 | from pathlib import Path
3 |
- with Path("file.txt").open() as f:
- contents = f.read()
4 + contents = Path("file.txt").read_text()
5 |
6 | with Path("file.txt").open("r") as f:
7 | contents = f.read()
FURB101 [*] `Path.open()` followed by `read()` can be replaced by `Path("file.txt").read_text()`
--> FURB101_1.py:7:6
|
5 | contents = f.read()
6 |
7 | with Path("file.txt").open("r") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 | contents = f.read()
|
help: Replace with `Path("file.txt").read_text()`
4 | with Path("file.txt").open() as f:
5 | contents = f.read()
6 |
- with Path("file.txt").open("r") as f:
- contents = f.read()
7 + contents = Path("file.txt").read_text()

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("test")`
--> FURB103_0.py:12:6
--> FURB103.py:12:6
|
11 | # FURB103
12 | with open("file.txt", "w") as f:
@@ -26,7 +26,7 @@ help: Replace with `Path("file.txt").write_text("test")`
16 | with open("file.txt", "wb") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)`
--> FURB103_0.py:16:6
--> FURB103.py:16:6
|
15 | # FURB103
16 | with open("file.txt", "wb") as f:
@@ -50,7 +50,7 @@ help: Replace with `Path("file.txt").write_bytes(foobar)`
20 | with open("file.txt", mode="wb") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(b"abc")`
--> FURB103_0.py:20:6
--> FURB103.py:20:6
|
19 | # FURB103
20 | with open("file.txt", mode="wb") as f:
@@ -74,7 +74,7 @@ help: Replace with `Path("file.txt").write_bytes(b"abc")`
24 | with open("file.txt", "w", encoding="utf8") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, encoding="utf8")`
--> FURB103_0.py:24:6
--> FURB103.py:24:6
|
23 | # FURB103
24 | with open("file.txt", "w", encoding="utf8") as f:
@@ -98,7 +98,7 @@ help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")`
28 | with open("file.txt", "w", errors="ignore") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, errors="ignore")`
--> FURB103_0.py:28:6
--> FURB103.py:28:6
|
27 | # FURB103
28 | with open("file.txt", "w", errors="ignore") as f:
@@ -122,7 +122,7 @@ help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")`
32 | with open("file.txt", mode="w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)`
--> FURB103_0.py:32:6
--> FURB103.py:32:6
|
31 | # FURB103
32 | with open("file.txt", mode="w") as f:
@@ -146,7 +146,7 @@ help: Replace with `Path("file.txt").write_text(foobar)`
36 | with open(foo(), "wb") as f:
FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())`
--> FURB103_0.py:36:6
--> FURB103.py:36:6
|
35 | # FURB103
36 | with open(foo(), "wb") as f:
@@ -157,7 +157,7 @@ FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())
help: Replace with `Path(foo()).write_bytes(bar())`
FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)`
--> FURB103_0.py:44:6
--> FURB103.py:44:6
|
43 | # FURB103
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b:
@@ -168,7 +168,7 @@ FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)`
help: Replace with `Path("a.txt").write_text(x)`
FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)`
--> FURB103_0.py:44:31
--> FURB103.py:44:31
|
43 | # FURB103
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b:
@@ -179,7 +179,7 @@ FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)`
help: Replace with `Path("b.txt").write_bytes(y)`
FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(bar(bar(a + x)))`
--> FURB103_0.py:49:18
--> FURB103.py:49:18
|
48 | # FURB103
49 | with foo() as a, open("file.txt", "w") as b, foo() as c:
@@ -190,7 +190,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba
help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))`
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")`
--> FURB103_0.py:58:6
--> FURB103.py:58:6
|
57 | # FURB103
58 | with open("file.txt", "w", newline="\r\n") as f:
@@ -214,7 +214,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")`
62 | import builtins
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")`
--> FURB103_0.py:66:6
--> FURB103.py:66:6
|
65 | # FURB103
66 | with builtins.open("file.txt", "w", newline="\r\n") as f:
@@ -237,7 +237,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")`
70 | from builtins import open as o
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")`
--> FURB103_0.py:74:6
--> FURB103.py:74:6
|
73 | # FURB103
74 | with o("file.txt", "w", newline="\r\n") as f:
@@ -260,7 +260,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")`
78 |
FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....`
--> FURB103_0.py:154:6
--> FURB103.py:154:6
|
152 | data = {"price": 100}
153 |
@@ -284,7 +284,7 @@ help: Replace with `Path("test.json")....`
158 | with open("tmp_path/pyproject.toml", "w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....`
--> FURB103_0.py:158:6
--> FURB103.py:158:6
|
157 | # See: https://github.com/astral-sh/ruff/issues/21381
158 | with open("tmp_path/pyproject.toml", "w") as f:

View File

@@ -1,157 +0,0 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")`
--> FURB103_1.py:3:6
|
1 | from pathlib import Path
2 |
3 | with Path("file.txt").open("w") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 | f.write("test")
|
help: Replace with `Path("file.txt").write_text("test")`
1 | from pathlib import Path
2 |
- with Path("file.txt").open("w") as f:
- f.write("test")
3 + Path("file.txt").write_text("test")
4 |
5 | with Path("file.txt").open("wb") as f:
6 | f.write(b"test")
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_bytes(b"test")`
--> FURB103_1.py:6:6
|
4 | f.write("test")
5 |
6 | with Path("file.txt").open("wb") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7 | f.write(b"test")
|
help: Replace with `Path("file.txt").write_bytes(b"test")`
3 | with Path("file.txt").open("w") as f:
4 | f.write("test")
5 |
- with Path("file.txt").open("wb") as f:
- f.write(b"test")
6 + Path("file.txt").write_bytes(b"test")
7 |
8 | with Path("file.txt").open(mode="w") as f:
9 | f.write("test")
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")`
--> FURB103_1.py:9:6
|
7 | f.write(b"test")
8 |
9 | with Path("file.txt").open(mode="w") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10 | f.write("test")
|
help: Replace with `Path("file.txt").write_text("test")`
6 | with Path("file.txt").open("wb") as f:
7 | f.write(b"test")
8 |
- with Path("file.txt").open(mode="w") as f:
- f.write("test")
9 + Path("file.txt").write_text("test")
10 |
11 | with Path("file.txt").open("w", encoding="utf8") as f:
12 | f.write("test")
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", encoding="utf8")`
--> FURB103_1.py:12:6
|
10 | f.write("test")
11 |
12 | with Path("file.txt").open("w", encoding="utf8") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13 | f.write("test")
|
help: Replace with `Path("file.txt").write_text("test", encoding="utf8")`
9 | with Path("file.txt").open(mode="w") as f:
10 | f.write("test")
11 |
- with Path("file.txt").open("w", encoding="utf8") as f:
- f.write("test")
12 + Path("file.txt").write_text("test", encoding="utf8")
13 |
14 | with Path("file.txt").open("w", errors="ignore") as f:
15 | f.write("test")
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", errors="ignore")`
--> FURB103_1.py:15:6
|
13 | f.write("test")
14 |
15 | with Path("file.txt").open("w", errors="ignore") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16 | f.write("test")
|
help: Replace with `Path("file.txt").write_text("test", errors="ignore")`
12 | with Path("file.txt").open("w", encoding="utf8") as f:
13 | f.write("test")
14 |
- with Path("file.txt").open("w", errors="ignore") as f:
- f.write("test")
15 + Path("file.txt").write_text("test", errors="ignore")
16 |
17 | with Path(foo()).open("w") as f:
18 | f.write("test")
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path(foo()).write_text("test")`
--> FURB103_1.py:18:6
|
16 | f.write("test")
17 |
18 | with Path(foo()).open("w") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
19 | f.write("test")
|
help: Replace with `Path(foo()).write_text("test")`
15 | with Path("file.txt").open("w", errors="ignore") as f:
16 | f.write("test")
17 |
- with Path(foo()).open("w") as f:
- f.write("test")
18 + Path(foo()).write_text("test")
19 |
20 | p = Path("file.txt")
21 | with p.open("w") as f:
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `p.write_text("test")`
--> FURB103_1.py:22:6
|
21 | p = Path("file.txt")
22 | with p.open("w") as f:
| ^^^^^^^^^^^^^^^^
23 | f.write("test")
|
help: Replace with `p.write_text("test")`
19 | f.write("test")
20 |
21 | p = Path("file.txt")
- with p.open("w") as f:
- f.write("test")
22 + p.write_text("test")
23 |
24 | with Path("foo", "bar", "baz").open("w") as f:
25 | f.write("test")
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("foo", "bar", "baz").write_text("test")`
--> FURB103_1.py:25:6
|
23 | f.write("test")
24 |
25 | with Path("foo", "bar", "baz").open("w") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 | f.write("test")
|
help: Replace with `Path("foo", "bar", "baz").write_text("test")`
22 | with p.open("w") as f:
23 | f.write("test")
24 |
- with Path("foo", "bar", "baz").open("w") as f:
- f.write("test")
25 + Path("foo", "bar", "baz").write_text("test")

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("test")`
--> FURB103_0.py:12:6
--> FURB103.py:12:6
|
11 | # FURB103
12 | with open("file.txt", "w") as f:
@@ -26,7 +26,7 @@ help: Replace with `Path("file.txt").write_text("test")`
16 | with open("file.txt", "wb") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)`
--> FURB103_0.py:16:6
--> FURB103.py:16:6
|
15 | # FURB103
16 | with open("file.txt", "wb") as f:
@@ -50,7 +50,7 @@ help: Replace with `Path("file.txt").write_bytes(foobar)`
20 | with open("file.txt", mode="wb") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(b"abc")`
--> FURB103_0.py:20:6
--> FURB103.py:20:6
|
19 | # FURB103
20 | with open("file.txt", mode="wb") as f:
@@ -74,7 +74,7 @@ help: Replace with `Path("file.txt").write_bytes(b"abc")`
24 | with open("file.txt", "w", encoding="utf8") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, encoding="utf8")`
--> FURB103_0.py:24:6
--> FURB103.py:24:6
|
23 | # FURB103
24 | with open("file.txt", "w", encoding="utf8") as f:
@@ -98,7 +98,7 @@ help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")`
28 | with open("file.txt", "w", errors="ignore") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, errors="ignore")`
--> FURB103_0.py:28:6
--> FURB103.py:28:6
|
27 | # FURB103
28 | with open("file.txt", "w", errors="ignore") as f:
@@ -122,7 +122,7 @@ help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")`
32 | with open("file.txt", mode="w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)`
--> FURB103_0.py:32:6
--> FURB103.py:32:6
|
31 | # FURB103
32 | with open("file.txt", mode="w") as f:
@@ -146,7 +146,7 @@ help: Replace with `Path("file.txt").write_text(foobar)`
36 | with open(foo(), "wb") as f:
FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())`
--> FURB103_0.py:36:6
--> FURB103.py:36:6
|
35 | # FURB103
36 | with open(foo(), "wb") as f:
@@ -157,7 +157,7 @@ FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())
help: Replace with `Path(foo()).write_bytes(bar())`
FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)`
--> FURB103_0.py:44:6
--> FURB103.py:44:6
|
43 | # FURB103
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b:
@@ -168,7 +168,7 @@ FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)`
help: Replace with `Path("a.txt").write_text(x)`
FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)`
--> FURB103_0.py:44:31
--> FURB103.py:44:31
|
43 | # FURB103
44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b:
@@ -179,7 +179,7 @@ FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)`
help: Replace with `Path("b.txt").write_bytes(y)`
FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(bar(bar(a + x)))`
--> FURB103_0.py:49:18
--> FURB103.py:49:18
|
48 | # FURB103
49 | with foo() as a, open("file.txt", "w") as b, foo() as c:
@@ -190,7 +190,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba
help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))`
FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....`
--> FURB103_0.py:154:6
--> FURB103.py:154:6
|
152 | data = {"price": 100}
153 |
@@ -214,7 +214,7 @@ help: Replace with `Path("test.json")....`
158 | with open("tmp_path/pyproject.toml", "w") as f:
FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....`
--> FURB103_0.py:158:6
--> FURB103.py:158:6
|
157 | # See: https://github.com/astral-sh/ruff/issues/21381
158 | with open("tmp_path/pyproject.toml", "w") as f:

View File

@@ -313,20 +313,12 @@ mod tests {
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
Rule::InvalidRuleCode,
Rule::InvalidSuppressionComment,
Rule::UnmatchedSuppressionComment,
])
.with_external_rules(&["TK421"]),
]),
&settings::LinterSettings::for_rules(vec![
Rule::UnusedVariable,
Rule::AmbiguousVariableName,
Rule::UnusedNOQA,
Rule::InvalidRuleCode,
Rule::InvalidSuppressionComment,
Rule::UnmatchedSuppressionComment,
])
.with_external_rules(&["TK421"])
.with_preview_mode(),
);
Ok(())

View File

@@ -9,21 +9,6 @@ use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
use crate::{AlwaysFixableViolation, Edit, Fix};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InvalidRuleCodeKind {
Noqa,
Suppression,
}
impl InvalidRuleCodeKind {
fn as_str(&self) -> &str {
match self {
InvalidRuleCodeKind::Noqa => "`# noqa`",
InvalidRuleCodeKind::Suppression => "suppression",
}
}
}
/// ## What it does
/// Checks for `noqa` codes that are invalid.
///
@@ -51,17 +36,12 @@ impl InvalidRuleCodeKind {
#[violation_metadata(preview_since = "0.11.4")]
pub(crate) struct InvalidRuleCode {
pub(crate) rule_code: String,
pub(crate) kind: InvalidRuleCodeKind,
}
impl AlwaysFixableViolation for InvalidRuleCode {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Invalid rule code in {}: {}",
self.kind.as_str(),
self.rule_code
)
format!("Invalid rule code in `# noqa`: {}", self.rule_code)
}
fn fix_title(&self) -> String {
@@ -81,9 +61,7 @@ pub(crate) fn invalid_noqa_code(
continue;
};
let all_valid = directive
.iter()
.all(|code| code_is_valid(code.as_str(), external));
let all_valid = directive.iter().all(|code| code_is_valid(code, external));
if all_valid {
continue;
@@ -91,7 +69,7 @@ pub(crate) fn invalid_noqa_code(
let (valid_codes, invalid_codes): (Vec<_>, Vec<_>) = directive
.iter()
.partition(|&code| code_is_valid(code.as_str(), external));
.partition(|&code| code_is_valid(code, external));
if valid_codes.is_empty() {
all_codes_invalid_diagnostic(directive, invalid_codes, context);
@@ -103,9 +81,10 @@ pub(crate) fn invalid_noqa_code(
}
}
pub(crate) fn code_is_valid(code: &str, external: &[String]) -> bool {
Rule::from_code(get_redirect_target(code).unwrap_or(code)).is_ok()
|| external.iter().any(|ext| code.starts_with(ext))
fn code_is_valid(code: &Code, external: &[String]) -> bool {
let code_str = code.as_str();
Rule::from_code(get_redirect_target(code_str).unwrap_or(code_str)).is_ok()
|| external.iter().any(|ext| code_str.starts_with(ext))
}
fn all_codes_invalid_diagnostic(
@@ -121,7 +100,6 @@ fn all_codes_invalid_diagnostic(
.map(Code::as_str)
.collect::<Vec<_>>()
.join(", "),
kind: InvalidRuleCodeKind::Noqa,
},
directive.range(),
)
@@ -138,7 +116,6 @@ fn some_codes_are_invalid_diagnostic(
.report_diagnostic(
InvalidRuleCode {
rule_code: invalid_code.to_string(),
kind: InvalidRuleCodeKind::Noqa,
},
invalid_code.range(),
)

View File

@@ -1,59 +0,0 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::AlwaysFixableViolation;
use crate::suppression::{InvalidSuppressionKind, ParseErrorKind};
/// ## What it does
/// Checks for invalid suppression comments
///
/// ## Why is this bad?
/// Invalid suppression comments are ignored by Ruff, and should either
/// be fixed or removed to avoid confusion.
///
/// ## Example
/// ```python
/// ruff: disable # missing codes
/// ```
///
/// Use instead:
/// ```python
/// # ruff: disable[E501]
/// ```
///
/// Or delete the invalid suppression comment.
///
/// ## References
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.11")]
pub(crate) struct InvalidSuppressionComment {
pub(crate) kind: InvalidSuppressionCommentKind,
}
impl AlwaysFixableViolation for InvalidSuppressionComment {
#[derive_message_formats]
fn message(&self) -> String {
let msg = match self.kind {
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Indentation) => {
"unexpected indentation".to_string()
}
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Trailing) => {
"trailing comments are not supported".to_string()
}
InvalidSuppressionCommentKind::Invalid(InvalidSuppressionKind::Unmatched) => {
"no matching 'disable' comment".to_string()
}
InvalidSuppressionCommentKind::Error(error) => format!("{error}"),
};
format!("Invalid suppression comment: {msg}")
}
fn fix_title(&self) -> String {
"Remove suppression comment".to_string()
}
}
pub(crate) enum InvalidSuppressionCommentKind {
Invalid(InvalidSuppressionKind),
Error(ParseErrorKind),
}

View File

@@ -22,7 +22,6 @@ pub(crate) use invalid_formatter_suppression_comment::*;
pub(crate) use invalid_index_type::*;
pub(crate) use invalid_pyproject_toml::*;
pub(crate) use invalid_rule_code::*;
pub(crate) use invalid_suppression_comment::*;
pub(crate) use legacy_form_pytest_raises::*;
pub(crate) use logging_eager_conversion::*;
pub(crate) use map_int_version_parsing::*;
@@ -47,7 +46,6 @@ pub(crate) use starmap_zip::*;
pub(crate) use static_key_dict_comprehension::*;
#[cfg(any(feature = "test-rules", test))]
pub(crate) use test_rules::*;
pub(crate) use unmatched_suppression_comment::*;
pub(crate) use unnecessary_cast_to_int::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
@@ -89,7 +87,6 @@ mod invalid_formatter_suppression_comment;
mod invalid_index_type;
mod invalid_pyproject_toml;
mod invalid_rule_code;
mod invalid_suppression_comment;
mod legacy_form_pytest_raises;
mod logging_eager_conversion;
mod map_int_version_parsing;
@@ -116,7 +113,6 @@ mod static_key_dict_comprehension;
mod suppression_comment_visitor;
#[cfg(any(feature = "test-rules", test))]
pub(crate) mod test_rules;
mod unmatched_suppression_comment;
mod unnecessary_cast_to_int;
mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;

View File

@@ -1,42 +0,0 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::Violation;
/// ## What it does
/// Checks for unmatched range suppression comments
///
/// ## Why is this bad?
/// Unmatched range suppression comments can inadvertently suppress violations
/// over larger sections of code than intended, particularly at module scope.
///
/// ## Example
/// ```python
/// def foo():
/// # ruff: disable[E501] # unmatched
/// REALLY_LONG_VALUES = [...]
///
/// print(REALLY_LONG_VALUES)
/// ```
///
/// Use instead:
/// ```python
/// def foo():
/// # ruff: disable[E501]
/// REALLY_LONG_VALUES = [...]
/// # ruff: enable[E501]
///
/// print(REALLY_LONG_VALUES)
/// ```
///
/// ## References
/// - [Ruff error suppression](https://docs.astral.sh/ruff/linter/#error-suppression)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.11")]
pub(crate) struct UnmatchedSuppressionComment;
impl Violation for UnmatchedSuppressionComment {
#[derive_message_formats]
fn message(&self) -> String {
"Suppression comment without matching `#ruff:enable` comment".to_string()
}
}

View File

@@ -6,8 +6,8 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs
+linter.preview = enabled
--- Summary ---
Removed: 15
Added: 23
Removed: 14
Added: 11
--- Removed ---
E741 Ambiguous variable name: `I`
@@ -238,60 +238,8 @@ help: Remove assignment to unused variable `I`
note: This is an unsafe fix and may change runtime behavior
F841 [*] Local variable `value` is assigned to but never used
--> suppressions.py:95:5
|
93 | # ruff: disable[YF829]
94 | # ruff: disable[F841, RQW320]
95 | value = 0
| ^^^^^
96 | # ruff: enable[F841, RQW320]
97 | # ruff: enable[YF829]
|
help: Remove assignment to unused variable `value`
92 | # Unknown rule codes
93 | # ruff: disable[YF829]
94 | # ruff: disable[F841, RQW320]
- value = 0
95 + pass
96 | # ruff: enable[F841, RQW320]
97 | # ruff: enable[YF829]
98 |
note: This is an unsafe fix and may change runtime behavior
--- Added ---
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:11:5
|
9 | # These should both be ignored by the implicit range suppression.
10 | # Should also generate an "unmatched suppression" warning.
11 | # ruff:disable[E741,F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
12 | I = 1
|
RUF103 [*] Invalid suppression comment: no matching 'disable' comment
--> suppressions.py:19:5
|
17 | # should be generated.
18 | I = 1
19 | # ruff: enable[E741, F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Remove suppression comment
16 | # Neither warning is ignored, and an "unmatched suppression"
17 | # should be generated.
18 | I = 1
- # ruff: enable[E741, F841]
19 |
20 |
21 | def f():
note: This is an unsafe fix and may change runtime behavior
RUF100 [*] Unused suppression (non-enabled: `E501`)
--> suppressions.py:46:5
|
@@ -350,17 +298,6 @@ help: Remove unused `noqa` directive
58 |
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:61:5
|
59 | def f():
60 | # TODO: Duplicate codes should be counted as duplicate, not unused
61 | # ruff: disable[F841, F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 | foo = 0
|
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:61:21
|
@@ -381,18 +318,6 @@ help: Remove unused suppression
64 |
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:68:5
|
66 | # Overlapping range suppressions, one should be marked as used,
67 | # and the other should trigger an unused suppression diagnostic
68 | # ruff: disable[F841]
| ^^^^^^^^^^^^^^^^^^^^^
69 | # ruff: disable[F841]
70 | foo = 0
|
RUF100 [*] Unused suppression (unused: `F841`)
--> suppressions.py:69:5
|
@@ -412,17 +337,6 @@ help: Remove unused suppression
71 |
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:75:5
|
73 | def f():
74 | # Multiple codes but only one is used
75 | # ruff: disable[E741, F401, F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
76 | foo = 0
|
RUF100 [*] Unused suppression (unused: `E741`)
--> suppressions.py:75:21
|
@@ -463,17 +377,6 @@ help: Remove unused suppression
78 |
RUF104 Suppression comment without matching `#ruff:enable` comment
--> suppressions.py:81:5
|
79 | def f():
80 | # Multiple codes but only two are used
81 | # ruff: disable[E741, F401, F841]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 | I = 0
|
RUF100 [*] Unused suppression (non-enabled: `F401`)
--> suppressions.py:81:27
|
@@ -510,8 +413,6 @@ help: Remove unused suppression
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[F401, F841]
88 | print("hello")
89 |
90 |
RUF100 [*] Unused suppression (non-enabled: `F401`)
@@ -530,8 +431,6 @@ help: Remove unused suppression
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[E741, F841]
88 | print("hello")
89 |
90 |
RUF100 [*] Unused suppression (unused: `F841`)
@@ -550,122 +449,3 @@ help: Remove unused suppression
- # ruff: disable[E741, F401, F841]
87 + # ruff: disable[E741, F401]
88 | print("hello")
89 |
90 |
RUF102 [*] Invalid rule code in suppression: YF829
--> suppressions.py:93:21
|
91 | def f():
92 | # Unknown rule codes
93 | # ruff: disable[YF829]
| ^^^^^
94 | # ruff: disable[F841, RQW320]
95 | value = 0
|
help: Remove the rule code
90 |
91 | def f():
92 | # Unknown rule codes
- # ruff: disable[YF829]
93 | # ruff: disable[F841, RQW320]
94 | value = 0
95 | # ruff: enable[F841, RQW320]
RUF102 [*] Invalid rule code in suppression: RQW320
--> suppressions.py:94:27
|
92 | # Unknown rule codes
93 | # ruff: disable[YF829]
94 | # ruff: disable[F841, RQW320]
| ^^^^^^
95 | value = 0
96 | # ruff: enable[F841, RQW320]
|
help: Remove the rule code
91 | def f():
92 | # Unknown rule codes
93 | # ruff: disable[YF829]
- # ruff: disable[F841, RQW320]
94 + # ruff: disable[F841]
95 | value = 0
96 | # ruff: enable[F841, RQW320]
97 | # ruff: enable[YF829]
RUF102 [*] Invalid rule code in suppression: RQW320
--> suppressions.py:96:26
|
94 | # ruff: disable[F841, RQW320]
95 | value = 0
96 | # ruff: enable[F841, RQW320]
| ^^^^^^
97 | # ruff: enable[YF829]
|
help: Remove the rule code
93 | # ruff: disable[YF829]
94 | # ruff: disable[F841, RQW320]
95 | value = 0
- # ruff: enable[F841, RQW320]
96 + # ruff: enable[F841]
97 | # ruff: enable[YF829]
98 |
99 |
RUF102 [*] Invalid rule code in suppression: YF829
--> suppressions.py:97:20
|
95 | value = 0
96 | # ruff: enable[F841, RQW320]
97 | # ruff: enable[YF829]
| ^^^^^
|
help: Remove the rule code
94 | # ruff: disable[F841, RQW320]
95 | value = 0
96 | # ruff: enable[F841, RQW320]
- # ruff: enable[YF829]
97 |
98 |
99 | def f():
RUF103 [*] Invalid suppression comment: missing suppression codes like `[E501, ...]`
--> suppressions.py:109:5
|
107 | def f():
108 | # Empty or missing rule codes
109 | # ruff: disable
| ^^^^^^^^^^^^^^^
110 | # ruff: disable[]
111 | print("hello")
|
help: Remove suppression comment
106 |
107 | def f():
108 | # Empty or missing rule codes
- # ruff: disable
109 | # ruff: disable[]
110 | print("hello")
note: This is an unsafe fix and may change runtime behavior
RUF103 [*] Invalid suppression comment: missing suppression codes like `[E501, ...]`
--> suppressions.py:110:5
|
108 | # Empty or missing rule codes
109 | # ruff: disable
110 | # ruff: disable[]
| ^^^^^^^^^^^^^^^^^
111 | print("hello")
|
help: Remove suppression comment
107 | def f():
108 | # Empty or missing rule codes
109 | # ruff: disable
- # ruff: disable[]
110 | print("hello")
note: This is an unsafe fix and may change runtime behavior

View File

@@ -471,13 +471,6 @@ impl LinterSettings {
self
}
#[must_use]
pub fn with_external_rules(mut self, rules: &[&str]) -> Self {
self.external
.extend(rules.iter().map(std::string::ToString::to_string));
self
}
/// Resolve the [`TargetVersion`] to use for linting.
///
/// This method respects the per-file version overrides in

View File

@@ -4,7 +4,6 @@ use ruff_db::diagnostic::Diagnostic;
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::whitespace::indentation;
use rustc_hash::FxHashSet;
use std::cell::Cell;
use std::{error::Error, fmt::Formatter};
use thiserror::Error;
@@ -18,11 +17,7 @@ use crate::checkers::ast::LintContext;
use crate::codes::Rule;
use crate::fix::edits::delete_comment;
use crate::preview::is_range_suppressions_enabled;
use crate::rule_redirects::get_redirect_target;
use crate::rules::ruff::rules::{
InvalidRuleCode, InvalidRuleCodeKind, InvalidSuppressionComment, InvalidSuppressionCommentKind,
UnmatchedSuppressionComment, UnusedCodes, UnusedNOQA, UnusedNOQAKind, code_is_valid,
};
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA, UnusedNOQAKind};
use crate::settings::LinterSettings;
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -135,7 +130,7 @@ impl Suppressions {
}
pub(crate) fn is_empty(&self) -> bool {
self.valid.is_empty() && self.invalid.is_empty() && self.errors.is_empty()
self.valid.is_empty()
}
/// Check if a diagnostic is suppressed by any known range suppressions
@@ -155,9 +150,7 @@ impl Suppressions {
};
for suppression in &self.valid {
let suppression_code =
get_redirect_target(suppression.code.as_str()).unwrap_or(suppression.code.as_str());
if *code == suppression_code && suppression.range.contains_range(range) {
if *code == suppression.code.as_str() && suppression.range.contains_range(range) {
suppression.used.set(true);
return true;
}
@@ -166,140 +159,81 @@ impl Suppressions {
}
pub(crate) fn check_suppressions(&self, context: &LintContext, locator: &Locator) {
let mut unmatched_ranges = FxHashSet::default();
for suppression in &self.valid {
if !code_is_valid(&suppression.code, &context.settings().external) {
// InvalidRuleCode
if context.is_rule_enabled(Rule::InvalidRuleCode) {
for comment in &suppression.comments {
let (range, edit) = Suppressions::delete_code_or_comment(
locator,
suppression,
comment,
true,
);
context
.report_diagnostic(
InvalidRuleCode {
rule_code: suppression.code.to_string(),
kind: InvalidRuleCodeKind::Suppression,
},
range,
)
.set_fix(Fix::safe_edit(edit));
}
}
} else if !suppression.used.get() {
// UnusedNOQA
if context.is_rule_enabled(Rule::UnusedNOQA) {
let Ok(rule) = Rule::from_code(
get_redirect_target(&suppression.code).unwrap_or(&suppression.code),
) else {
continue; // "external" lint code, don't treat it as unused
};
for comment in &suppression.comments {
let (range, edit) = Suppressions::delete_code_or_comment(
locator,
suppression,
comment,
false,
);
let codes = if context.is_rule_enabled(rule) {
UnusedCodes {
unmatched: vec![suppression.code.to_string()],
..Default::default()
}
} else {
UnusedCodes {
disabled: vec![suppression.code.to_string()],
..Default::default()
}
};
context
.report_diagnostic(
UnusedNOQA {
codes: Some(codes),
kind: UnusedNOQAKind::Suppression,
},
range,
)
.set_fix(Fix::safe_edit(edit));
}
}
} else if suppression.comments.len() == 1 {
// UnmatchedSuppressionComment
let range = suppression.comments[0].range;
if unmatched_ranges.insert(range) {
context.report_diagnostic_if_enabled(UnmatchedSuppressionComment {}, range);
}
}
if !context.any_rule_enabled(&[Rule::UnusedNOQA, Rule::InvalidRuleCode]) {
return;
}
if context.is_rule_enabled(Rule::InvalidSuppressionComment) {
for error in &self.errors {
context
.report_diagnostic(
InvalidSuppressionComment {
kind: InvalidSuppressionCommentKind::Error(error.kind),
},
error.range,
)
.set_fix(Fix::unsafe_edit(delete_comment(error.range, locator)));
}
}
let unused = self
.valid
.iter()
.filter(|suppression| !suppression.used.get());
if context.is_rule_enabled(Rule::InvalidSuppressionComment) {
for invalid in &self.invalid {
context
.report_diagnostic(
InvalidSuppressionComment {
kind: InvalidSuppressionCommentKind::Invalid(invalid.kind),
},
invalid.comment.range,
)
.set_fix(Fix::unsafe_edit(delete_comment(
invalid.comment.range,
locator,
)));
}
}
}
fn delete_code_or_comment(
locator: &Locator<'_>,
suppression: &Suppression,
comment: &SuppressionComment,
highlight_only_code: bool,
) -> (TextRange, Edit) {
let mut range = comment.range;
let edit = if comment.codes.len() == 1 {
if highlight_only_code {
range = comment.codes[0];
}
delete_comment(comment.range, locator)
} else {
let code_index = comment
.codes
.iter()
.position(|range| locator.slice(range) == suppression.code)
.unwrap();
range = comment.codes[code_index];
let code_range = if code_index < (comment.codes.len() - 1) {
TextRange::new(
comment.codes[code_index].start(),
comment.codes[code_index + 1].start(),
)
} else {
TextRange::new(
comment.codes[code_index - 1].end(),
comment.codes[code_index].end(),
)
for suppression in unused {
let Ok(rule) = Rule::from_code(&suppression.code) else {
continue; // TODO: invalid code
};
Edit::range_deletion(code_range)
};
(range, edit)
for comment in &suppression.comments {
let mut range = comment.range;
let edit = if comment.codes.len() == 1 {
delete_comment(comment.range, locator)
} else {
let code_index = comment
.codes
.iter()
.position(|range| locator.slice(range) == suppression.code)
.unwrap();
range = comment.codes[code_index];
let code_range = if code_index < (comment.codes.len() - 1) {
TextRange::new(
comment.codes[code_index].start(),
comment.codes[code_index + 1].start(),
)
} else {
TextRange::new(
comment.codes[code_index - 1].end(),
comment.codes[code_index].end(),
)
};
Edit::range_deletion(code_range)
};
let codes = if context.is_rule_enabled(rule) {
UnusedCodes {
unmatched: vec![suppression.code.to_string()],
..Default::default()
}
} else {
UnusedCodes {
disabled: vec![suppression.code.to_string()],
..Default::default()
}
};
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(codes),
kind: UnusedNOQAKind::Suppression,
},
range,
);
diagnostic.set_fix(Fix::safe_edit(edit));
}
}
for error in self
.errors
.iter()
.filter(|error| error.kind == ParseErrorKind::MissingCodes)
{
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(UnusedCodes::default()),
kind: UnusedNOQAKind::Suppression,
},
error.range,
);
diagnostic.set_fix(Fix::safe_edit(delete_comment(error.range, locator)));
}
}
}
@@ -457,7 +391,7 @@ impl<'a> SuppressionsBuilder<'a> {
}
#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
pub(crate) enum ParseErrorKind {
enum ParseErrorKind {
#[error("not a suppression comment")]
NotASuppression,
@@ -467,7 +401,7 @@ pub(crate) enum ParseErrorKind {
#[error("unknown ruff directive")]
UnknownAction,
#[error("missing suppression codes like `[E501, ...]`")]
#[error("missing suppression codes")]
MissingCodes,
#[error("missing closing bracket")]

View File

@@ -12,7 +12,6 @@ pub use python_version::*;
pub mod comparable;
pub mod docstrings;
mod expression;
pub mod find_node;
mod generated;
pub mod helpers;
pub mod identifier;

View File

@@ -2,25 +2,18 @@
use crate::{self as ast, AnyNodeRef, ExceptHandler, Stmt};
/// Given a [`Stmt`] and its parent, return the [`ast::Suite`] that contains the [`Stmt`].
pub fn suite<'a>(
stmt: impl Into<AnyNodeRef<'a>>,
parent: impl Into<AnyNodeRef<'a>>,
) -> Option<EnclosingSuite<'a>> {
pub fn suite<'a>(stmt: &'a Stmt, parent: &'a Stmt) -> Option<EnclosingSuite<'a>> {
// TODO: refactor this to work without a parent, ie when `stmt` is at the top level
let stmt = stmt.into();
match parent.into() {
AnyNodeRef::ModModule(ast::ModModule { body, .. }) => EnclosingSuite::new(body, stmt),
AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. }) => {
EnclosingSuite::new(body, stmt)
}
AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. }) => EnclosingSuite::new(body, stmt),
AnyNodeRef::StmtFor(ast::StmtFor { body, orelse, .. }) => [body, orelse]
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. }) => EnclosingSuite::new(body, stmt),
Stmt::ClassDef(ast::StmtClassDef { body, .. }) => EnclosingSuite::new(body, stmt),
Stmt::For(ast::StmtFor { body, orelse, .. }) => [body, orelse]
.iter()
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => [body, orelse]
Stmt::While(ast::StmtWhile { body, orelse, .. }) => [body, orelse]
.iter()
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
AnyNodeRef::StmtIf(ast::StmtIf {
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
@@ -28,12 +21,12 @@ pub fn suite<'a>(
.into_iter()
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
AnyNodeRef::StmtWith(ast::StmtWith { body, .. }) => EnclosingSuite::new(body, stmt),
AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => cases
Stmt::With(ast::StmtWith { body, .. }) => EnclosingSuite::new(body, stmt),
Stmt::Match(ast::StmtMatch { cases, .. }) => cases
.iter()
.map(|case| &case.body)
.find_map(|body| EnclosingSuite::new(body, stmt)),
AnyNodeRef::StmtTry(ast::StmtTry {
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
@@ -58,10 +51,10 @@ pub struct EnclosingSuite<'a> {
}
impl<'a> EnclosingSuite<'a> {
pub fn new(suite: &'a [Stmt], stmt: AnyNodeRef<'a>) -> Option<Self> {
pub fn new(suite: &'a [Stmt], stmt: &'a Stmt) -> Option<Self> {
let position = suite
.iter()
.position(|sibling| AnyNodeRef::ptr_eq(sibling.into(), stmt))?;
.position(|sibling| AnyNodeRef::ptr_eq(sibling.into(), stmt.into()))?;
Some(EnclosingSuite { suite, position })
}

View File

@@ -222,17 +222,6 @@ where
visitor.leave_node(node);
}
pub fn walk_node<'a, V>(visitor: &mut V, node: AnyNodeRef<'a>)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
if visitor.enter_node(node).is_traverse() {
node.visit_source_order(visitor);
}
visitor.leave_node(node);
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum TraversalSignal {
Traverse,

View File

@@ -179,45 +179,42 @@ impl LineIndex {
let line = self.line_index(offset);
let line_start = self.line_start(line, text);
let character_offset =
self.characters_between(TextRange::new(line_start, offset), text, encoding);
SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed(character_offset),
}
}
fn characters_between(
&self,
range: TextRange,
text: &str,
encoding: PositionEncoding,
) -> usize {
if self.is_ascii() {
return (range.end() - range.start()).to_usize();
return SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed((offset - line_start).to_usize()),
};
}
match encoding {
PositionEncoding::Utf8 => (range.end() - range.start()).to_usize(),
PositionEncoding::Utf8 => {
let character_offset = offset - line_start;
SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed(character_offset.to_usize()),
}
}
PositionEncoding::Utf16 => {
let up_to_character = &text[range];
up_to_character.encode_utf16().count()
let up_to_character = &text[TextRange::new(line_start, offset)];
let character = up_to_character.encode_utf16().count();
SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed(character),
}
}
PositionEncoding::Utf32 => {
let up_to_character = &text[range];
up_to_character.chars().count()
let up_to_character = &text[TextRange::new(line_start, offset)];
let character = up_to_character.chars().count();
SourceLocation {
line,
character_offset: OneIndexed::from_zero_indexed(character),
}
}
}
}
/// Returns the length of the line in characters, respecting the given encoding
pub fn line_len(&self, line: OneIndexed, text: &str, encoding: PositionEncoding) -> usize {
let line_range = self.line_range(line, text);
self.characters_between(line_range, text, encoding)
}
/// Return the number of lines in the source code.
pub fn line_count(&self) -> usize {
self.line_starts().len()

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.14.10"
version = "0.14.9"
publish = false
authors = { workspace = true }
edition = { workspace = true }

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

@@ -5,7 +5,7 @@
## `byte-string-type-annotation`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%20byte-string-type-annotation" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L36" target="_blank">View source</a>
@@ -36,7 +36,7 @@ def test(): -> "int":
## `call-non-callable`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L135" target="_blank">View source</a>
@@ -60,7 +60,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
## `conflicting-argument-forms`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L179" target="_blank">View source</a>
@@ -92,7 +92,7 @@ f(int) # error
## `conflicting-declarations`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L205" target="_blank">View source</a>
@@ -123,7 +123,7 @@ a = 1
## `conflicting-metaclass`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L230" target="_blank">View source</a>
@@ -155,7 +155,7 @@ class C(A, B): ...
## `cyclic-class-definition`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L256" target="_blank">View source</a>
@@ -187,7 +187,7 @@ class B(A): ...
## `cyclic-type-alias-definition`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
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%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L282" target="_blank">View source</a>
@@ -215,7 +215,7 @@ type B = A
## `duplicate-base`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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#L343" target="_blank">View source</a>
@@ -242,7 +242,7 @@ class B(A, A): ...
## `duplicate-kw-only`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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#L364" target="_blank">View source</a>
@@ -280,7 +280,7 @@ class A: # Crash at runtime
## `escape-character-in-forward-annotation`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%20escape-character-in-forward-annotation" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L120" target="_blank">View source</a>
@@ -292,7 +292,7 @@ TODO #14889
## `fstring-type-annotation`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%20fstring-type-annotation" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L11" target="_blank">View source</a>
@@ -323,7 +323,7 @@ def test(): -> "int":
## `implicit-concatenated-string-type-annotation`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%20implicit-concatenated-string-type-annotation" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L86" target="_blank">View source</a>
@@ -354,7 +354,7 @@ def test(): -> "Literal[5]":
## `inconsistent-mro`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L590" target="_blank">View source</a>
@@ -384,7 +384,7 @@ class C(A, B): ...
## `index-out-of-bounds`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L614" target="_blank">View source</a>
@@ -410,7 +410,7 @@ t[3] # IndexError: tuple index out of range
## `instance-layout-conflict`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L396" target="_blank">View source</a>
@@ -499,7 +499,7 @@ an atypical memory layout.
## `invalid-argument-type`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L668" target="_blank">View source</a>
@@ -526,7 +526,7 @@ func("foo") # error: [invalid-argument-type]
## `invalid-assignment`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L708" target="_blank">View source</a>
@@ -554,7 +554,7 @@ a: int = ''
## `invalid-attribute-access`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L2003" target="_blank">View source</a>
@@ -588,7 +588,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
## `invalid-await`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.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#L730" target="_blank">View source</a>
@@ -624,7 +624,7 @@ asyncio.run(main())
## `invalid-base`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L760" target="_blank">View source</a>
@@ -648,7 +648,7 @@ class A(42): ... # error: [invalid-base]
## `invalid-context-manager`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L811" target="_blank">View source</a>
@@ -675,7 +675,7 @@ with 1:
## `invalid-declaration`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L832" target="_blank">View source</a>
@@ -704,7 +704,7 @@ a: str
## `invalid-exception-caught`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L855" target="_blank">View source</a>
@@ -748,7 +748,7 @@ except ZeroDivisionError:
## `invalid-explicit-override`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.28">0.0.1-alpha.28</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1673" target="_blank">View source</a>
@@ -790,7 +790,7 @@ class D(A):
## `invalid-frozen-dataclass-subclass`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.35">0.0.1-alpha.35</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-frozen-dataclass-subclass" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2229" target="_blank">View source</a>
@@ -834,7 +834,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
## `invalid-generic-class`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L891" target="_blank">View source</a>
@@ -872,7 +872,7 @@ class D(Generic[U, T]): ...
## `invalid-key`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.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#L635" target="_blank">View source</a>
@@ -911,7 +911,7 @@ carol = Person(name="Carol", age=25) # typo!
## `invalid-legacy-type-variable`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L922" target="_blank">View source</a>
@@ -946,7 +946,7 @@ def f(t: TypeVar("U")): ...
## `invalid-metaclass`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1019" target="_blank">View source</a>
@@ -980,7 +980,7 @@ class B(metaclass=f): ...
## `invalid-method-override`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.20">0.0.1-alpha.20</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2131" target="_blank">View source</a>
@@ -1087,7 +1087,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
## `invalid-named-tuple`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.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#L542" target="_blank">View source</a>
@@ -1141,7 +1141,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
## `invalid-newtype`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
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#L995" target="_blank">View source</a>
@@ -1171,7 +1171,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
## `invalid-overload`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1046" target="_blank">View source</a>
@@ -1221,7 +1221,7 @@ def foo(x: int) -> int: ...
## `invalid-parameter-default`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1145" target="_blank">View source</a>
@@ -1247,7 +1247,7 @@ def f(a: int = ''): ...
## `invalid-paramspec`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L950" target="_blank">View source</a>
@@ -1278,7 +1278,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
## `invalid-protocol`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L478" target="_blank">View source</a>
@@ -1312,7 +1312,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
## `invalid-raise`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1165" target="_blank">View source</a>
@@ -1361,7 +1361,7 @@ def g():
## `invalid-return-type`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L689" target="_blank">View source</a>
@@ -1386,7 +1386,7 @@ def func() -> int:
## `invalid-super-argument`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1208" target="_blank">View source</a>
@@ -1432,7 +1432,7 @@ super(B, A) # error: `A` does not satisfy `issubclass(A, B)`
## `invalid-syntax-in-forward-annotation`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%20invalid-syntax-in-forward-annotation" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L111" target="_blank">View source</a>
@@ -1444,7 +1444,7 @@ TODO #14889
## `invalid-type-alias-type`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
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#L974" target="_blank">View source</a>
@@ -1471,7 +1471,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
## `invalid-type-arguments`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1440" target="_blank">View source</a>
@@ -1518,7 +1518,7 @@ Bar[int] # error: too few arguments
## `invalid-type-checking-constant`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1247" target="_blank">View source</a>
@@ -1548,7 +1548,7 @@ TYPE_CHECKING = ''
## `invalid-type-form`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1271" target="_blank">View source</a>
@@ -1578,7 +1578,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
## `invalid-type-guard-call`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.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#L1323" target="_blank">View source</a>
@@ -1612,7 +1612,7 @@ f(10) # Error
## `invalid-type-guard-definition`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.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#L1295" target="_blank">View source</a>
@@ -1646,7 +1646,7 @@ class C:
## `invalid-type-variable-constraints`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1351" target="_blank">View source</a>
@@ -1681,7 +1681,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
## `missing-argument`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1380" target="_blank">View source</a>
@@ -1706,7 +1706,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
## `missing-typed-dict-key`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.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#L2104" target="_blank">View source</a>
@@ -1739,7 +1739,7 @@ alice["age"] # KeyError
## `no-matching-overload`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1399" target="_blank">View source</a>
@@ -1768,7 +1768,7 @@ func("string") # error: [no-matching-overload]
## `non-subscriptable`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1422" target="_blank">View source</a>
@@ -1792,7 +1792,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
## `not-iterable`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1481" target="_blank">View source</a>
@@ -1818,7 +1818,7 @@ for i in 34: # TypeError: 'int' object is not iterable
## `override-of-final-method`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.29">0.0.1-alpha.29</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1646" target="_blank">View source</a>
@@ -1851,7 +1851,7 @@ class B(A):
## `parameter-already-assigned`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1532" target="_blank">View source</a>
@@ -1878,7 +1878,7 @@ f(1, x=2) # Error raised here
## `positional-only-parameter-as-kwarg`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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.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#L1857" target="_blank">View source</a>
@@ -1905,7 +1905,7 @@ f(x=1) # Error raised here
## `raw-string-type-annotation`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%20raw-string-type-annotation" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L61" target="_blank">View source</a>
@@ -1936,7 +1936,7 @@ def test(): -> "int":
## `static-assert-error`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1979" target="_blank">View source</a>
@@ -1966,7 +1966,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
## `subclass-of-final-class`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1623" target="_blank">View source</a>
@@ -1995,7 +1995,7 @@ class B(A): ... # Error raised here
## `super-call-in-named-tuple-method`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
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/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1791" target="_blank">View source</a>
@@ -2029,7 +2029,7 @@ class F(NamedTuple):
## `too-many-positional-arguments`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1731" target="_blank">View source</a>
@@ -2056,7 +2056,7 @@ f("foo") # Error raised here
## `type-assertion-failure`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1709" target="_blank">View source</a>
@@ -2084,7 +2084,7 @@ def _(x: int):
## `unavailable-implicit-super-arguments`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1752" target="_blank">View source</a>
@@ -2130,7 +2130,7 @@ class A:
## `unknown-argument`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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#L1836" target="_blank">View source</a>
@@ -2157,7 +2157,7 @@ f(x=1, y=2) # Error raised here
## `unresolved-attribute`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1878" target="_blank">View source</a>
@@ -2185,7 +2185,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
## `unresolved-import`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1900" target="_blank">View source</a>
@@ -2210,7 +2210,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
## `unresolved-reference`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1919" target="_blank">View source</a>
@@ -2235,7 +2235,7 @@ print(x) # NameError: name 'x' is not defined
## `unsupported-bool-conversion`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1501" target="_blank">View source</a>
@@ -2272,7 +2272,7 @@ b1 < b2 < b1 # exception raised here
## `unsupported-operator`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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%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#L1938" target="_blank">View source</a>
@@ -2300,7 +2300,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
## `zero-stepsize-in-slice`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></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#L1960" target="_blank">View source</a>
@@ -2325,7 +2325,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
## `ambiguous-protocol-member`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L507" target="_blank">View source</a>
@@ -2366,7 +2366,7 @@ class SubProto(BaseProto, Protocol):
## `deprecated`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L322" target="_blank">View source</a>
@@ -2393,7 +2393,7 @@ old_func() # emits [deprecated] diagnostic
## `ignore-comment-unknown-rule`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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%20ignore-comment-unknown-rule" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L47" target="_blank">View source</a>
@@ -2424,7 +2424,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
## `invalid-ignore-comment`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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%20invalid-ignore-comment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L72" target="_blank">View source</a>
@@ -2454,7 +2454,7 @@ a = 20 / 0 # type: ignore
## `possibly-missing-attribute`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L1553" target="_blank">View source</a>
@@ -2482,7 +2482,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
## `possibly-missing-implicit-call`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L153" target="_blank">View source</a>
@@ -2511,10 +2511,42 @@ class A:
A()[0] # TypeError: 'A' object is not subscriptable
```
## `possibly-missing-import`
<small>
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#L1575" target="_blank">View source</a>
</small>
**What it does**
Checks for imports of symbols that may be missing.
**Why is this bad?**
Importing a missing module or name will raise a `ModuleNotFoundError`
or `ImportError` at runtime.
**Examples**
```python
# module.py
import datetime
if datetime.date.today().weekday() != 6:
a = 1
# main.py
from module import a # ImportError: cannot import name 'a' from 'module'
```
## `redundant-cast`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L2031" target="_blank">View source</a>
@@ -2541,7 +2573,7 @@ cast(int, f()) # Redundant
## `undefined-reveal`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L1818" target="_blank">View source</a>
@@ -2565,7 +2597,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
## `unresolved-global`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L2052" target="_blank">View source</a>
@@ -2623,7 +2655,7 @@ def g():
## `unsupported-base`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L778" target="_blank">View source</a>
@@ -2662,7 +2694,7 @@ class D(C): ... # error: [unsupported-base]
## `useless-overload-body`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
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#L1089" target="_blank">View source</a>
@@ -2725,7 +2757,7 @@ def foo(x: int | str) -> int | str:
## `division-by-zero`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
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#L304" target="_blank">View source</a>
@@ -2746,42 +2778,10 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
5 / 0
```
## `possibly-missing-import`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1575" target="_blank">View source</a>
</small>
**What it does**
Checks for imports of symbols that may be missing.
**Why is this bad?**
Importing a missing module or name will raise a `ModuleNotFoundError`
or `ImportError` at runtime.
**Examples**
```python
# module.py
import datetime
if datetime.date.today().weekday() != 6:
a = 1
# main.py
from module import a # ImportError: cannot import name 'a' from 'module'
```
## `possibly-unresolved-reference`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
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#L1601" target="_blank">View source</a>
@@ -2809,7 +2809,7 @@ print(x) # NameError: name 'x' is not defined
## `unused-ignore-comment`
<small>
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
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%20unused-ignore-comment" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L22" target="_blank">View source</a>

View File

@@ -15,7 +15,7 @@ import-deprioritizes-type_check_only,main.py,3,2
import-deprioritizes-type_check_only,main.py,4,3
import-keyword-completion,main.py,0,1
internal-typeshed-hidden,main.py,0,2
none-completion,main.py,0,1
none-completion,main.py,0,2
numpy-array,main.py,0,159
numpy-array,main.py,1,1
object-attr-instance-methods,main.py,0,1
1 name file index rank
15 import-deprioritizes-type_check_only main.py 4 3
16 import-keyword-completion main.py 0 1
17 internal-typeshed-hidden main.py 0 2
18 none-completion main.py 0 1 2
19 numpy-array main.py 0 159
20 numpy-array main.py 1 1
21 object-attr-instance-methods main.py 0 1

View File

@@ -1,8 +1,7 @@
use crate::completion;
use crate::{completion, find_node::covering_node};
use ruff_db::{files::File, parsed::parsed_module};
use ruff_diagnostics::Edit;
use ruff_python_ast::find_node::covering_node;
use ruff_text_size::TextRange;
use ty_project::Db;
use ty_python_semantic::create_suppression_fix;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
use crate::AnyNodeRef;
use crate::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_node};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
use ruff_text_size::{Ranged, TextRange};
use std::fmt;
use std::fmt::Formatter;
@@ -11,7 +11,7 @@ use std::fmt::Formatter;
///
/// ## Panics
/// Panics if `range` is not contained within `root`.
pub fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
struct Visitor<'a> {
range: TextRange,
found: bool,
@@ -48,12 +48,15 @@ pub fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
ancestors: Vec::new(),
};
walk_node(&mut visitor, root);
root.visit_source_order(&mut visitor);
if visitor.ancestors.is_empty() {
visitor.ancestors.push(root);
}
CoveringNode::from_ancestors(visitor.ancestors)
}
/// The node with a minimal range that fully contains the search range.
pub struct CoveringNode<'a> {
pub(crate) struct CoveringNode<'a> {
/// The covering node, along with all of its ancestors up to the
/// root. The root is always the first element and the covering
/// node found is always the last node. This sequence is guaranteed
@@ -64,12 +67,12 @@ pub struct CoveringNode<'a> {
impl<'a> CoveringNode<'a> {
/// Creates a new `CoveringNode` from a list of ancestor nodes.
/// The ancestors should be ordered from root to the covering node.
pub fn from_ancestors(ancestors: Vec<AnyNodeRef<'a>>) -> Self {
pub(crate) fn from_ancestors(ancestors: Vec<AnyNodeRef<'a>>) -> Self {
Self { nodes: ancestors }
}
/// Returns the covering node found.
pub fn node(&self) -> AnyNodeRef<'a> {
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
*self
.nodes
.last()
@@ -77,7 +80,7 @@ impl<'a> CoveringNode<'a> {
}
/// Returns the node's parent.
pub fn parent(&self) -> Option<AnyNodeRef<'a>> {
pub(crate) fn parent(&self) -> Option<AnyNodeRef<'a>> {
let penultimate = self.nodes.len().checked_sub(2)?;
self.nodes.get(penultimate).copied()
}
@@ -87,7 +90,7 @@ impl<'a> CoveringNode<'a> {
///
/// The "first" here means that the node closest to a leaf is
/// returned.
pub fn find_first(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
pub(crate) fn find_first(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
let Some(index) = self.find_first_index(f) else {
return Err(self);
};
@@ -102,7 +105,7 @@ impl<'a> CoveringNode<'a> {
/// the highest ancestor found satisfying the given predicate is
/// returned. Note that this is *not* the same as finding the node
/// closest to the root that satisfies the given predictate.
pub fn find_last(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
pub(crate) fn find_last(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
let Some(mut index) = self.find_first_index(&f) else {
return Err(self);
};
@@ -115,7 +118,7 @@ impl<'a> CoveringNode<'a> {
/// Returns an iterator over the ancestor nodes, starting with the node itself
/// and walking towards the root.
pub fn ancestors(&self) -> impl DoubleEndedIterator<Item = AnyNodeRef<'a>> + '_ {
pub(crate) fn ancestors(&self) -> impl DoubleEndedIterator<Item = AnyNodeRef<'a>> + '_ {
self.nodes.iter().copied().rev()
}

View File

@@ -5,9 +5,9 @@ pub use crate::goto_type_definition::goto_type_definition;
use std::borrow::Cow;
use crate::find_node::covering_node;
use crate::stub_mapping::StubMapper;
use ruff_db::parsed::ParsedModuleRef;
use ruff_python_ast::find_node::{CoveringNode, covering_node};
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -665,7 +665,7 @@ impl GotoTarget<'_> {
/// Creates a `GotoTarget` from a `CoveringNode` and an offset within the node
pub(crate) fn from_covering_node<'a>(
model: &SemanticModel,
covering_node: &CoveringNode<'a>,
covering_node: &crate::find_node::CoveringNode<'a>,
offset: TextSize,
tokens: &Tokens,
) -> Option<GotoTarget<'a>> {

View File

@@ -386,29 +386,6 @@ FOO = 0
");
}
#[test]
fn goto_declaration_from_import_rhs_is_module() {
let test = CursorTest::builder()
.source("lib/__init__.py", r#""#)
.source("lib/module.py", r#""#)
.source("main.py", r#"from lib import module<CURSOR>"#)
.build();
// Should resolve to the actual function definition, not the import statement
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Go to declaration
--> main.py:1:17
|
1 | from lib import module
| ^^^^^^ Clicking here
|
info: Found 1 declaration
--> lib/module.py:1:1
|
|
");
}
#[test]
fn goto_declaration_from_import_as() {
let test = CursorTest::builder()

View File

@@ -187,42 +187,29 @@ mod tests {
let test = cursor_test(
r#"
a = 10
"""This is the docs for this value
Wow these are good docs!
"""
a<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[10]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[10]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:8:1
--> main.py:4:1
|
6 | """
7 |
8 | a
2 | a = 10
3 |
4 | a
| ^- Cursor offset
| |
| source
|
"#);
");
}
#[test]
@@ -716,10 +703,6 @@ mod tests {
def __init__(a: int, b: str):
self.a = a
"""This is the docs for this value
Wow these are good docs!
"""
self.b: str = b
foo = Foo()
@@ -735,10 +718,10 @@ mod tests {
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:14:5
--> main.py:10:5
|
13 | foo = Foo()
14 | foo.a
9 | foo = Foo()
10 | foo.a
| -
| |
| source
@@ -1200,13 +1183,13 @@ def ab(a: str): ...
.build();
assert_snapshot!(test.hover(), @r"
def ab(a: int) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the int overload
---------------------------------------------
```python
def ab(a: int) -> Unknown
(a: int) -> Unknown
```
---
the int overload
@@ -1260,13 +1243,13 @@ def ab(a: str):
.build();
assert_snapshot!(test.hover(), @r#"
def ab(a: str) -> Unknown
(a: str) -> Unknown
---------------------------------------------
the int overload
---------------------------------------------
```python
def ab(a: str) -> Unknown
(a: str) -> Unknown
```
---
the int overload
@@ -1320,7 +1303,7 @@ def ab(a: int):
.build();
assert_snapshot!(test.hover(), @r"
def ab(
(
a: int,
b: int
) -> Unknown
@@ -1329,7 +1312,7 @@ def ab(a: int):
---------------------------------------------
```python
def ab(
(
a: int,
b: int
) -> Unknown
@@ -1386,13 +1369,13 @@ def ab(a: int):
.build();
assert_snapshot!(test.hover(), @r"
def ab(a: int) -> Unknown
(a: int) -> Unknown
---------------------------------------------
the two arg overload
---------------------------------------------
```python
def ab(a: int) -> Unknown
(a: int) -> Unknown
```
---
the two arg overload
@@ -1450,7 +1433,7 @@ def ab(a: int, *, c: int):
.build();
assert_snapshot!(test.hover(), @r"
def ab(
(
a: int,
*,
b: int
@@ -1460,7 +1443,7 @@ def ab(a: int, *, c: int):
---------------------------------------------
```python
def ab(
(
a: int,
*,
b: int
@@ -1522,7 +1505,7 @@ def ab(a: int, *, c: int):
.build();
assert_snapshot!(test.hover(), @r"
def ab(
(
a: int,
*,
c: int
@@ -1532,7 +1515,7 @@ def ab(a: int, *, c: int):
---------------------------------------------
```python
def ab(
(
a: int,
*,
c: int
@@ -1581,11 +1564,11 @@ def ab(a: int, *, c: int):
);
assert_snapshot!(test.hover(), @r#"
def foo(
(
a: int,
b
) -> Unknown
def foo(
(
a: str,
b
) -> Unknown
@@ -1594,11 +1577,11 @@ def ab(a: int, *, c: int):
---------------------------------------------
```python
def foo(
(
a: int,
b
) -> Unknown
def foo(
(
a: str,
b
) -> Unknown
@@ -1645,15 +1628,15 @@ def ab(a: int, *, c: int):
);
assert_snapshot!(test.hover(), @r#"
def foo(a: int) -> Unknown
def foo(a: str) -> Unknown
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
The first overload
---------------------------------------------
```python
def foo(a: int) -> Unknown
def foo(a: str) -> Unknown
(a: int) -> Unknown
(a: str) -> Unknown
```
---
The first overload
@@ -2362,28 +2345,15 @@ def function():
let test = cursor_test(
r#"
value<CURSOR> = 1
"""This is the docs for this value
Wow these are good docs!
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[1]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:1
@@ -2392,9 +2362,8 @@ def function():
| ^^^^^- Cursor offset
| |
| source
3 | """This is the docs for this value
|
"#);
");
}
#[test]
@@ -2402,15 +2371,7 @@ def function():
let test = cursor_test(
r#"
value = 1
"""This is the docs for this value
Wow these are good docs!
"""
value<CURSOR> += 2
"""Other docs???
Is this allowed???
"""
"#,
);
@@ -2418,34 +2379,23 @@ def function():
// Showing the new value might be more intuitive for some users, but the actual 'use'
// of the `value` symbol here in read-context is `1`. This comment mainly exists to
// signal that it might be okay to revisit this in the future and reveal 3 instead.
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[1]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:7:1
--> main.py:3:1
|
5 | Wow these are good docs!
6 | """
7 | value += 2
2 | value = 1
3 | value += 2
| ^^^^^- Cursor offset
| |
| source
8 | """Other docs???
|
"#);
");
}
#[test]
@@ -2454,47 +2404,29 @@ def function():
r#"
class C:
attr: int = 1
"""This is the docs for this value
Wow these are good docs!
"""
C.attr<CURSOR> = 2
"""Other docs???
Is this allowed???
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[2]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[2]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:9:3
|
7 | """
8 |
9 | C.attr = 2
| ^^^^- Cursor offset
| |
| source
10 | """Other docs???
|
"#);
--> main.py:5:3
|
3 | attr: int = 1
4 |
5 | C.attr = 2
| ^^^^- Cursor offset
| |
| source
|
");
}
#[test]
@@ -2503,49 +2435,31 @@ def function():
r#"
class C:
attr = 1
"""This is the docs for this value
Wow these are good docs!
"""
C.attr<CURSOR> += 2
"""Other docs???
Is this allowed???
"""
"#,
);
// See the comment in the `hover_augmented_assignment` test above. The same
// reasoning applies here.
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Unknown | Literal[1]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Unknown | Literal[1]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:9:3
|
7 | """
8 |
9 | C.attr += 2
| ^^^^- Cursor offset
| |
| source
10 | """Other docs???
|
"#);
--> main.py:5:3
|
3 | attr = 1
4 |
5 | C.attr += 2
| ^^^^- Cursor offset
| |
| source
|
");
}
#[test]
@@ -2554,28 +2468,15 @@ def function():
r#"
class Foo:
a<CURSOR>: int
"""This is the docs for this value
Wow these are good docs!
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
int
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:3:5
@@ -2585,9 +2486,8 @@ def function():
| ^- Cursor offset
| |
| source
4 | """This is the docs for this value
|
"#);
");
}
#[test]
@@ -2596,28 +2496,15 @@ def function():
r#"
class Foo:
a<CURSOR>: int = 1
"""This is the docs for this value
Wow these are good docs!
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
Literal[1]
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
Literal[1]
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:3:5
@@ -2627,52 +2514,7 @@ def function():
| ^- Cursor offset
| |
| source
4 | """This is the docs for this value
|
"#);
}
#[test]
fn hover_annotated_assignment_with_rhs_use() {
let test = cursor_test(
r#"
class Foo:
a: int = 1
"""This is the docs for this value
Wow these are good docs!
"""
x = Foo()
x.a<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
int
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:10:3
|
9 | x = Foo()
10 | x.a
| ^- Cursor offset
| |
| source
|
");
}
@@ -2683,28 +2525,15 @@ def function():
class Foo:
def __init__(self, a: int):
self.a<CURSOR>: int = a
"""This is the docs for this value
Wow these are good docs!
"""
"#,
);
assert_snapshot!(test.hover(), @r#"
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
int
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:4:14
@@ -2715,53 +2544,7 @@ def function():
| ^- Cursor offset
| |
| source
5 | """This is the docs for this value
|
"#);
}
#[test]
fn hover_annotated_attribute_assignment_use() {
let test = cursor_test(
r#"
class Foo:
def __init__(self, a: int):
self.a: int = a
"""This is the docs for this value
Wow these are good docs!
"""
x = Foo(1)
x.a<CURSOR>
"#,
);
assert_snapshot!(test.hover(), @r"
int
---------------------------------------------
This is the docs for this value
Wow these are good docs!
---------------------------------------------
```python
int
```
---
This is the docs for this value
Wow these are good docs!
---------------------------------------------
info[hover]: Hovered content is
--> main.py:11:3
|
10 | x = Foo(1)
11 | x.a
| ^- Cursor offset
| |
| source
|
");
}
@@ -3450,12 +3233,12 @@ def function():
// TODO: We should only show the matching overload here.
// https://github.com/astral-sh/ty/issues/73
assert_snapshot!(test.hover(), @r"
def __add__(other: Test, /) -> Test
def __add__(other: Other, /) -> Test
(other: Test, /) -> Test
(other: Other, /) -> Test
---------------------------------------------
```python
def __add__(other: Test, /) -> Test
def __add__(other: Other, /) -> Test
(other: Test, /) -> Test
(other: Other, /) -> Test
```
---------------------------------------------
info[hover]: Hovered content is

View File

@@ -869,6 +869,7 @@ mod tests {
use insta::assert_snapshot;
use insta::internals::SettingsBindDropGuard;
use crate::find_node::covering_node;
use crate::tests::{CursorTest, CursorTestBuilder, cursor_test};
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
use ruff_db::files::{File, FileRootKind, system_path_to_file};
@@ -876,7 +877,6 @@ mod tests {
use ruff_db::source::source_text;
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
use ruff_db::{Db, system};
use ruff_python_ast::find_node::covering_node;
use ruff_python_codegen::Stylist;
use ruff_python_trivia::textwrap::dedent;
use ruff_text_size::TextSize;

View File

@@ -8,6 +8,7 @@ mod completion;
mod doc_highlights;
mod docstring;
mod document_symbols;
mod find_node;
mod find_references;
mod goto;
mod goto_declaration;

View File

@@ -10,10 +10,10 @@
//! all references to these externally-visible symbols therefore requires
//! an expensive search of all source files in the workspace.
use crate::find_node::CoveringNode;
use crate::goto::GotoTarget;
use crate::{Db, NavigationTargets, ReferenceKind, ReferenceTarget};
use ruff_db::files::File;
use ruff_python_ast::find_node::CoveringNode;
use ruff_python_ast::token::Tokens;
use ruff_python_ast::{
self as ast, AnyNodeRef,
@@ -334,7 +334,10 @@ impl LocalReferencesFinder<'_> {
/// Determines whether the given covering node is a reference to
/// the symbol we are searching for
fn check_reference_from_covering_node(&mut self, covering_node: &CoveringNode<'_>) {
fn check_reference_from_covering_node(
&mut self,
covering_node: &crate::find_node::CoveringNode<'_>,
) {
// Use the start of the covering node as the offset. Any offset within
// the node is fine here. Offsets matter only for import statements
// where the identifier might be a multi-part module name.

View File

@@ -1,9 +1,9 @@
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::find_node::covering_node;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::Db;
use crate::find_node::covering_node;
/// Returns a list of nested selection ranges, where each range contains the next one.
/// The first range in the list is the largest range containing the cursor position.
@@ -66,28 +66,20 @@ x = 1 + <CURSOR>2
assert_snapshot!(test.selection_range(), @r"
info[selection-range]: Selection Range 0
--> main.py:1:1
|
1 | /
2 | | x = 1 + 2
| |__________^
|
info[selection-range]: Selection Range 1
--> main.py:2:1
|
2 | x = 1 + 2
| ^^^^^^^^^
|
info[selection-range]: Selection Range 2
info[selection-range]: Selection Range 1
--> main.py:2:5
|
2 | x = 1 + 2
| ^^^^^
|
info[selection-range]: Selection Range 3
info[selection-range]: Selection Range 2
--> main.py:2:9
|
2 | x = 1 + 2
@@ -110,28 +102,20 @@ print(\"he<CURSOR>llo\")
assert_snapshot!(test.selection_range(), @r#"
info[selection-range]: Selection Range 0
--> main.py:1:1
|
1 | /
2 | | print("hello")
| |_______________^
|
info[selection-range]: Selection Range 1
--> main.py:2:1
|
2 | print("hello")
| ^^^^^^^^^^^^^^
|
info[selection-range]: Selection Range 2
info[selection-range]: Selection Range 1
--> main.py:2:6
|
2 | print("hello")
| ^^^^^^^^^
|
info[selection-range]: Selection Range 3
info[selection-range]: Selection Range 2
--> main.py:2:7
|
2 | print("hello")
@@ -155,15 +139,6 @@ def my_<CURSOR>function():
assert_snapshot!(test.selection_range(), @r"
info[selection-range]: Selection Range 0
--> main.py:1:1
|
1 | /
2 | | def my_function():
3 | | return 42
| |______________^
|
info[selection-range]: Selection Range 1
--> main.py:2:1
|
2 | / def my_function():
@@ -171,7 +146,7 @@ def my_<CURSOR>function():
| |_____________^
|
info[selection-range]: Selection Range 2
info[selection-range]: Selection Range 1
--> main.py:2:5
|
2 | def my_function():
@@ -197,16 +172,6 @@ class My<CURSOR>Class:
assert_snapshot!(test.selection_range(), @r"
info[selection-range]: Selection Range 0
--> main.py:1:1
|
1 | /
2 | | class MyClass:
3 | | def __init__(self):
4 | | self.value = 1
| |_______________________^
|
info[selection-range]: Selection Range 1
--> main.py:2:1
|
2 | / class MyClass:
@@ -215,7 +180,7 @@ class My<CURSOR>Class:
| |______________________^
|
info[selection-range]: Selection Range 2
info[selection-range]: Selection Range 1
--> main.py:2:7
|
2 | class MyClass:
@@ -240,56 +205,48 @@ result = [(lambda x: x[key.<CURSOR>attr])(item) for item in data if item is not
assert_snapshot!(test.selection_range(), @r"
info[selection-range]: Selection Range 0
--> main.py:1:1
|
1 | /
2 | | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]
| |______________________________________________________________________________^
|
info[selection-range]: Selection Range 1
--> main.py:2:1
|
2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info[selection-range]: Selection Range 2
info[selection-range]: Selection Range 1
--> main.py:2:10
|
2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info[selection-range]: Selection Range 3
info[selection-range]: Selection Range 2
--> main.py:2:11
|
2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
info[selection-range]: Selection Range 4
info[selection-range]: Selection Range 3
--> main.py:2:12
|
2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]
| ^^^^^^^^^^^^^^^^^^^^^
|
info[selection-range]: Selection Range 5
info[selection-range]: Selection Range 4
--> main.py:2:22
|
2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]
| ^^^^^^^^^^^
|
info[selection-range]: Selection Range 6
info[selection-range]: Selection Range 5
--> main.py:2:24
|
2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]
| ^^^^^^^^
|
info[selection-range]: Selection Range 7
info[selection-range]: Selection Range 6
--> main.py:2:28
|
2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]

View File

@@ -6,12 +6,11 @@
//! types, and documentation. It supports multiple signatures for union types
//! and overloads.
use crate::Db;
use crate::docstring::Docstring;
use crate::goto::Definitions;
use crate::{Db, find_node::covering_node};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::find_node::covering_node;
use ruff_python_ast::token::TokenKind;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange, TextSize};

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::name::Name;
use std::sync::Arc;
use thiserror::Error;
use ty_combine::Combine;
use ty_python_semantic::{MisconfigurationMode, ProgramSettings};
use ty_python_semantic::ProgramSettings;
use crate::metadata::options::ProjectOptionsOverrides;
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
@@ -37,9 +37,6 @@ pub struct ProjectMetadata {
/// The path ordering doesn't imply precedence.
#[cfg_attr(test, serde(skip_serializing_if = "Vec::is_empty"))]
pub(super) extra_configuration_paths: Vec<SystemPathBuf>,
#[cfg_attr(test, serde(skip))]
pub(super) misconfiguration_mode: MisconfigurationMode,
}
impl ProjectMetadata {
@@ -50,7 +47,6 @@ impl ProjectMetadata {
root,
extra_configuration_paths: Vec::default(),
options: Options::default(),
misconfiguration_mode: MisconfigurationMode::Fail,
}
}
@@ -74,7 +70,6 @@ impl ProjectMetadata {
root: system.current_directory().to_path_buf(),
options,
extra_configuration_paths: vec![path],
misconfiguration_mode: MisconfigurationMode::Fail,
})
}
@@ -87,7 +82,6 @@ impl ProjectMetadata {
pyproject.tool.and_then(|tool| tool.ty).unwrap_or_default(),
root,
pyproject.project.as_ref(),
MisconfigurationMode::Fail,
)
}
@@ -96,7 +90,6 @@ impl ProjectMetadata {
mut options: Options,
root: SystemPathBuf,
project: Option<&Project>,
misconfiguration_mode: MisconfigurationMode,
) -> Result<Self, ResolveRequiresPythonError> {
let name = project
.and_then(|project| project.name.as_deref())
@@ -124,7 +117,6 @@ impl ProjectMetadata {
root,
options,
extra_configuration_paths: Vec::new(),
misconfiguration_mode,
})
}
@@ -202,7 +194,6 @@ impl ProjectMetadata {
pyproject
.as_ref()
.and_then(|pyproject| pyproject.project.as_ref()),
MisconfigurationMode::Fail,
)
.map_err(|err| {
ProjectMetadataError::InvalidRequiresPythonConstraint {
@@ -282,13 +273,8 @@ impl ProjectMetadata {
system: &dyn System,
vendored: &VendoredFileSystem,
) -> anyhow::Result<ProgramSettings> {
self.options.to_program_settings(
self.root(),
self.name(),
system,
vendored,
self.misconfiguration_mode,
)
self.options
.to_program_settings(self.root(), self.name(), system, vendored)
}
pub fn apply_overrides(&mut self, overrides: &ProjectOptionsOverrides) {

View File

@@ -30,9 +30,9 @@ use thiserror::Error;
use ty_combine::Combine;
use ty_python_semantic::lint::{Level, LintSource, RuleSelection};
use ty_python_semantic::{
MisconfigurationMode, ProgramSettings, PythonEnvironment, PythonPlatform,
PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SearchPathSettings,
SearchPathValidationError, SearchPaths, SitePackagesPaths, SysPrefixPathOrigin,
ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionFileSource,
PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SearchPathValidationError,
SearchPaths, SitePackagesPaths, SysPrefixPathOrigin,
};
use ty_static::EnvVars;
@@ -117,7 +117,6 @@ impl Options {
project_name: &str,
system: &dyn System,
vendored: &VendoredFileSystem,
misconfiguration_mode: MisconfigurationMode,
) -> anyhow::Result<ProgramSettings> {
let environment = self.environment.or_default();
@@ -155,25 +154,14 @@ impl Options {
ValueSource::Editor => SysPrefixPathOrigin::Editor,
};
PythonEnvironment::new(python_path.absolute(project_root, system), origin, system)
.map_err(anyhow::Error::from)
.map(Some)
Some(PythonEnvironment::new(
python_path.absolute(project_root, system),
origin,
system,
)?)
} else {
PythonEnvironment::discover(project_root, system)
.context("Failed to discover local Python environment")
};
// If in safe-mode, fallback to None if this fails instead of erroring.
let python_environment = match python_environment {
Ok(python_environment) => python_environment,
Err(err) => {
if misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Default settings failed to discover local Python environment");
None
} else {
return Err(err);
}
}
.context("Failed to discover local Python environment")?
};
let self_site_packages = self_environment_search_paths(
@@ -186,23 +174,11 @@ impl Options {
.unwrap_or_default();
let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() {
let site_packages_paths = python_environment
.site_packages_paths(system)
.context("Failed to discover the site-packages directory");
let site_packages_paths = match site_packages_paths {
Ok(paths) => paths,
Err(err) => {
if misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!(
"Default settings failed to discover site-packages directory"
);
SitePackagesPaths::default()
} else {
return Err(err);
}
}
};
self_site_packages.concatenate(site_packages_paths)
self_site_packages.concatenate(
python_environment
.site_packages_paths(system)
.context("Failed to discover the site-packages directory")?,
)
} else {
tracing::debug!("No virtual environment found");
self_site_packages
@@ -225,7 +201,6 @@ impl Options {
.or_else(|| site_packages_paths.python_version_from_layout())
.unwrap_or_default();
// Safe mode is handled inside this function, so we just assume this can't fail
let search_paths = self.to_search_paths(
project_root,
project_name,
@@ -233,7 +208,6 @@ impl Options {
real_stdlib_path,
system,
vendored,
misconfiguration_mode,
)?;
tracing::info!(
@@ -248,7 +222,6 @@ impl Options {
})
}
#[expect(clippy::too_many_arguments)]
fn to_search_paths(
&self,
project_root: &SystemPath,
@@ -257,7 +230,6 @@ impl Options {
real_stdlib_path: Option<SystemPathBuf>,
system: &dyn System,
vendored: &VendoredFileSystem,
misconfiguration_mode: MisconfigurationMode,
) -> Result<SearchPaths, SearchPathValidationError> {
let environment = self.environment.or_default();
let src = self.src.or_default();
@@ -372,7 +344,6 @@ impl Options {
.map(|path| path.absolute(project_root, system)),
site_packages_paths: site_packages_paths.into_vec(),
real_stdlib_path,
misconfiguration_mode,
};
settings.to_search_paths(system, vendored)

View File

@@ -33,7 +33,7 @@ camino = { workspace = true }
colored = { workspace = true }
compact_str = { workspace = true }
drop_bomb = { workspace = true }
get-size2 = { workspace = true, features = ["indexmap", "ordermap"] }
get-size2 = { workspace = true, features = ["indexmap", "ordermap"]}
indexmap = { workspace = true }
itertools = { workspace = true }
ordermap = { workspace = true }
@@ -62,7 +62,7 @@ ty_test = { workspace = true }
ty_vendored = { workspace = true }
anyhow = { workspace = true }
datatest-stable = { workspace = true }
dir-test = { workspace = true }
glob = { workspace = true }
indoc = { workspace = true }
insta = { workspace = true }
@@ -76,9 +76,5 @@ schemars = ["dep:schemars", "dep:serde_json"]
serde = ["ruff_db/serde", "dep:serde", "ruff_python_ast/serde"]
testing = []
[[test]]
name = "mdtest"
harness = false
[lints]
workspace = true

View File

@@ -0,0 +1,4 @@
/// Rebuild the crate if a test file is added or removed from
pub fn main() {
println!("cargo::rerun-if-changed=resources/mdtest");
}

View File

@@ -129,8 +129,17 @@ class MDTestRunner:
check=False,
)
def _mangle_path(self, markdown_file: Path) -> str:
return (
markdown_file.as_posix()
.replace("/", "_")
.replace("-", "_")
.removesuffix(".md")
)
def _run_mdtests_for_file(self, markdown_file: Path) -> None:
test_name = f"mdtest::{markdown_file}"
path_mangled = self._mangle_path(markdown_file)
test_name = f"mdtest__{path_mangled}"
output = self._run_mdtest(["--exact", test_name], capture_output=True)
@@ -236,10 +245,16 @@ class MDTestRunner:
if rust_code_has_changed:
if self._recompile_tests("Rust code has changed, recompiling tests..."):
self._run_mdtest(self.filters)
elif vendored_typeshed_has_changed and self._recompile_tests(
"Vendored typeshed has changed, recompiling tests..."
):
self._run_mdtest(self.filters)
elif vendored_typeshed_has_changed:
if self._recompile_tests(
"Vendored typeshed has changed, recompiling tests..."
):
self._run_mdtest(self.filters)
elif new_md_files:
files = " ".join(file.as_posix() for file in new_md_files)
self._recompile_tests(
f"New Markdown test [yellow]{files}[/yellow] detected, recompiling tests..."
)
for path in new_md_files | changed_md_files:
self._run_mdtests_for_file(path)

View File

@@ -205,93 +205,3 @@ class B:
class A(B): ...
class B: ...
```
## Default argument values
### Not deferred in regular files
```py
# error: [unresolved-reference]
def f(mode: int = ParseMode.test):
pass
class ParseMode:
test = 1
```
### Deferred in stub files
Forward references in default argument values are allowed in stub files.
```pyi
def f(mode: int = ParseMode.test): ...
class ParseMode:
test: int
```
### Undefined names are still errors in stub files
```pyi
# error: [unresolved-reference]
def f(mode: int = NeverDefined.test): ...
```
## Class keyword arguments
### Not deferred in regular files
```py
# error: [unresolved-reference]
class Foo(metaclass=SomeMeta):
pass
class SomeMeta(type):
pass
```
### Deferred in stub files
Forward references in class keyword arguments are allowed in stub files.
```pyi
class Foo(metaclass=SomeMeta): ...
class SomeMeta(type): ...
```
### Undefined names are still errors in stub files
```pyi
# error: [unresolved-reference]
class Foo(metaclass=NeverDefined): ...
```
## Lambda default argument values
### Not deferred in regular files
```py
# error: [unresolved-reference]
f = lambda x=Foo(): x
class Foo:
pass
```
### Deferred in stub files
Forward references in lambda default argument values are allowed in stub files.
```pyi
f = lambda x=Foo(): x
class Foo: ...
```
### Undefined names are still errors in stub files
```pyi
# error: [unresolved-reference]
f = lambda x=NeverDefined(): x
```

View File

@@ -615,22 +615,6 @@ def _(x: type[typing.Any], y: typing.Any):
reveal_type(super(x, y)) # revealed: <super: Any, Any>
```
### Diagnostic when the invalid type is rendered very verbosely
<!-- snapshot-diagnostics -->
```py
def coinflip() -> bool:
return False
def f():
if coinflip():
class A: ...
else:
class A: ...
super(A, A()) # error: [invalid-super-argument]
```
### Instance Member Access via `super`
Accessing instance members through `super()` is not allowed.

View File

@@ -451,10 +451,6 @@ class Answer(Enum):
# revealed: tuple[Literal["YES"], Literal["NO"]]
reveal_type(enum_members(Answer))
# `nonmember` attributes are unwrapped to the inner value type when accessed.
# revealed: int
reveal_type(Answer.OTHER)
```
`member` can also be used as a decorator:

View File

@@ -738,8 +738,6 @@ def f[T](x: T, y: Not[T]) -> T:
## `Callable` parameters
### Class constructors
We can recurse into the parameters and return values of `Callable` parameters to infer
specializations of a generic function.
@@ -893,46 +891,3 @@ def _(x: list[str]):
# error: [invalid-argument-type]
reveal_type(accepts_callable(GenericClass)(x, x))
```
### Don't include identical lower/upper bounds in type mapping multiple times
This is was a performance regression reported in
[ty#1968](https://github.com/astral-sh/ty/issues/1968). Before fixing this, we would see the
`U ≤ M1 | ... | M7` upper bound 7 times. Since we intersect upper bounds before recording a single
type mapping, we would perform 7 intersections. Each intersection would require 7^2 comparisons of
the `Mx` types. We now have a simple heuristics that avoids processing any identical lower or upper
bound more than once, since we know the extra copies cannot affect the result.
```py
from typing import Callable, Generic, TypeVar, Union
class M1: ...
class M2: ...
class M3: ...
class M4: ...
class M5: ...
class M6: ...
class M7: ...
Msg = Union[M1, M2, M3, M4, M5, M6, M7]
T = TypeVar("T")
U_co = TypeVar("U_co", covariant=True)
class Stream(Generic[T]):
def apply(self, func: Callable[["Stream[T]"], "Stream[U_co]"]) -> "Stream[U_co]":
return func(self)
TMsg = TypeVar("TMsg", bound=Msg)
class Builder(Generic[TMsg]):
def build(self) -> Stream[TMsg]:
stream: Stream[TMsg] = Stream()
# TODO: no error
# error: [invalid-assignment]
stream = stream.apply(self._handler)
return stream
def _handler(self, stream: Stream[Msg]) -> Stream[Msg]:
return stream
```

View File

@@ -289,14 +289,6 @@ reveal_type(x) # revealed: <class 'A'> | <class 'B'>
class Foo(x): ...
reveal_mro(Foo) # revealed: (<class 'Foo'>, Unknown, <class 'object'>)
def f():
if returns_bool():
class C: ...
else:
class C: ...
class D(C): ... # error: [unsupported-base]
```
## `UnionType` instances are now allowed as a base

View File

@@ -104,8 +104,6 @@ class C:
value: str | None
def foo(c: C):
# The truthiness check `c.value` narrows to `str & ~AlwaysFalsy`.
# The subsequent `len(c.value)` doesn't narrow further since `str` is not narrowable by len().
if c.value and len(c.value):
reveal_type(c.value) # revealed: str & ~AlwaysFalsy
@@ -116,7 +114,7 @@ def foo(c: C):
if c.value is None or not len(c.value):
reveal_type(c.value) # revealed: str | None
else: # c.value is not None and len(c.value)
# `c.value is not None` narrows to `str`, but `str` is not narrowable by len().
# TODO: should be # `str & ~AlwaysFalsy`
reveal_type(c.value) # revealed: str
```

View File

@@ -1,131 +0,0 @@
# Narrowing for `len(..)` checks
When `len(x)` is used in a boolean context, we can narrow the type of `x` based on whether `len(x)`
is truthy (non-zero) or falsy (zero).
We apply `~AlwaysFalsy` narrowing when ANY part of the type is narrowable (string/bytes literals,
`LiteralString`, tuples). This removes types that are always falsy (like `Literal[""]`) while
leaving non-narrowable types (like `str`, `list`) unchanged.
## String literals
The intersection with `~AlwaysFalsy` simplifies to just the non-empty literal.
```py
from typing import Literal
def _(x: Literal["foo", ""]):
if len(x):
reveal_type(x) # revealed: Literal["foo"]
else:
reveal_type(x) # revealed: Literal[""]
```
## Bytes literals
```py
from typing import Literal
def _(x: Literal[b"foo", b""]):
if len(x):
reveal_type(x) # revealed: Literal[b"foo"]
else:
reveal_type(x) # revealed: Literal[b""]
```
## LiteralString
```toml
[environment]
python-version = "3.11"
```
```py
from typing import LiteralString
def _(x: LiteralString):
if len(x):
reveal_type(x) # revealed: LiteralString & ~Literal[""]
else:
reveal_type(x) # revealed: Literal[""]
```
## Tuples
Ideally we'd narrow these types further, e.g. to `tuple[int, ...] & ~tuple[()]` in the positive case
and `tuple[()]` in the negative case (see <https://github.com/astral-sh/ty/issues/560>).
```py
def _(x: tuple[int, ...]):
if len(x):
reveal_type(x) # revealed: tuple[int, ...] & ~AlwaysFalsy
else:
reveal_type(x) # revealed: tuple[int, ...] & ~AlwaysTruthy
```
## Unions of narrowable types
```py
from typing import Literal
def _(x: Literal["foo", ""] | tuple[int, ...]):
if len(x):
reveal_type(x) # revealed: Literal["foo"] | (tuple[int, ...] & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: Literal[""] | (tuple[int, ...] & ~AlwaysTruthy)
```
## Types that are not narrowed
For `str`, `list`, and other types where a subclass could have a `__bool__` that disagrees with
`__len__`, we do not narrow:
```py
def not_narrowed_str(x: str):
if len(x):
# No narrowing because `str` could be subclassed with a custom `__bool__`
reveal_type(x) # revealed: str
def not_narrowed_list(x: list[int]):
if len(x):
# No narrowing because `list` could be subclassed with a custom `__bool__`
reveal_type(x) # revealed: list[int]
```
## Mixed unions (narrowable and non-narrowable)
When a union contains both narrowable and non-narrowable types, we narrow the narrowable parts while
leaving the non-narrowable parts unchanged:
```py
from typing import Literal
def _(x: Literal["foo", ""] | list[int]):
if len(x):
# `Literal[""]` is removed, `list[int]` is unchanged
reveal_type(x) # revealed: Literal["foo"] | list[int]
else:
reveal_type(x) # revealed: Literal[""] | list[int]
```
## Narrowing away empty literals
This pattern is common when a prior truthiness check narrows a type, and then a conditional
expression adds an empty literal back:
```py
def _(lines: list[str]):
for line in lines:
if not line:
continue
reveal_type(line) # revealed: str & ~AlwaysFalsy
value = line if len(line) < 3 else ""
reveal_type(value) # revealed: (str & ~AlwaysFalsy) | Literal[""]
if len(value):
# `Literal[""]` is removed, `str & ~AlwaysFalsy` is unchanged
reveal_type(value) # revealed: str & ~AlwaysFalsy
# Accessing value[0] is safe here
_ = value[0]
```

View File

@@ -31,25 +31,17 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/mro.md
17 | class Foo(x): ...
18 |
19 | reveal_mro(Foo) # revealed: (<class 'Foo'>, Unknown, <class 'object'>)
20 |
21 | def f():
22 | if returns_bool():
23 | class C: ...
24 | else:
25 | class C: ...
26 |
27 | class D(C): ... # error: [unsupported-base]
```
# Diagnostics
```
warning[unsupported-base]: Unsupported class base
warning[unsupported-base]: Unsupported class base with type `<class 'A'> | <class 'B'>`
--> src/mdtest_snippet.py:17:11
|
16 | # error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"
17 | class Foo(x): ...
| ^ Has type `<class 'A'> | <class 'B'>`
| ^
18 |
19 | reveal_mro(Foo) # revealed: (<class 'Foo'>, Unknown, <class 'object'>)
|
@@ -58,18 +50,3 @@ info: Only class objects or `Any` are supported as class bases
info: rule `unsupported-base` is enabled by default
```
```
warning[unsupported-base]: Unsupported class base
--> src/mdtest_snippet.py:27:13
|
25 | class C: ...
26 |
27 | class D(C): ... # error: [unsupported-base]
| ^ Has type `<class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:23'> | <class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:25'>`
|
info: ty cannot resolve a consistent MRO for class `D` due to this base
info: Only class objects or `Any` are supported as class bases
info: rule `unsupported-base` is enabled by default
```

View File

@@ -47,13 +47,13 @@ info: rule `invalid-base` is enabled by default
```
```
warning[unsupported-base]: Unsupported class base
warning[unsupported-base]: Unsupported class base with type `Foo`
--> src/mdtest_snippet.py:6:11
|
4 | return ()
5 |
6 | class Bar(Foo()): ... # error: [unsupported-base]
| ^^^^^ Has type `Foo`
| ^^^^^
7 | class Bad1:
8 | def __mro_entries__(self, bases, extra_arg):
|

View File

@@ -1,39 +0,0 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: super.md - Super - Invalid Usages - Diagnostic when the invalid type is rendered very verbosely
mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
---
# Python source files
## mdtest_snippet.py
```
1 | def coinflip() -> bool:
2 | return False
3 |
4 | def f():
5 | if coinflip():
6 | class A: ...
7 | else:
8 | class A: ...
9 | super(A, A()) # error: [invalid-super-argument]
```
# Diagnostics
```
error[invalid-super-argument]: Argument is not a valid class
--> src/mdtest_snippet.py:9:5
|
7 | else:
8 | class A: ...
9 | super(A, A()) # error: [invalid-super-argument]
| ^^^^^^^^^^^^^ Argument has type `<class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:6'> | <class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:8'>`
|
info: rule `invalid-super-argument` is enabled by default
```

View File

@@ -1726,298 +1726,5 @@ reveal_type(actual_td) # revealed: ActualTypedDict
reveal_type(actual_td["name"]) # revealed: str
```
## Disjointness with other `TypedDict`s
Two `TypedDict` types are disjoint if it's impossible to come up with a third (fully-static)
`TypedDict` that's assignable to both. The simplest way to establish this is if both sides have
fields with the same name but disjoint types:
```py
from typing import TypedDict, final
from typing_extensions import ReadOnly
from ty_extensions import static_assert, is_disjoint_from
# Two simple disjoint types, to avoid relying on `@disjoint_base` special cases for built-ins like
# `int` and `str`.
@final
class Final1: ...
@final
class Final2: ...
static_assert(is_disjoint_from(Final1, Final2))
class DisjointTD1(TypedDict):
# Make this example `ReadOnly` because that actually ends up checking the field types for
# disjointness in practice. Mutable fields are stricter. We'll get to that below.
disjoint: ReadOnly[Final1]
# While we're here: It doesn't matter how many other compatible fields there are. Just the one
# incompatible field above establishes disjointness.
common1: object
common2: object
class DisjointTD2(TypedDict):
disjoint: ReadOnly[Final2]
common1: object
common2: object
static_assert(is_disjoint_from(DisjointTD1, DisjointTD2))
```
However, note that most pairs of non-final classes are *not* disjoint from each other, even if
neither inherits from the other, because we could define a third class that multiply-inherits from
both. `TypedDict` disjointness takes this into account. For example:
```py
from ty_extensions import is_assignable_to
class NonFinal1: ...
class NonFinal2: ...
class CommonSub(NonFinal1, NonFinal2): ...
static_assert(not is_disjoint_from(NonFinal1, NonFinal2))
static_assert(not is_assignable_to(NonFinal1, NonFinal2))
static_assert(is_assignable_to(CommonSub, NonFinal1))
static_assert(is_assignable_to(CommonSub, NonFinal2))
class NonDisjointTD1(TypedDict):
non_disjoint: ReadOnly[NonFinal1]
# While we're here: It doesn't matter how many "extra" fields there are, or what order the
# fields are in. Only shared field names can establish disjointness.
extra1: int
class NonDisjointTD2(TypedDict):
extra2: str
non_disjoint: ReadOnly[NonFinal2]
class CommonSubTD(TypedDict):
extra2: str
extra1: int
non_disjoint: ReadOnly[CommonSub]
# The first two TDs above are not assignable in either direction...
static_assert(not is_assignable_to(NonDisjointTD1, NonDisjointTD2))
static_assert(not is_assignable_to(NonDisjointTD2, NonDisjointTD1))
# ...but they're still not disjoint...
static_assert(not is_disjoint_from(NonDisjointTD1, NonDisjointTD2))
# ...because the third TD above is assignable to both of them.
static_assert(is_assignable_to(CommonSubTD, NonDisjointTD1))
static_assert(is_assignable_to(CommonSubTD, NonDisjointTD2))
static_assert(not is_disjoint_from(CommonSubTD, NonDisjointTD1))
static_assert(not is_disjoint_from(CommonSubTD, NonDisjointTD2))
```
We made the important fields `ReadOnly` above, because those only establish disjointness when
they're disjoint themselves. However, the rules for mutable fields are stricter. Mutable fields in
common need to have *compatible* types (in the fully-static case, equivalent types):
```py
from typing import Any, Generic, TypeVar
class IntTD(TypedDict):
x: int
class BoolTD(TypedDict):
x: bool
# `bool` is assignable to `int`, but `int` is not assignable to `bool`. If `x` was `ReadOnly` (even,
# as we'll see below, only on the `int` side), then these two TDs would not be disjoint, but in this
# mutable case they are.
static_assert(is_disjoint_from(IntTD, BoolTD))
static_assert(is_disjoint_from(BoolTD, IntTD))
# Gradual types: `int` is compatible with `bool | Any`, because that could materialize to
# `bool | int`, which is just `int`. (And `int | Any` and `bool | Any` are compatible with each
# other for the same reason.) However, `bool` is *not* compatible with `int | Any`, because there's
# no materialization that's equivalent to `bool`.
class IntOrAnyTD(TypedDict):
x: int | Any
class BoolOrAnyTD(TypedDict):
x: bool | Any
static_assert(not is_disjoint_from(IntTD, IntOrAnyTD))
static_assert(not is_disjoint_from(IntOrAnyTD, IntTD))
static_assert(not is_disjoint_from(IntTD, BoolOrAnyTD))
static_assert(not is_disjoint_from(BoolOrAnyTD, IntTD))
static_assert(not is_disjoint_from(IntOrAnyTD, BoolOrAnyTD))
static_assert(not is_disjoint_from(BoolOrAnyTD, IntOrAnyTD))
static_assert(is_disjoint_from(BoolTD, IntOrAnyTD))
static_assert(is_disjoint_from(IntOrAnyTD, BoolTD))
static_assert(not is_disjoint_from(BoolTD, BoolOrAnyTD))
static_assert(not is_disjoint_from(BoolOrAnyTD, BoolTD))
# `Any` is compatible with everything.
class AnyTD(TypedDict):
x: Any
static_assert(not is_disjoint_from(IntTD, AnyTD))
static_assert(not is_disjoint_from(AnyTD, IntTD))
static_assert(not is_disjoint_from(BoolTD, AnyTD))
static_assert(not is_disjoint_from(AnyTD, BoolTD))
static_assert(not is_disjoint_from(IntOrAnyTD, AnyTD))
static_assert(not is_disjoint_from(AnyTD, IntOrAnyTD))
static_assert(not is_disjoint_from(BoolOrAnyTD, AnyTD))
static_assert(not is_disjoint_from(AnyTD, BoolOrAnyTD))
static_assert(not is_disjoint_from(AnyTD, AnyTD))
# This works with generic `TypedDict`s too.
class TwoIntsTD(TypedDict):
x: int
y: int
class TwoBoolsTD(TypedDict):
x: bool
y: bool
class IntBoolTD(TypedDict):
x: int
y: bool
T = TypeVar("T")
class TwoGenericTD(TypedDict, Generic[T]):
x: T
y: T
static_assert(not is_disjoint_from(TwoGenericTD[Any], TwoIntsTD))
static_assert(not is_disjoint_from(TwoGenericTD[int], TwoIntsTD))
static_assert(is_disjoint_from(TwoGenericTD[bool], TwoIntsTD))
static_assert(not is_disjoint_from(TwoGenericTD[Any], TwoBoolsTD))
static_assert(is_disjoint_from(TwoGenericTD[int], TwoBoolsTD))
static_assert(not is_disjoint_from(TwoGenericTD[bool], TwoBoolsTD))
# TODO: T can't be compatible with both `int` and `bool` at the same time, so these types should be
# disjoint, regardless of the materialization of `T`.
static_assert(not is_disjoint_from(TwoGenericTD[Any], IntBoolTD))
```
If one side is mutable but the other is not, then a "third `TypedDict` that's assignable to both"
would have to have the same type as the mutable side, so we establish disjointness if that type
isn't assignable to the immutable side:
```py
class ReadOnlyIntTD(TypedDict):
x: ReadOnly[int]
class ReadOnlyBoolTD(TypedDict):
x: ReadOnly[bool]
static_assert(not is_disjoint_from(ReadOnlyIntTD, ReadOnlyBoolTD))
static_assert(not is_disjoint_from(ReadOnlyBoolTD, ReadOnlyIntTD))
static_assert(not is_disjoint_from(BoolTD, ReadOnlyIntTD))
static_assert(not is_disjoint_from(ReadOnlyIntTD, BoolTD))
static_assert(is_disjoint_from(IntTD, ReadOnlyBoolTD))
static_assert(is_disjoint_from(ReadOnlyBoolTD, IntTD))
```
With mutability above we were able to make the simplifying assumption that the "third `TypedDict`
that's assignable to both" has only mutable fields, because a mutable field is always assignable to
its immutable counterpart. However, `Required` vs `NotRequired` are more complicated, because a a
`Required` field is *not* necessarily assignable to its `NotRequired` counterpart. In particular, if
a `NotRequired` field is also mutable (intuitively, if we're allowed to `del` it), then no
`Required` field is ever assignable to it. So, if either side is `NotRequired` and mutable, and the
other side is `Required` (regardless of mutability), then that's sufficient to establish
disjointness:
```py
from typing_extensions import NotRequired
class NotRequiredIntTD(TypedDict):
x: NotRequired[int]
class NotRequiredReadOnlyIntTD(TypedDict):
x: NotRequired[ReadOnly[int]]
static_assert(is_disjoint_from(NotRequiredIntTD, IntTD))
static_assert(is_disjoint_from(IntTD, NotRequiredIntTD))
static_assert(is_disjoint_from(NotRequiredIntTD, ReadOnlyIntTD))
static_assert(is_disjoint_from(ReadOnlyIntTD, NotRequiredIntTD))
static_assert(not is_disjoint_from(NotRequiredIntTD, NotRequiredReadOnlyIntTD))
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, NotRequiredIntTD))
```
All those rules put together give us the "full disjointness table". We've pretty well tested above
that disjointness is symmetrical, so here we won't worry about asserting both directions for each
check:
```py
class NotRequiredBoolTD(TypedDict):
x: NotRequired[bool]
class NotRequiredReadOnlyBoolTD(TypedDict):
x: NotRequired[ReadOnly[bool]]
static_assert(not is_disjoint_from(IntTD, IntTD))
static_assert(is_disjoint_from(IntTD, BoolTD))
static_assert(not is_disjoint_from(IntTD, ReadOnlyIntTD))
static_assert(is_disjoint_from(IntTD, ReadOnlyBoolTD))
static_assert(is_disjoint_from(IntTD, NotRequiredIntTD))
static_assert(is_disjoint_from(IntTD, NotRequiredBoolTD))
static_assert(not is_disjoint_from(IntTD, NotRequiredReadOnlyIntTD))
static_assert(is_disjoint_from(IntTD, NotRequiredReadOnlyBoolTD))
static_assert(not is_disjoint_from(ReadOnlyIntTD, BoolTD))
static_assert(not is_disjoint_from(ReadOnlyIntTD, ReadOnlyIntTD))
static_assert(not is_disjoint_from(ReadOnlyIntTD, ReadOnlyBoolTD))
static_assert(is_disjoint_from(ReadOnlyIntTD, NotRequiredIntTD))
static_assert(is_disjoint_from(ReadOnlyIntTD, NotRequiredBoolTD))
static_assert(not is_disjoint_from(ReadOnlyIntTD, NotRequiredReadOnlyIntTD))
static_assert(not is_disjoint_from(ReadOnlyIntTD, NotRequiredReadOnlyBoolTD))
static_assert(is_disjoint_from(NotRequiredIntTD, BoolTD))
static_assert(is_disjoint_from(NotRequiredIntTD, ReadOnlyBoolTD))
static_assert(not is_disjoint_from(NotRequiredIntTD, NotRequiredIntTD))
static_assert(is_disjoint_from(NotRequiredIntTD, NotRequiredBoolTD))
static_assert(not is_disjoint_from(NotRequiredIntTD, NotRequiredReadOnlyIntTD))
static_assert(is_disjoint_from(NotRequiredIntTD, NotRequiredReadOnlyBoolTD))
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, BoolTD))
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, ReadOnlyBoolTD))
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, NotRequiredBoolTD))
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, NotRequiredReadOnlyIntTD))
static_assert(not is_disjoint_from(NotRequiredReadOnlyIntTD, NotRequiredReadOnlyBoolTD))
static_assert(not is_disjoint_from(BoolTD, BoolTD))
static_assert(not is_disjoint_from(BoolTD, ReadOnlyBoolTD))
static_assert(is_disjoint_from(BoolTD, NotRequiredBoolTD))
static_assert(not is_disjoint_from(BoolTD, NotRequiredReadOnlyBoolTD))
static_assert(not is_disjoint_from(ReadOnlyBoolTD, ReadOnlyBoolTD))
static_assert(is_disjoint_from(ReadOnlyBoolTD, NotRequiredBoolTD))
static_assert(not is_disjoint_from(ReadOnlyBoolTD, NotRequiredReadOnlyBoolTD))
static_assert(not is_disjoint_from(NotRequiredBoolTD, NotRequiredBoolTD))
static_assert(not is_disjoint_from(NotRequiredBoolTD, NotRequiredReadOnlyBoolTD))
static_assert(not is_disjoint_from(NotRequiredReadOnlyBoolTD, NotRequiredReadOnlyBoolTD))
```
## Disjointness with other types
```py
from typing import TypedDict, Mapping
from ty_extensions import static_assert, is_disjoint_from
class TD(TypedDict):
x: int
class RegularNonTD: ...
static_assert(not is_disjoint_from(TD, object))
static_assert(not is_disjoint_from(TD, Mapping[str, object]))
static_assert(is_disjoint_from(TD, Mapping[int, object]))
static_assert(is_disjoint_from(TD, RegularNonTD))
# TODO: We approximate disjointness with other types `T` by asking whether `dict[str, Any]` is
# assignable to `T`. That covers common cases like the ones above, but does it have some false
# negatives with `dict` types. A `TypedDict` is almost never assignable to a `dict` (or vice versa),
# even when all of the `TypedDict`'s field types match the `dict`'s value type (and are mutable).
# The problem is that the `TypedDict` could have been assigned to from *another* `TypedDict` with
# additional fields, and we don't usually know anything about the types or mutability of those. On
# the other hand, the assignment to `dict` can be allowed if the `TypedDict` has mutable
# `extra_items` of a compatible type. See: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-with-dict
static_assert(is_disjoint_from(TD, dict[str, int])) # error: [static-assert-error]
static_assert(is_disjoint_from(TD, dict[str, str])) # error: [static-assert-error]
```
[subtyping section]: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html

View File

@@ -17,7 +17,7 @@ pub use module_resolver::{
resolve_real_module_confident, resolve_real_shadowable_module, system_module_search_paths,
};
pub use program::{
MisconfigurationMode, Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
PythonVersionWithSource, SearchPathSettings,
};
pub use python_platform::PythonPlatform;

View File

@@ -50,7 +50,6 @@ use ruff_python_ast::{
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::typeshed::{TypeshedVersions, vendored_typeshed_versions};
use crate::program::MisconfigurationMode;
use crate::{Program, SearchPathSettings};
use super::module::{Module, ModuleKind};
@@ -571,7 +570,6 @@ impl SearchPaths {
custom_typeshed: typeshed,
site_packages_paths,
real_stdlib_path,
misconfiguration_mode,
} = settings;
let mut static_paths = vec![];
@@ -580,30 +578,12 @@ impl SearchPaths {
let path = canonicalize(path, system);
tracing::debug!("Adding extra search-path `{path}`");
match SearchPath::extra(system, path) {
Ok(path) => static_paths.push(path),
Err(err) => {
if *misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Skipping invalid extra search-path: {err}");
} else {
return Err(err);
}
}
}
static_paths.push(SearchPath::extra(system, path)?);
}
for src_root in src_roots {
tracing::debug!("Adding first-party search path `{src_root}`");
match SearchPath::first_party(system, src_root.to_path_buf()) {
Ok(path) => static_paths.push(path),
Err(err) => {
if *misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Skipping invalid first-party search-path: {err}");
} else {
return Err(err);
}
}
}
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
}
let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed {
@@ -612,31 +592,18 @@ impl SearchPaths {
let versions_path = typeshed.join("stdlib/VERSIONS");
let results = system
.read_to_string(&versions_path)
.map_err(
|error| SearchPathValidationError::FailedToReadVersionsFile {
path: versions_path,
error,
},
)
.and_then(|versions_content| Ok(versions_content.parse()?))
.and_then(|parsed| Ok((parsed, SearchPath::custom_stdlib(system, &typeshed)?)));
match results {
Ok(results) => results,
Err(err) => {
if settings.misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Skipping custom-stdlib search-path: {err}");
(
vendored_typeshed_versions(vendored),
SearchPath::vendored_stdlib(),
)
} else {
return Err(err);
}
let versions_content = system.read_to_string(&versions_path).map_err(|error| {
SearchPathValidationError::FailedToReadVersionsFile {
path: versions_path,
error,
}
}
})?;
let parsed: TypeshedVersions = versions_content.parse()?;
let search_path = SearchPath::custom_stdlib(system, &typeshed)?;
(parsed, search_path)
} else {
tracing::debug!("Using vendored stdlib");
(
@@ -646,17 +613,7 @@ impl SearchPaths {
};
let real_stdlib_path = if let Some(path) = real_stdlib_path {
match SearchPath::real_stdlib(system, path.clone()) {
Ok(path) => Some(path),
Err(err) => {
if *misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Skipping invalid real-stdlib search-path: {err}");
None
} else {
return Err(err);
}
}
}
Some(SearchPath::real_stdlib(system, path.clone())?)
} else {
None
};
@@ -665,16 +622,7 @@ impl SearchPaths {
for path in site_packages_paths {
tracing::debug!("Adding site-packages search path `{path}`");
match SearchPath::site_packages(system, path.clone()) {
Ok(path) => site_packages.push(path),
Err(err) => {
if settings.misconfiguration_mode == MisconfigurationMode::UseDefault {
tracing::debug!("Skipping invalid real-stdlib search-path: {err}");
} else {
return Err(err);
}
}
}
site_packages.push(SearchPath::site_packages(system, path.clone())?);
}
// TODO vendor typeshed's third-party stubs as well as the stdlib and

View File

@@ -163,17 +163,6 @@ impl Default for PythonVersionWithSource {
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone, get_size2::GetSize)]
pub enum MisconfigurationMode {
/// Settings Failure Is Not An Error.
///
/// This is used by the default database, which we are incentivized to make infallible,
/// while still trying to "do our best" to set things up properly where we can.
UseDefault,
/// Settings Failure Is An Error.
Fail,
}
/// Configures the search paths for module resolution.
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct SearchPathSettings {
@@ -198,9 +187,6 @@ pub struct SearchPathSettings {
/// We should ideally only ever use this for things like goto-definition,
/// where typeshed isn't the right answer.
pub real_stdlib_path: Option<SystemPathBuf>,
/// How to handle apparent misconfiguration
pub misconfiguration_mode: MisconfigurationMode,
}
impl SearchPathSettings {
@@ -218,7 +204,6 @@ impl SearchPathSettings {
custom_typeshed: None,
site_packages_paths: vec![],
real_stdlib_path: None,
misconfiguration_mode: MisconfigurationMode::Fail,
}
}

View File

@@ -2,9 +2,7 @@ use std::ops::Deref;
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::find_node::covering_node;
use ruff_python_ast::traversal::suite;
use ruff_python_ast::{self as ast, AnyNodeRef, Expr};
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange};
use crate::Db;
@@ -103,7 +101,7 @@ impl<'db> Definition<'db> {
}
/// Extract a docstring from this definition, if applicable.
/// This method returns a docstring for function, class, and attribute definitions.
/// This method returns a docstring for function and class definitions.
/// The docstring is extracted from the first statement in the body if it's a string literal.
pub fn docstring(self, db: &'db dyn Db) -> Option<String> {
let file = self.file(db);
@@ -111,16 +109,6 @@ impl<'db> Definition<'db> {
let kind = self.kind(db);
match kind {
DefinitionKind::Assignment(assign_def) => {
let assign_node = assign_def.target(&module);
attribute_docstring(&module, assign_node)
.map(|docstring_expr| docstring_expr.value.to_str().to_owned())
}
DefinitionKind::AnnotatedAssignment(assign_def) => {
let assign_node = assign_def.target(&module);
attribute_docstring(&module, assign_node)
.map(|docstring_expr| docstring_expr.value.to_str().to_owned())
}
DefinitionKind::Function(function_def) => {
let function_node = function_def.node(&module);
docstring_from_body(&function_node.body)
@@ -136,7 +124,7 @@ impl<'db> Definition<'db> {
}
}
/// Get the module-level docstring for the given file.
/// Get the module-level docstring for the given file
pub(crate) fn module_docstring(db: &dyn Db, file: File) -> Option<String> {
let module = parsed_module(db, file).load(db);
docstring_from_body(module.suite())
@@ -159,44 +147,6 @@ fn docstring_from_body(body: &[ast::Stmt]) -> Option<&ast::ExprStringLiteral> {
value.as_string_literal_expr()
}
/// Extract a docstring from an attribute.
///
/// This is a non-standardized but popular-and-supported-by-sphinx kind of docstring
/// where you just place the docstring underneath an assignment to an attribute and
/// that counts as docs.
///
/// This is annoying to extract because we have a reference to (part of) an assignment statement
/// and we need to find the statement *after it*, which is easy to say but not something the
/// AST wants to encourage.
fn attribute_docstring<'a>(
module: &'a ParsedModuleRef,
assign_lvalue: &Expr,
) -> Option<&'a ast::ExprStringLiteral> {
// Find all the ancestors of the assign lvalue
let covering_node = covering_node(module.syntax().into(), assign_lvalue.range());
// The assignment is the closest parent statement
let assign = covering_node.find_first(AnyNodeRef::is_statement).ok()?;
let parent = assign.parent()?;
let assign_node = assign.node();
// The docs must be the next statement
let parent_body = suite(assign_node, parent)?;
let next_stmt = parent_body.next_sibling()?;
// Require the docstring to be a standalone expression.
let ast::Stmt::Expr(ast::StmtExpr {
value,
range: _,
node_index: _,
}) = next_stmt
else {
return None;
};
// Only match string literals.
value.as_string_literal_expr()
}
/// One or more [`Definition`]s.
#[derive(Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub struct Definitions<'db> {

View File

@@ -369,61 +369,6 @@ fn type_excluded_by_previous_patterns<'db>(
builder.build()
}
/// Analyze a pattern predicate to determine its static truthiness.
///
/// This is a Salsa tracked function to enable memoization. Without memoization, for a match
/// statement with N cases where each case references the subject (e.g., `self`), we would
/// re-analyze each pattern O(N) times (once per reference), leading to O(N²) total work.
/// With memoization, each pattern is analyzed exactly once.
#[salsa::tracked(cycle_initial = analyze_pattern_predicate_cycle_initial, heap_size = get_size2::GetSize::get_heap_size)]
fn analyze_pattern_predicate<'db>(db: &'db dyn Db, predicate: PatternPredicate<'db>) -> Truthiness {
let subject_ty = infer_expression_type(db, predicate.subject(db), TypeContext::default());
let narrowed_subject = IntersectionBuilder::new(db)
.add_positive(subject_ty)
.add_negative(type_excluded_by_previous_patterns(db, predicate));
let narrowed_subject_ty = narrowed_subject.clone().build();
// Consider a case where we match on a subject type of `Self` with an upper bound of `Answer`,
// where `Answer` is a {YES, NO} enum. After a previous pattern matching on `NO`, the narrowed
// subject type is `Self & ~Literal[NO]`. This type is *not* equivalent to `Literal[YES]`,
// because `Self` could also specialize to `Literal[NO]` or `Never`, making the intersection
// empty. However, if the current pattern matches on `YES`, the *next* narrowed subject type
// will be `Self & ~Literal[NO] & ~Literal[YES]`, which *is* always equivalent to `Never`. This
// means that subsequent patterns can never match. And we know that if we reach this point,
// the current pattern will have to match. We return `AlwaysTrue` here, since the call to
// `analyze_single_pattern_predicate_kind` below would return `Ambiguous` in this case.
let next_narrowed_subject_ty = narrowed_subject
.add_negative(pattern_kind_to_type(db, predicate.kind(db)))
.build();
if !narrowed_subject_ty.is_never() && next_narrowed_subject_ty.is_never() {
return Truthiness::AlwaysTrue;
}
let truthiness = ReachabilityConstraints::analyze_single_pattern_predicate_kind(
db,
predicate.kind(db),
narrowed_subject_ty,
);
if truthiness == Truthiness::AlwaysTrue && predicate.guard(db).is_some() {
// Fall back to ambiguous, the guard might change the result.
// TODO: actually analyze guard truthiness
Truthiness::Ambiguous
} else {
truthiness
}
}
fn analyze_pattern_predicate_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
_predicate: PatternPredicate<'db>,
) -> Truthiness {
Truthiness::Ambiguous
}
/// A collection of reachability constraints for a given scope.
#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) struct ReachabilityConstraints {
@@ -856,6 +801,46 @@ impl ReachabilityConstraints {
}
}
fn analyze_single_pattern_predicate(db: &dyn Db, predicate: PatternPredicate) -> Truthiness {
let subject_ty = infer_expression_type(db, predicate.subject(db), TypeContext::default());
let narrowed_subject = IntersectionBuilder::new(db)
.add_positive(subject_ty)
.add_negative(type_excluded_by_previous_patterns(db, predicate));
let narrowed_subject_ty = narrowed_subject.clone().build();
// Consider a case where we match on a subject type of `Self` with an upper bound of `Answer`,
// where `Answer` is a {YES, NO} enum. After a previous pattern matching on `NO`, the narrowed
// subject type is `Self & ~Literal[NO]`. This type is *not* equivalent to `Literal[YES]`,
// because `Self` could also specialize to `Literal[NO]` or `Never`, making the intersection
// empty. However, if the current pattern matches on `YES`, the *next* narrowed subject type
// will be `Self & ~Literal[NO] & ~Literal[YES]`, which *is* always equivalent to `Never`. This
// means that subsequent patterns can never match. And we know that if we reach this point,
// the current pattern will have to match. We return `AlwaysTrue` here, since the call to
// `analyze_single_pattern_predicate_kind` below would return `Ambiguous` in this case.
let next_narrowed_subject_ty = narrowed_subject
.add_negative(pattern_kind_to_type(db, predicate.kind(db)))
.build();
if !narrowed_subject_ty.is_never() && next_narrowed_subject_ty.is_never() {
return Truthiness::AlwaysTrue;
}
let truthiness = Self::analyze_single_pattern_predicate_kind(
db,
predicate.kind(db),
narrowed_subject_ty,
);
if truthiness == Truthiness::AlwaysTrue && predicate.guard(db).is_some() {
// Fall back to ambiguous, the guard might change the result.
// TODO: actually analyze guard truthiness
Truthiness::Ambiguous
} else {
truthiness
}
}
fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness {
let _span = tracing::trace_span!("analyze_single", ?predicate).entered();
@@ -923,7 +908,7 @@ impl ReachabilityConstraints {
}
.negate_if(!predicate.is_positive)
}
PredicateNode::Pattern(inner) => analyze_pattern_predicate(db, inner),
PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner),
PredicateNode::StarImportPlaceholder(star_import) => {
let place_table = place_table(db, star_import.scope(db));
let symbol = place_table.symbol(star_import.symbol_id(db));

View File

@@ -380,7 +380,7 @@ pub struct MemberDefinition<'db> {
/// single-underscore names. This matches the order of the variants defined for
/// this enum, which is in turn picked up by the derived trait implementation
/// for `Ord`.
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
pub enum NameKind {
Normal,
Dunder,

View File

@@ -3307,6 +3307,11 @@ impl<'db> Type<'db> {
})
}
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
// TODO: Implement disjointness for TypedDict
ConstraintSet::from(false)
}
// `type[T]` is disjoint from a callable or protocol instance if its upper bound or constraints are.
(Type::SubclassOf(subclass_of), Type::Callable(_) | Type::ProtocolInstance(_))
| (Type::Callable(_) | Type::ProtocolInstance(_), Type::SubclassOf(subclass_of))
@@ -4033,34 +4038,6 @@ impl<'db> Type<'db> {
}
(Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => ConstraintSet::from(true),
(Type::TypedDict(self_typeddict), Type::TypedDict(other_typeddict)) => {
disjointness_visitor.visit((self, other), || {
self_typeddict.is_disjoint_from_impl(
db,
other_typeddict,
inferable,
disjointness_visitor,
relation_visitor,
)
})
}
// For any type `T`, if `dict[str, Any]` is not assignable to `T`, then all `TypedDict`
// types will always be disjoint from `T`. This doesn't cover all cases -- in fact
// `dict` *itself* is almost always disjoint from `TypedDict` -- but it's a good
// approximation, and some false negatives are acceptable.
(Type::TypedDict(_), other) | (other, Type::TypedDict(_)) => KnownClass::Dict
.to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()])
.has_relation_to_impl(
db,
other,
inferable,
TypeRelation::Assignability,
relation_visitor,
disjointness_visitor,
)
.negate(db),
}
}

View File

@@ -76,25 +76,15 @@ impl<'db> BoundSuperError<'db> {
BoundSuperError::InvalidPivotClassType { pivot_class } => {
if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) {
match pivot_class {
Type::GenericAlias(alias) => {
builder.into_diagnostic(format_args!(
"`types.GenericAlias` instance `{}` is not a valid class",
alias.display_with(context.db(), DisplaySettings::default()),
));
}
_ => {
let mut diagnostic =
builder.into_diagnostic("Argument is not a valid class");
diagnostic.set_primary_message(format_args!(
"Argument has type `{}`",
pivot_class.display(context.db())
));
diagnostic.set_concise_message(format_args!(
"`{}` is not a valid class",
pivot_class.display(context.db()),
));
}
}
Type::GenericAlias(alias) => builder.into_diagnostic(format_args!(
"`types.GenericAlias` instance `{}` is not a valid class",
alias.display_with(context.db(), DisplaySettings::default()),
)),
_ => builder.into_diagnostic(format_args!(
"`{pivot_class}` is not a valid class",
pivot_class = pivot_class.display(context.db()),
)),
};
}
}
BoundSuperError::FailingConditionCheck {

View File

@@ -300,7 +300,7 @@ impl<'db> UnionBuilder<'db> {
}
}
for ty in replace_with {
self.add_in_place_impl(ty, seen_aliases, None);
self.add_in_place_impl(ty, seen_aliases);
}
}
@@ -312,15 +312,10 @@ impl<'db> UnionBuilder<'db> {
/// Adds a type to this union.
pub(crate) fn add_in_place(&mut self, ty: Type<'db>) {
self.add_in_place_impl(ty, &mut vec![], None);
self.add_in_place_impl(ty, &mut vec![]);
}
pub(crate) fn add_in_place_impl(
&mut self,
ty: Type<'db>,
seen_aliases: &mut Vec<Type<'db>>,
skip_checks_after: Option<usize>,
) {
pub(crate) fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec<Type<'db>>) {
let cycle_recovery = self.cycle_recovery;
let should_widen = |literals, recursively_defined: RecursivelyDefined| {
if recursively_defined.is_yes() && cycle_recovery {
@@ -334,22 +329,8 @@ impl<'db> UnionBuilder<'db> {
Type::Union(union) => {
let new_elements = union.elements(self.db);
self.elements.reserve(new_elements.len());
// Capture the current element count to avoid comparing union elements against each other.
// The union has already been simplified, so its elements don't need redundancy checks
// between themselves, only against pre-existing elements. However, we only apply this
// optimization when not in cycle recovery AND both the builder and union have
// recursively_defined=No (indicating neither is involved in recursive type definitions
// where simplification might be incomplete).
let batch_start = if !self.cycle_recovery
&& self.recursively_defined == RecursivelyDefined::No
&& union.recursively_defined(self.db) == RecursivelyDefined::No
{
Some(self.elements.len())
} else {
None
};
for element in new_elements {
self.add_in_place_impl(*element, seen_aliases, batch_start);
self.add_in_place_impl(*element, seen_aliases);
}
self.recursively_defined = self
.recursively_defined
@@ -374,9 +355,7 @@ impl<'db> UnionBuilder<'db> {
// leave out the recursive alias. TODO surface this error.
} else {
seen_aliases.push(ty);
// Don't pass through skip_checks_after when unpacking aliases, since the alias
// value is not part of the original union batch.
self.add_in_place_impl(alias.value_type(self.db), seen_aliases, None);
self.add_in_place_impl(alias.value_type(self.db), seen_aliases);
}
}
// If adding a string literal, look for an existing `UnionElement::StringLiterals` to
@@ -386,33 +365,26 @@ impl<'db> UnionBuilder<'db> {
Type::StringLiteral(literal) => {
let mut found = None;
let mut to_remove = None;
let mut ty_negated = None;
let ty_negated = ty.negate(self.db);
for (index, element) in self.elements.iter_mut().enumerate() {
match element {
UnionElement::StringLiterals(literals) => {
if should_widen(literals.len(), self.recursively_defined) {
let replace_with = KnownClass::Str.to_instance(self.db);
self.add_in_place_impl(replace_with, seen_aliases, None);
self.add_in_place_impl(replace_with, seen_aliases);
return;
}
found = Some(literals);
continue;
}
UnionElement::Type(existing) => {
// Skip redundancy checks against elements from the same union batch.
if skip_checks_after.is_some_and(|batch_start| index >= batch_start) {
continue;
}
// e.g. `existing` could be `Literal[""] & Any`,
// and `ty` could be `Literal[""]`
if ty.is_subtype_of(self.db, *existing) {
return;
}
if existing.is_subtype_of(self.db, ty) {
to_remove = Some(index);
continue;
}
let negated = ty_negated.get_or_insert_with(|| ty.negate(self.db));
if negated.is_subtype_of(self.db, *existing) {
if ty_negated.is_subtype_of(self.db, *existing) {
// The type that includes both this new element, and its negation
// (or a supertype of its negation), must be simply `object`.
self.collapse_to_object();
@@ -438,32 +410,26 @@ impl<'db> UnionBuilder<'db> {
Type::BytesLiteral(literal) => {
let mut found = None;
let mut to_remove = None;
let mut ty_negated = None;
let ty_negated = ty.negate(self.db);
for (index, element) in self.elements.iter_mut().enumerate() {
match element {
UnionElement::BytesLiterals(literals) => {
if should_widen(literals.len(), self.recursively_defined) {
let replace_with = KnownClass::Bytes.to_instance(self.db);
self.add_in_place_impl(replace_with, seen_aliases, None);
self.add_in_place_impl(replace_with, seen_aliases);
return;
}
found = Some(literals);
continue;
}
UnionElement::Type(existing) => {
// Skip redundancy checks against elements from the same union batch.
if skip_checks_after.is_some_and(|batch_start| index >= batch_start) {
continue;
}
if ty.is_subtype_of(self.db, *existing) {
return;
}
if existing.is_subtype_of(self.db, ty) {
to_remove = Some(index);
continue;
}
let negated = ty_negated.get_or_insert_with(|| ty.negate(self.db));
if negated.is_subtype_of(self.db, *existing) {
if ty_negated.is_subtype_of(self.db, *existing) {
// The type that includes both this new element, and its negation
// (or a supertype of its negation), must be simply `object`.
self.collapse_to_object();
@@ -489,32 +455,26 @@ impl<'db> UnionBuilder<'db> {
Type::IntLiteral(literal) => {
let mut found = None;
let mut to_remove = None;
let mut ty_negated = None;
let ty_negated = ty.negate(self.db);
for (index, element) in self.elements.iter_mut().enumerate() {
match element {
UnionElement::IntLiterals(literals) => {
if should_widen(literals.len(), self.recursively_defined) {
let replace_with = KnownClass::Int.to_instance(self.db);
self.add_in_place_impl(replace_with, seen_aliases, None);
self.add_in_place_impl(replace_with, seen_aliases);
return;
}
found = Some(literals);
continue;
}
UnionElement::Type(existing) => {
// Skip redundancy checks against elements from the same union batch.
if skip_checks_after.is_some_and(|batch_start| index >= batch_start) {
continue;
}
if ty.is_subtype_of(self.db, *existing) {
return;
}
if existing.is_subtype_of(self.db, ty) {
to_remove = Some(index);
continue;
}
let negated = ty_negated.get_or_insert_with(|| ty.negate(self.db));
if negated.is_subtype_of(self.db, *existing) {
if ty_negated.is_subtype_of(self.db, *existing) {
// The type that includes both this new element, and its negation
// (or a supertype of its negation), must be simply `object`.
self.collapse_to_object();
@@ -554,12 +514,9 @@ impl<'db> UnionBuilder<'db> {
.all(|name| enum_members_in_union.contains(name));
if all_members_are_in_union {
// Don't pass through skip_checks_after when converting to enum class instance,
// since this is a transformation and not part of the original union batch.
self.add_in_place_impl(
enum_member_to_add.enum_class_instance(self.db),
seen_aliases,
None,
);
} else if !self
.elements
@@ -567,11 +524,7 @@ impl<'db> UnionBuilder<'db> {
.filter_map(UnionElement::to_type_element)
.any(|ty| Type::EnumLiteral(enum_member_to_add).is_subtype_of(self.db, ty))
{
self.push_type(
Type::EnumLiteral(enum_member_to_add),
seen_aliases,
skip_checks_after,
);
self.push_type(Type::EnumLiteral(enum_member_to_add), seen_aliases);
}
}
// Adding `object` to a union results in `object`.
@@ -579,17 +532,12 @@ impl<'db> UnionBuilder<'db> {
self.collapse_to_object();
}
_ => {
self.push_type(ty, seen_aliases, skip_checks_after);
self.push_type(ty, seen_aliases);
}
}
}
fn push_type(
&mut self,
ty: Type<'db>,
seen_aliases: &mut Vec<Type<'db>>,
skip_checks_after: Option<usize>,
) {
fn push_type(&mut self, ty: Type<'db>, seen_aliases: &mut Vec<Type<'db>>) {
let bool_pair = if let Type::BooleanLiteral(b) = ty {
Some(Type::BooleanLiteral(!b))
} else {
@@ -601,14 +549,18 @@ impl<'db> UnionBuilder<'db> {
// unpacking them.
let should_simplify_full = !matches!(ty, Type::TypeAlias(_)) && !self.cycle_recovery;
let mut ty_negated: Option<Type> = None;
let mut to_remove = SmallVec::<[usize; 2]>::new();
let ty_negated = if should_simplify_full {
ty.negate(self.db)
} else {
Type::Never // won't be used
};
for (i, element) in self.elements.iter_mut().enumerate() {
for (index, element) in self.elements.iter_mut().enumerate() {
let element_type = match element.try_reduce(self.db, ty) {
ReduceResult::KeepIf(keep) => {
if !keep {
to_remove.push(i);
to_remove.push(index);
}
continue;
}
@@ -627,39 +579,27 @@ impl<'db> UnionBuilder<'db> {
}
if Some(element_type) == bool_pair {
self.add_in_place_impl(KnownClass::Bool.to_instance(self.db), seen_aliases, None);
self.add_in_place_impl(KnownClass::Bool.to_instance(self.db), seen_aliases);
return;
}
// Skip redundancy checks when comparing elements from the same union batch.
// The union has already been simplified, so its elements don't need to be checked
// against each other.
let skip_redundancy_checks =
skip_checks_after.is_some_and(|batch_start| i >= batch_start);
// Comparing `TypedDict`s for redundancy requires iterating over their fields, which is
// problematic if some of those fields point to recursive `Union`s. To avoid cycles,
// compare `TypedDict`s by name/identity instead of using the `has_relation_to`
// machinery.
if element_type.is_typed_dict() && ty.is_typed_dict() {
if let (Type::TypedDict(element_td), Type::TypedDict(ty_td)) = (element_type, ty) {
if element_td == ty_td {
return;
}
continue;
}
if should_simplify_full
&& !skip_redundancy_checks
&& !matches!(element_type, Type::TypeAlias(_))
{
if should_simplify_full && !matches!(element_type, Type::TypeAlias(_)) {
if ty.is_redundant_with(self.db, element_type) {
return;
}
if element_type.is_redundant_with(self.db, ty) {
to_remove.push(i);
continue;
}
let negated = ty_negated.get_or_insert_with(|| ty.negate(self.db));
if negated.is_subtype_of(self.db, element_type) {
} else if element_type.is_redundant_with(self.db, ty) {
to_remove.push(index);
} else if ty_negated.is_subtype_of(self.db, element_type) {
// We add `ty` to the union. We just checked that `~ty` is a subtype of an
// existing `element`. This also means that `~ty | ty` is a subtype of
// `element | ty`, because both elements in the first union are subtypes of
@@ -674,12 +614,10 @@ impl<'db> UnionBuilder<'db> {
}
}
}
let mut to_remove = to_remove.into_iter();
if let Some(first) = to_remove.next() {
if let Some((&first, rest)) = to_remove.split_first() {
self.elements[first] = UnionElement::Type(ty);
// We iterate in descending order to keep remaining indices valid after `swap_remove`.
for index in to_remove.rev() {
for &index in rest.iter().rev() {
self.elements.swap_remove(index);
}
} else {

View File

@@ -20,9 +20,7 @@ use crate::types::bound_super::BoundSuperError;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::context::InferContext;
use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
use crate::types::enums::{
enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value,
};
use crate::types::enums::enum_metadata;
use crate::types::function::{
DataclassTransformerFlags, DataclassTransformerParams, KnownFunction,
};
@@ -2357,18 +2355,6 @@ impl<'db> ClassLiteral<'db> {
// The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s.
return Self::implicit_attribute(db, body_scope, name, MethodDecorator::ClassMethod);
}
// For enum classes, `nonmember(value)` creates a non-member attribute.
// At runtime, the enum metaclass unwraps the value, so accessing the attribute
// returns the inner value, not the `nonmember` wrapper.
if let Some(ty) = member.inner.place.ignore_possibly_undefined() {
if let Some(value_ty) = try_unwrap_nonmember_value(db, ty) {
if is_enum_class_by_inheritance(db, self) {
return Member::definitely_declared(value_ty);
}
}
}
member
}

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