Compare commits
30 Commits
gankra/fin
...
0.14.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45bbb4cbff | ||
|
|
42b972753a | ||
|
|
f7ec178400 | ||
|
|
c315164732 | ||
|
|
bb1955e98c | ||
|
|
070e08a043 | ||
|
|
bab3924833 | ||
|
|
10748b2fdb | ||
|
|
56539db520 | ||
|
|
8d32ad1cab | ||
|
|
b2a8c42b51 | ||
|
|
7bb5dd87ff | ||
|
|
06305f3c02 | ||
|
|
9cc132f098 | ||
|
|
cf8d2e35a8 | ||
|
|
0290f5dc3b | ||
|
|
5bb9ee2a9d | ||
|
|
638f230910 | ||
|
|
b36ff75a24 | ||
|
|
30c3f9aafe | ||
|
|
883701ae88 | ||
|
|
0bd7a94c27 | ||
|
|
421f88bb32 | ||
|
|
b0eb39d112 | ||
|
|
260f463edd | ||
|
|
52849a5e68 | ||
|
|
2a61fe2353 | ||
|
|
764ad8b29b | ||
|
|
85af715880 | ||
|
|
b0bc990cbf |
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@2e1816eac09c90140b1ba51d19afc5f59da460f5"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@55df3c868f3fa9ab34cff0498dd6106722aac205"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@2e1816eac09c90140b1ba51d19afc5f59da460f5"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,5 +1,54 @@
|
||||
# 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
29
Cargo.lock
generated
@@ -1004,27 +1004,6 @@ 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"
|
||||
@@ -2908,7 +2887,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.9"
|
||||
version = "0.14.10"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -3166,7 +3145,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.9"
|
||||
version = "0.14.10"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3525,7 +3504,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.9"
|
||||
version = "0.14.10"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -4513,7 +4492,7 @@ dependencies = [
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"compact_str",
|
||||
"dir-test",
|
||||
"datatest-stable",
|
||||
"drop_bomb",
|
||||
"get-size2",
|
||||
"glob",
|
||||
|
||||
@@ -82,7 +82,6 @@ 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" }
|
||||
|
||||
@@ -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.9/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.9/install.ps1 | iex"
|
||||
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"
|
||||
```
|
||||
|
||||
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.9
|
||||
rev: v0.14.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -4,6 +4,7 @@ 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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.9"
|
||||
version = "0.14.10"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use glob::PatternError;
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::panic::RefUnwindSafe;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -20,18 +21,44 @@ use super::walk_directory::WalkDirectoryBuilder;
|
||||
///
|
||||
/// ## Warning
|
||||
/// Don't use this system for production code. It's intended for testing only.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct TestSystem {
|
||||
inner: Arc<dyn WritableSystem + RefUnwindSafe + Send + Sync>,
|
||||
/// Environment variable overrides. If a key is present here, it takes precedence
|
||||
/// over the inner system's environment variables.
|
||||
env_overrides: Arc<Mutex<FxHashMap<String, Option<String>>>>,
|
||||
}
|
||||
|
||||
impl Clone for TestSystem {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
env_overrides: self.env_overrides.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestSystem {
|
||||
pub fn new(inner: impl WritableSystem + RefUnwindSafe + Send + Sync + 'static) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(inner),
|
||||
env_overrides: Arc::new(Mutex::new(FxHashMap::default())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets an environment variable override. This takes precedence over the inner system.
|
||||
pub fn set_env_var(&self, name: impl Into<String>, value: impl Into<String>) {
|
||||
self.env_overrides
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(name.into(), Some(value.into()));
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Returns the [`InMemorySystem`].
|
||||
///
|
||||
/// ## Panics
|
||||
@@ -147,6 +174,18 @@ impl System for TestSystem {
|
||||
self.system().case_sensitivity()
|
||||
}
|
||||
|
||||
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
||||
// Check overrides first
|
||||
if let Some(override_value) = self.env_overrides.lock().unwrap().get(name) {
|
||||
return match override_value {
|
||||
Some(value) => Ok(value.clone()),
|
||||
None => Err(std::env::VarError::NotPresent),
|
||||
};
|
||||
}
|
||||
// Fall back to inner system
|
||||
self.system().env_var(name)
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn System> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
@@ -156,6 +195,7 @@ impl Default for TestSystem {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(InMemorySystem::default()),
|
||||
env_overrides: Arc::new(Mutex::new(FxHashMap::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.9"
|
||||
version = "0.14.10"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
66
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py
vendored
Normal file
66
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC004.py
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
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"
|
||||
)
|
||||
@@ -9,3 +9,15 @@ 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
|
||||
|
||||
@@ -136,4 +136,38 @@ 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)
|
||||
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")
|
||||
|
||||
@@ -132,7 +132,6 @@ async def c():
|
||||
# Non-errors
|
||||
###
|
||||
|
||||
# False-negative: RustPython doesn't parse the `\N{snowman}`.
|
||||
"\N{snowman} {}".format(a)
|
||||
|
||||
"{".format(a)
|
||||
@@ -276,3 +275,6 @@ if __name__ == "__main__":
|
||||
number = 0
|
||||
string = "{}".format(number := number + 1)
|
||||
print(string)
|
||||
|
||||
# Unicode escape
|
||||
"\N{angle}AOB = {angle}°".format(angle=180)
|
||||
|
||||
@@ -138,5 +138,6 @@ 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("file.txt", encoding="utf-8") as f:
|
||||
with open("file1.txt", encoding="utf-8") as f:
|
||||
contents: str = process_contents(f.read())
|
||||
|
||||
8
crates/ruff_linter/resources/test/fixtures/refurb/FURB101_1.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/refurb/FURB101_1.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
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()
|
||||
26
crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py
vendored
Normal file
26
crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
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")
|
||||
38
crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py
vendored
Normal file
38
crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
a: int = 1
|
||||
def f1():
|
||||
global a
|
||||
a: str = "foo" # error
|
||||
|
||||
b: int = 1
|
||||
def outer():
|
||||
def inner():
|
||||
global b
|
||||
b: str = "nested" # error
|
||||
|
||||
c: int = 1
|
||||
def f2():
|
||||
global c
|
||||
c: list[str] = [] # error
|
||||
|
||||
d: int = 1
|
||||
def f3():
|
||||
global d
|
||||
d: str # error
|
||||
|
||||
e: int = 1
|
||||
def f4():
|
||||
e: str = "happy" # okay
|
||||
|
||||
global f
|
||||
f: int = 1 # okay
|
||||
|
||||
g: int = 1
|
||||
global g # error
|
||||
|
||||
class C:
|
||||
x: str
|
||||
global x # error
|
||||
|
||||
class D:
|
||||
global x # error
|
||||
x: str
|
||||
@@ -214,6 +214,13 @@ 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);
|
||||
@@ -1329,6 +1336,13 @@ 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);
|
||||
}
|
||||
|
||||
@@ -454,6 +454,7 @@ 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,
|
||||
|
||||
@@ -1001,6 +1001,7 @@ mod tests {
|
||||
#[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)]
|
||||
#[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("annotated_global.py"), PythonVersion::PY314)]
|
||||
fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"semantic_syntax_error_{}_{}",
|
||||
|
||||
@@ -22,6 +22,7 @@ static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
# Case-sensitive
|
||||
pyright
|
||||
| pyrefly
|
||||
| ruff\s*:\s*(disable|enable)
|
||||
| mypy:
|
||||
| type:\s*ignore
|
||||
| SPDX-License-Identifier:
|
||||
@@ -148,6 +149,8 @@ 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",
|
||||
&[]
|
||||
|
||||
@@ -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`.
|
||||
fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
pub(crate) fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ mod async_zero_sleep;
|
||||
mod blocking_http_call;
|
||||
mod blocking_http_call_httpx;
|
||||
mod blocking_input;
|
||||
mod blocking_open_call;
|
||||
pub(crate) mod blocking_open_call;
|
||||
mod blocking_path_methods;
|
||||
mod blocking_process_invocation;
|
||||
mod blocking_sleep;
|
||||
|
||||
@@ -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`] does not perform any escaping, so passing dynamic
|
||||
/// [`markupsafe.Markup`][markupsafe-markup] does not perform any escaping, so passing dynamic
|
||||
/// content, like f-strings, variables or interpolated strings will potentially
|
||||
/// lead to XSS vulnerabilities.
|
||||
///
|
||||
|
||||
@@ -32,6 +32,10 @@ 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(
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
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())],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
pub(crate) use collection_literal::*;
|
||||
pub(crate) use explicit::*;
|
||||
pub(crate) use implicit::*;
|
||||
|
||||
mod collection_literal;
|
||||
mod explicit;
|
||||
mod implicit;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
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
|
||||
@@ -125,6 +125,9 @@ 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
|
||||
@@ -181,6 +184,8 @@ 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());
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@ 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_call(checker: &Checker) -> bool {
|
||||
pub(crate) fn is_top_level_expression_in_statement(checker: &Checker) -> bool {
|
||||
checker.semantic().current_expression_parent().is_none()
|
||||
&& checker.semantic().current_statement().is_expr_stmt()
|
||||
}
|
||||
|
||||
@@ -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_call;
|
||||
use crate::rules::flake8_use_pathlib::helpers::is_top_level_expression_in_statement;
|
||||
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_call(checker)
|
||||
|| !is_top_level_expression_in_statement(checker)
|
||||
{
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -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_call,
|
||||
is_top_level_expression_in_statement,
|
||||
};
|
||||
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_call(checker) {
|
||||
let applicability = if !is_top_level_expression_in_statement(checker) {
|
||||
// Unsafe because the return type changes (str/bytes -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -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_call,
|
||||
is_keyword_only_argument_non_default, is_top_level_expression_in_statement,
|
||||
};
|
||||
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_call(checker) {
|
||||
let applicability = if !is_top_level_expression_in_statement(checker) {
|
||||
// Unsafe because the return type changes (None -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -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_call,
|
||||
is_keyword_only_argument_non_default, is_top_level_expression_in_statement,
|
||||
};
|
||||
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_call(checker) {
|
||||
let applicability = if !is_top_level_expression_in_statement(checker) {
|
||||
// Unsafe because the return type changes (None -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
|
||||
@@ -567,5 +567,64 @@ 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()`
|
||||
|
||||
@@ -1037,5 +1037,142 @@ 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
|
||||
|
||||
@@ -902,56 +902,76 @@ help: Convert to f-string
|
||||
132 | # Non-errors
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:160:1
|
||||
--> UP032_0.py:135:1
|
||||
|
|
||||
158 | r'"\N{snowman} {}".format(a)'
|
||||
159 |
|
||||
160 | / "123456789 {}".format(
|
||||
161 | | 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
162 | | )
|
||||
| |_^
|
||||
163 |
|
||||
164 | """
|
||||
133 | ###
|
||||
134 |
|
||||
135 | "\N{snowman} {}".format(a)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
136 |
|
||||
137 | "{".format(a)
|
||||
|
|
||||
help: Convert to f-string
|
||||
157 |
|
||||
158 | r'"\N{snowman} {}".format(a)'
|
||||
159 |
|
||||
132 | # Non-errors
|
||||
133 | ###
|
||||
134 |
|
||||
- "\N{snowman} {}".format(a)
|
||||
135 + f"\N{snowman} {a}"
|
||||
136 |
|
||||
137 | "{".format(a)
|
||||
138 |
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:159:1
|
||||
|
|
||||
157 | r'"\N{snowman} {}".format(a)'
|
||||
158 |
|
||||
159 | / "123456789 {}".format(
|
||||
160 | | 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
161 | | )
|
||||
| |_^
|
||||
162 |
|
||||
163 | """
|
||||
|
|
||||
help: Convert to f-string
|
||||
156 |
|
||||
157 | r'"\N{snowman} {}".format(a)'
|
||||
158 |
|
||||
- "123456789 {}".format(
|
||||
- 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
- )
|
||||
160 + f"123456789 {11111111111111111111111111111111111111111111111111111111111111111111111111}"
|
||||
161 |
|
||||
162 | """
|
||||
163 | {}
|
||||
159 + f"123456789 {11111111111111111111111111111111111111111111111111111111111111111111111111}"
|
||||
160 |
|
||||
161 | """
|
||||
162 | {}
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:164:1
|
||||
--> UP032_0.py:163:1
|
||||
|
|
||||
162 | )
|
||||
163 |
|
||||
164 | / """
|
||||
161 | )
|
||||
162 |
|
||||
163 | / """
|
||||
164 | | {}
|
||||
165 | | {}
|
||||
166 | | {}
|
||||
167 | | {}
|
||||
168 | | """.format(
|
||||
169 | | 1,
|
||||
170 | | 2,
|
||||
171 | | 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
172 | | )
|
||||
167 | | """.format(
|
||||
168 | | 1,
|
||||
169 | | 2,
|
||||
170 | | 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
171 | | )
|
||||
| |_^
|
||||
173 |
|
||||
174 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
172 |
|
||||
173 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
|
|
||||
help: Convert to f-string
|
||||
161 | 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
162 | )
|
||||
163 |
|
||||
164 + f"""
|
||||
165 + {1}
|
||||
166 + {2}
|
||||
167 + {111111111111111111111111111111111111111111111111111111111111111111111111111111111111111}
|
||||
168 | """
|
||||
160 | 11111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
161 | )
|
||||
162 |
|
||||
163 + f"""
|
||||
164 + {1}
|
||||
165 + {2}
|
||||
166 + {111111111111111111111111111111111111111111111111111111111111111111111111111111111111111}
|
||||
167 | """
|
||||
- {}
|
||||
- {}
|
||||
- {}
|
||||
@@ -960,392 +980,408 @@ help: Convert to f-string
|
||||
- 2,
|
||||
- 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
- )
|
||||
169 |
|
||||
170 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
171 | """.format(
|
||||
168 |
|
||||
169 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
170 | """.format(
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:174:84
|
||||
--> UP032_0.py:173:84
|
||||
|
|
||||
172 | )
|
||||
173 |
|
||||
174 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
171 | )
|
||||
172 |
|
||||
173 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
| ____________________________________________________________________________________^
|
||||
175 | | """.format(
|
||||
176 | | 111111
|
||||
177 | | )
|
||||
174 | | """.format(
|
||||
175 | | 111111
|
||||
176 | | )
|
||||
| |_^
|
||||
178 |
|
||||
179 | "{}".format(
|
||||
177 |
|
||||
178 | "{}".format(
|
||||
|
|
||||
help: Convert to f-string
|
||||
171 | 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
172 | )
|
||||
173 |
|
||||
170 | 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
|
||||
171 | )
|
||||
172 |
|
||||
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
|
||||
- """.format(
|
||||
- 111111
|
||||
- )
|
||||
174 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = f"""{111111}
|
||||
175 + """
|
||||
176 |
|
||||
177 | "{}".format(
|
||||
178 | [
|
||||
173 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = f"""{111111}
|
||||
174 + """
|
||||
175 |
|
||||
176 | "{}".format(
|
||||
177 | [
|
||||
|
||||
UP032 Use f-string instead of `format` call
|
||||
--> UP032_0.py:202:1
|
||||
--> UP032_0.py:201:1
|
||||
|
|
||||
200 | "{}".format(**c)
|
||||
201 |
|
||||
202 | / "{}".format(
|
||||
203 | | 1 # comment
|
||||
204 | | )
|
||||
199 | "{}".format(**c)
|
||||
200 |
|
||||
201 | / "{}".format(
|
||||
202 | | 1 # comment
|
||||
203 | | )
|
||||
| |_^
|
||||
|
|
||||
help: Convert to f-string
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:209:1
|
||||
--> UP032_0.py:208:1
|
||||
|
|
||||
207 | # The fixed string will exceed the line length, but it's still smaller than the
|
||||
208 | # existing line length, so it's fine.
|
||||
209 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
206 | # The fixed string will exceed the line length, but it's still smaller than the
|
||||
207 | # existing line length, so it's fine.
|
||||
208 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
210 |
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
209 |
|
||||
210 | # When fixing, trim the trailing empty string.
|
||||
|
|
||||
help: Convert to f-string
|
||||
206 |
|
||||
207 | # The fixed string will exceed the line length, but it's still smaller than the
|
||||
208 | # existing line length, so it's fine.
|
||||
205 |
|
||||
206 | # The fixed string will exceed the line length, but it's still smaller than the
|
||||
207 | # existing line length, so it's fine.
|
||||
- "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
209 + f"<Customer: {self.internal_ids}, {self.external_ids}, {self.properties}, {self.tags}, {self.others}>"
|
||||
210 |
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
212 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
208 + f"<Customer: {self.internal_ids}, {self.external_ids}, {self.properties}, {self.tags}, {self.others}>"
|
||||
209 |
|
||||
210 | # When fixing, trim the trailing empty string.
|
||||
211 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:212:18
|
||||
--> UP032_0.py:211:18
|
||||
|
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
212 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
210 | # When fixing, trim the trailing empty string.
|
||||
211 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
| __________________^
|
||||
213 | | "".format(new_dict, d))
|
||||
212 | | "".format(new_dict, d))
|
||||
| |_______________________________________^
|
||||
214 |
|
||||
215 | # When fixing, trim the trailing empty string.
|
||||
213 |
|
||||
214 | # When fixing, trim the trailing empty string.
|
||||
|
|
||||
help: Convert to f-string
|
||||
209 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
210 |
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
208 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
209 |
|
||||
210 | # When fixing, trim the trailing empty string.
|
||||
- raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
- "".format(new_dict, d))
|
||||
212 + raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}")
|
||||
211 + raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}")
|
||||
212 |
|
||||
213 | # When fixing, trim the trailing empty string.
|
||||
214 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:215:18
|
||||
|
|
||||
214 | # When fixing, trim the trailing empty string.
|
||||
215 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
| __________________^
|
||||
216 | | .format(new_dict, d))
|
||||
| |_____________________________________^
|
||||
217 |
|
||||
218 | raise ValueError(
|
||||
|
|
||||
help: Convert to f-string
|
||||
212 | "".format(new_dict, d))
|
||||
213 |
|
||||
214 | # When fixing, trim the trailing empty string.
|
||||
215 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:216:18
|
||||
|
|
||||
215 | # When fixing, trim the trailing empty string.
|
||||
216 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
| __________________^
|
||||
217 | | .format(new_dict, d))
|
||||
| |_____________________________________^
|
||||
218 |
|
||||
219 | raise ValueError(
|
||||
|
|
||||
help: Convert to f-string
|
||||
213 | "".format(new_dict, d))
|
||||
214 |
|
||||
215 | # When fixing, trim the trailing empty string.
|
||||
- raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
- .format(new_dict, d))
|
||||
216 + raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
217 + )
|
||||
218 |
|
||||
219 | raise ValueError(
|
||||
220 | "Conflicting configuration dicts: {!r} {!r}"
|
||||
215 + raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
216 + )
|
||||
217 |
|
||||
218 | raise ValueError(
|
||||
219 | "Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:220:5
|
||||
--> UP032_0.py:219:5
|
||||
|
|
||||
219 | raise ValueError(
|
||||
220 | / "Conflicting configuration dicts: {!r} {!r}"
|
||||
221 | | "".format(new_dict, d)
|
||||
218 | raise ValueError(
|
||||
219 | / "Conflicting configuration dicts: {!r} {!r}"
|
||||
220 | | "".format(new_dict, d)
|
||||
| |__________________________^
|
||||
222 | )
|
||||
221 | )
|
||||
|
|
||||
help: Convert to f-string
|
||||
217 | .format(new_dict, d))
|
||||
218 |
|
||||
219 | raise ValueError(
|
||||
216 | .format(new_dict, d))
|
||||
217 |
|
||||
218 | raise ValueError(
|
||||
- "Conflicting configuration dicts: {!r} {!r}"
|
||||
- "".format(new_dict, d)
|
||||
220 + f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
219 + f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
220 | )
|
||||
221 |
|
||||
222 | raise ValueError(
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:224:5
|
||||
|
|
||||
223 | raise ValueError(
|
||||
224 | / "Conflicting configuration dicts: {!r} {!r}"
|
||||
225 | | "".format(new_dict, d)
|
||||
| |__________________________^
|
||||
226 |
|
||||
227 | )
|
||||
|
|
||||
help: Convert to f-string
|
||||
221 | )
|
||||
222 |
|
||||
223 | raise ValueError(
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:225:5
|
||||
|
|
||||
224 | raise ValueError(
|
||||
225 | / "Conflicting configuration dicts: {!r} {!r}"
|
||||
226 | | "".format(new_dict, d)
|
||||
| |__________________________^
|
||||
227 |
|
||||
228 | )
|
||||
|
|
||||
help: Convert to f-string
|
||||
222 | )
|
||||
223 |
|
||||
224 | raise ValueError(
|
||||
- "Conflicting configuration dicts: {!r} {!r}"
|
||||
- "".format(new_dict, d)
|
||||
225 + f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
226 |
|
||||
227 | )
|
||||
228 |
|
||||
224 + f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
225 |
|
||||
226 | )
|
||||
227 |
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:231:1
|
||||
--> UP032_0.py:230:1
|
||||
|
|
||||
230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
|
||||
231 | / (
|
||||
232 | | "{}"
|
||||
233 | | "{{}}"
|
||||
234 | | ).format(a)
|
||||
229 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
|
||||
230 | / (
|
||||
231 | | "{}"
|
||||
232 | | "{{}}"
|
||||
233 | | ).format(a)
|
||||
| |___________^
|
||||
235 |
|
||||
236 | ("{}" "{{}}").format(a)
|
||||
234 |
|
||||
235 | ("{}" "{{}}").format(a)
|
||||
|
|
||||
help: Convert to f-string
|
||||
229 |
|
||||
230 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
|
||||
231 | (
|
||||
232 + f"{a}"
|
||||
233 | "{}"
|
||||
228 |
|
||||
229 | # The first string will be converted to an f-string and the curly braces in the second should be converted to be unescaped
|
||||
230 | (
|
||||
231 + f"{a}"
|
||||
232 | "{}"
|
||||
- "{{}}"
|
||||
- ).format(a)
|
||||
234 + )
|
||||
235 |
|
||||
236 | ("{}" "{{}}").format(a)
|
||||
237 |
|
||||
233 + )
|
||||
234 |
|
||||
235 | ("{}" "{{}}").format(a)
|
||||
236 |
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:236:1
|
||||
--> UP032_0.py:235:1
|
||||
|
|
||||
234 | ).format(a)
|
||||
235 |
|
||||
236 | ("{}" "{{}}").format(a)
|
||||
233 | ).format(a)
|
||||
234 |
|
||||
235 | ("{}" "{{}}").format(a)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Convert to f-string
|
||||
233 | "{{}}"
|
||||
234 | ).format(a)
|
||||
235 |
|
||||
232 | "{{}}"
|
||||
233 | ).format(a)
|
||||
234 |
|
||||
- ("{}" "{{}}").format(a)
|
||||
236 + (f"{a}" "{}")
|
||||
235 + (f"{a}" "{}")
|
||||
236 |
|
||||
237 |
|
||||
238 |
|
||||
239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
238 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:240:1
|
||||
--> UP032_0.py:239:1
|
||||
|
|
||||
239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
240 | / (
|
||||
241 | | "{}"
|
||||
242 | | "{{{}}}"
|
||||
243 | | ).format(a, b)
|
||||
238 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
239 | / (
|
||||
240 | | "{}"
|
||||
241 | | "{{{}}}"
|
||||
242 | | ).format(a, b)
|
||||
| |______________^
|
||||
244 |
|
||||
245 | ("{}" "{{{}}}").format(a, b)
|
||||
243 |
|
||||
244 | ("{}" "{{{}}}").format(a, b)
|
||||
|
|
||||
help: Convert to f-string
|
||||
238 |
|
||||
239 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
240 | (
|
||||
237 |
|
||||
238 | # Both strings will be converted to an f-string and the curly braces in the second should left escaped
|
||||
239 | (
|
||||
- "{}"
|
||||
- "{{{}}}"
|
||||
- ).format(a, b)
|
||||
241 + f"{a}"
|
||||
242 + f"{{{b}}}"
|
||||
243 + )
|
||||
244 |
|
||||
245 | ("{}" "{{{}}}").format(a, b)
|
||||
246 |
|
||||
240 + f"{a}"
|
||||
241 + f"{{{b}}}"
|
||||
242 + )
|
||||
243 |
|
||||
244 | ("{}" "{{{}}}").format(a, b)
|
||||
245 |
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:245:1
|
||||
--> UP032_0.py:244:1
|
||||
|
|
||||
243 | ).format(a, b)
|
||||
244 |
|
||||
245 | ("{}" "{{{}}}").format(a, b)
|
||||
242 | ).format(a, b)
|
||||
243 |
|
||||
244 | ("{}" "{{{}}}").format(a, b)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
246 |
|
||||
247 | # The dictionary should be parenthesized.
|
||||
245 |
|
||||
246 | # The dictionary should be parenthesized.
|
||||
|
|
||||
help: Convert to f-string
|
||||
242 | "{{{}}}"
|
||||
243 | ).format(a, b)
|
||||
244 |
|
||||
241 | "{{{}}}"
|
||||
242 | ).format(a, b)
|
||||
243 |
|
||||
- ("{}" "{{{}}}").format(a, b)
|
||||
245 + (f"{a}" f"{{{b}}}")
|
||||
246 |
|
||||
247 | # The dictionary should be parenthesized.
|
||||
248 | "{}".format({0: 1}[0])
|
||||
244 + (f"{a}" f"{{{b}}}")
|
||||
245 |
|
||||
246 | # The dictionary should be parenthesized.
|
||||
247 | "{}".format({0: 1}[0])
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:248:1
|
||||
--> UP032_0.py:247:1
|
||||
|
|
||||
247 | # The dictionary should be parenthesized.
|
||||
248 | "{}".format({0: 1}[0])
|
||||
246 | # The dictionary should be parenthesized.
|
||||
247 | "{}".format({0: 1}[0])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
249 |
|
||||
250 | # The dictionary should be parenthesized.
|
||||
248 |
|
||||
249 | # The dictionary should be parenthesized.
|
||||
|
|
||||
help: Convert to f-string
|
||||
245 | ("{}" "{{{}}}").format(a, b)
|
||||
246 |
|
||||
247 | # The dictionary should be parenthesized.
|
||||
244 | ("{}" "{{{}}}").format(a, b)
|
||||
245 |
|
||||
246 | # The dictionary should be parenthesized.
|
||||
- "{}".format({0: 1}[0])
|
||||
248 + f"{({0: 1}[0])}"
|
||||
249 |
|
||||
250 | # The dictionary should be parenthesized.
|
||||
251 | "{}".format({0: 1}.bar)
|
||||
247 + f"{({0: 1}[0])}"
|
||||
248 |
|
||||
249 | # The dictionary should be parenthesized.
|
||||
250 | "{}".format({0: 1}.bar)
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:251:1
|
||||
--> UP032_0.py:250:1
|
||||
|
|
||||
250 | # The dictionary should be parenthesized.
|
||||
251 | "{}".format({0: 1}.bar)
|
||||
249 | # The dictionary should be parenthesized.
|
||||
250 | "{}".format({0: 1}.bar)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
252 |
|
||||
253 | # The dictionary should be parenthesized.
|
||||
251 |
|
||||
252 | # The dictionary should be parenthesized.
|
||||
|
|
||||
help: Convert to f-string
|
||||
248 | "{}".format({0: 1}[0])
|
||||
249 |
|
||||
250 | # The dictionary should be parenthesized.
|
||||
247 | "{}".format({0: 1}[0])
|
||||
248 |
|
||||
249 | # The dictionary should be parenthesized.
|
||||
- "{}".format({0: 1}.bar)
|
||||
251 + f"{({0: 1}.bar)}"
|
||||
252 |
|
||||
253 | # The dictionary should be parenthesized.
|
||||
254 | "{}".format({0: 1}())
|
||||
250 + f"{({0: 1}.bar)}"
|
||||
251 |
|
||||
252 | # The dictionary should be parenthesized.
|
||||
253 | "{}".format({0: 1}())
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:254:1
|
||||
--> UP032_0.py:253:1
|
||||
|
|
||||
253 | # The dictionary should be parenthesized.
|
||||
254 | "{}".format({0: 1}())
|
||||
252 | # The dictionary should be parenthesized.
|
||||
253 | "{}".format({0: 1}())
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
255 |
|
||||
256 | # The string shouldn't be converted, since it would require repeating the function call.
|
||||
254 |
|
||||
255 | # The string shouldn't be converted, since it would require repeating the function call.
|
||||
|
|
||||
help: Convert to f-string
|
||||
251 | "{}".format({0: 1}.bar)
|
||||
252 |
|
||||
253 | # The dictionary should be parenthesized.
|
||||
250 | "{}".format({0: 1}.bar)
|
||||
251 |
|
||||
252 | # The dictionary should be parenthesized.
|
||||
- "{}".format({0: 1}())
|
||||
254 + f"{({0: 1}())}"
|
||||
255 |
|
||||
256 | # The string shouldn't be converted, since it would require repeating the function call.
|
||||
257 | "{x} {x}".format(x=foo())
|
||||
253 + f"{({0: 1}())}"
|
||||
254 |
|
||||
255 | # The string shouldn't be converted, since it would require repeating the function call.
|
||||
256 | "{x} {x}".format(x=foo())
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:261:1
|
||||
--> UP032_0.py:260:1
|
||||
|
|
||||
260 | # The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
261 | "{0} {1}".format(foo(), foo())
|
||||
259 | # The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
260 | "{0} {1}".format(foo(), foo())
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
262 |
|
||||
263 | # The call should be removed, but the string itself should remain.
|
||||
261 |
|
||||
262 | # The call should be removed, but the string itself should remain.
|
||||
|
|
||||
help: Convert to f-string
|
||||
258 | "{0} {0}".format(foo())
|
||||
259 |
|
||||
260 | # The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
257 | "{0} {0}".format(foo())
|
||||
258 |
|
||||
259 | # The string _should_ be converted, since the function call is repeated in the arguments.
|
||||
- "{0} {1}".format(foo(), foo())
|
||||
261 + f"{foo()} {foo()}"
|
||||
262 |
|
||||
263 | # The call should be removed, but the string itself should remain.
|
||||
264 | ''.format(self.project)
|
||||
260 + f"{foo()} {foo()}"
|
||||
261 |
|
||||
262 | # The call should be removed, but the string itself should remain.
|
||||
263 | ''.format(self.project)
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:264:1
|
||||
--> UP032_0.py:263:1
|
||||
|
|
||||
263 | # The call should be removed, but the string itself should remain.
|
||||
264 | ''.format(self.project)
|
||||
262 | # The call should be removed, but the string itself should remain.
|
||||
263 | ''.format(self.project)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
265 |
|
||||
266 | # The call should be removed, but the string itself should remain.
|
||||
264 |
|
||||
265 | # The call should be removed, but the string itself should remain.
|
||||
|
|
||||
help: Convert to f-string
|
||||
261 | "{0} {1}".format(foo(), foo())
|
||||
262 |
|
||||
263 | # The call should be removed, but the string itself should remain.
|
||||
260 | "{0} {1}".format(foo(), foo())
|
||||
261 |
|
||||
262 | # The call should be removed, but the string itself should remain.
|
||||
- ''.format(self.project)
|
||||
264 + ''
|
||||
265 |
|
||||
266 | # The call should be removed, but the string itself should remain.
|
||||
267 | "".format(self.project)
|
||||
263 + ''
|
||||
264 |
|
||||
265 | # The call should be removed, but the string itself should remain.
|
||||
266 | "".format(self.project)
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:267:1
|
||||
--> UP032_0.py:266:1
|
||||
|
|
||||
266 | # The call should be removed, but the string itself should remain.
|
||||
267 | "".format(self.project)
|
||||
265 | # The call should be removed, but the string itself should remain.
|
||||
266 | "".format(self.project)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
268 |
|
||||
269 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
267 |
|
||||
268 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
|
|
||||
help: Convert to f-string
|
||||
264 | ''.format(self.project)
|
||||
265 |
|
||||
266 | # The call should be removed, but the string itself should remain.
|
||||
263 | ''.format(self.project)
|
||||
264 |
|
||||
265 | # The call should be removed, but the string itself should remain.
|
||||
- "".format(self.project)
|
||||
267 + ""
|
||||
268 |
|
||||
269 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
270 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
266 + ""
|
||||
267 |
|
||||
268 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
269 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:271:5
|
||||
--> UP032_0.py:270:5
|
||||
|
|
||||
269 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
270 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
271 | x: "'{} + {}'.format(x, y)"
|
||||
268 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
269 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
270 | x: "'{} + {}'.format(x, y)"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
272 |
|
||||
273 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
||||
271 |
|
||||
272 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
||||
|
|
||||
help: Convert to f-string
|
||||
268 |
|
||||
269 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
270 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
267 |
|
||||
268 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
269 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
- x: "'{} + {}'.format(x, y)"
|
||||
271 + x: "f'{x} + {y}'"
|
||||
272 |
|
||||
273 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
||||
274 | # Fix should parenthesize walrus
|
||||
270 + x: "f'{x} + {y}'"
|
||||
271 |
|
||||
272 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
||||
273 | # Fix should parenthesize walrus
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:277:14
|
||||
--> UP032_0.py:276:14
|
||||
|
|
||||
275 | if __name__ == "__main__":
|
||||
276 | number = 0
|
||||
277 | string = "{}".format(number := number + 1)
|
||||
274 | if __name__ == "__main__":
|
||||
275 | number = 0
|
||||
276 | string = "{}".format(number := number + 1)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
278 | print(string)
|
||||
277 | print(string)
|
||||
|
|
||||
help: Convert to f-string
|
||||
274 | # Fix should parenthesize walrus
|
||||
275 | if __name__ == "__main__":
|
||||
276 | number = 0
|
||||
273 | # Fix should parenthesize walrus
|
||||
274 | if __name__ == "__main__":
|
||||
275 | number = 0
|
||||
- string = "{}".format(number := number + 1)
|
||||
277 + string = f"{(number := number + 1)}"
|
||||
278 | print(string)
|
||||
276 + string = f"{(number := number + 1)}"
|
||||
277 | print(string)
|
||||
278 |
|
||||
279 | # Unicode escape
|
||||
|
||||
UP032 [*] Use f-string instead of `format` call
|
||||
--> UP032_0.py:280:1
|
||||
|
|
||||
279 | # Unicode escape
|
||||
280 | "\N{angle}AOB = {angle}°".format(angle=180)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Convert to f-string
|
||||
277 | print(string)
|
||||
278 |
|
||||
279 | # Unicode escape
|
||||
- "\N{angle}AOB = {angle}°".format(angle=180)
|
||||
280 + f"\N{angle}AOB = {180}°"
|
||||
|
||||
@@ -3,10 +3,11 @@ 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::{BindingId, ResolvedReference, SemanticModel};
|
||||
use ruff_python_semantic::{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()`.
|
||||
@@ -119,14 +120,13 @@ 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,6 +137,45 @@ 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,
|
||||
@@ -146,10 +185,65 @@ 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))
|
||||
.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))
|
||||
})
|
||||
.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,
|
||||
@@ -165,8 +259,6 @@ 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`.
|
||||
@@ -187,58 +279,57 @@ 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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
Some(FileOpen {
|
||||
resolve_file_open(
|
||||
item,
|
||||
filename,
|
||||
with,
|
||||
semantic,
|
||||
read_mode,
|
||||
mode,
|
||||
keywords,
|
||||
reference,
|
||||
})
|
||||
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())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if !is_open_call_from_pathlib(func, semantic) {
|
||||
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(
|
||||
item,
|
||||
with,
|
||||
semantic,
|
||||
read_mode,
|
||||
mode,
|
||||
keywords,
|
||||
OpenArgument::Pathlib {
|
||||
path: attr.value.as_ref(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Match positional arguments. Return expression for the file name and open mode.
|
||||
|
||||
@@ -15,7 +15,8 @@ mod tests {
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_diagnostics, settings};
|
||||
|
||||
#[test_case(Rule::ReadWholeFile, Path::new("FURB101.py"))]
|
||||
#[test_case(Rule::ReadWholeFile, Path::new("FURB101_0.py"))]
|
||||
#[test_case(Rule::ReadWholeFile, Path::new("FURB101_1.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"))]
|
||||
@@ -46,7 +47,8 @@ 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.py"))]
|
||||
#[test_case(Rule::WriteWholeFile, Path::new("FURB103_0.py"))]
|
||||
#[test_case(Rule::WriteWholeFile, Path::new("FURB103_1.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"))]
|
||||
@@ -65,7 +67,7 @@ mod tests {
|
||||
#[test]
|
||||
fn write_whole_file_python_39() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("refurb/FURB103.py"),
|
||||
Path::new("refurb/FURB103_0.py"),
|
||||
&settings::LinterSettings::for_rule(Rule::WriteWholeFile)
|
||||
.with_target_version(PythonVersion::PY39),
|
||||
)?;
|
||||
|
||||
@@ -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, find_file_opens};
|
||||
use crate::rules::refurb::helpers::{FileOpen, OpenArgument, find_file_opens};
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -42,27 +42,41 @@ 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 {
|
||||
pub(crate) struct ReadWholeFile<'a> {
|
||||
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();
|
||||
format!("`open` and `read` should be replaced by `Path({filename}).{suggestion}`")
|
||||
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}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!(
|
||||
"Replace with `Path({}).{}`",
|
||||
self.filename.truncated_display(),
|
||||
self.suggestion.truncated_display(),
|
||||
))
|
||||
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}`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,13 +128,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(
|
||||
&self.checker.generator().expr(open.filename),
|
||||
),
|
||||
filename: SourceCodeSnippet::from_str(filename_display),
|
||||
suggestion: SourceCodeSnippet::from_str(&suggestion),
|
||||
argument: open.argument,
|
||||
},
|
||||
open.item.range(),
|
||||
);
|
||||
@@ -188,8 +202,6 @@ 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(
|
||||
@@ -206,10 +218,15 @@ fn generate_fix(
|
||||
[Stmt::Assign(ast::StmtAssign { targets, value, .. })] if value.range() == expr.range() => {
|
||||
match targets.as_slice() {
|
||||
[Expr::Name(name)] => {
|
||||
format!(
|
||||
"{name} = {binding}({filename_code}).{suggestion}",
|
||||
name = name.id
|
||||
)
|
||||
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)
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
@@ -223,8 +240,16 @@ 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} = {binding}({filename_code}).{suggestion}",
|
||||
"{var}: {ann} = {target}.{suggestion}",
|
||||
var = name.id,
|
||||
ann = locator.slice(annotation.range())
|
||||
)
|
||||
|
||||
@@ -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)?
|
||||
EnclosingSuite::new(semantic.definitions.python_ast()?, stmt.into())?
|
||||
} else {
|
||||
// Otherwise, go to the parent, and take its body as a sequence of siblings.
|
||||
semantic
|
||||
|
||||
@@ -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, find_file_opens};
|
||||
use crate::rules::refurb::helpers::{FileOpen, OpenArgument, find_file_opens};
|
||||
use crate::{FixAvailability, Locator, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -42,26 +42,40 @@ 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 {
|
||||
pub(crate) struct WriteWholeFile<'a> {
|
||||
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();
|
||||
format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`")
|
||||
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}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!(
|
||||
"Replace with `Path({}).{}`",
|
||||
self.filename.truncated_display(),
|
||||
self.suggestion.truncated_display(),
|
||||
))
|
||||
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}`"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,16 +139,15 @@ 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(
|
||||
&self.checker.generator().expr(open.filename),
|
||||
),
|
||||
filename: SourceCodeSnippet::from_str(filename_display),
|
||||
suggestion: SourceCodeSnippet::from_str(&suggestion),
|
||||
argument: open.argument,
|
||||
},
|
||||
open.item.range(),
|
||||
);
|
||||
@@ -198,7 +211,6 @@ fn generate_fix(
|
||||
}
|
||||
|
||||
let locator = checker.locator();
|
||||
let filename_code = locator.slice(open.filename.range());
|
||||
|
||||
let (import_edit, binding) = checker
|
||||
.importer()
|
||||
@@ -209,7 +221,15 @@ fn generate_fix(
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
let replacement = format!("{binding}({filename_code}).{suggestion}");
|
||||
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 applicability = if checker.comment_ranges().intersects(with_stmt.range()) {
|
||||
Applicability::Unsafe
|
||||
|
||||
@@ -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.py:12:6
|
||||
--> FURB101_0.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.py:16:6
|
||||
--> FURB101_0.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.py:20:6
|
||||
--> FURB101_0.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.py:24:6
|
||||
--> FURB101_0.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.py:28:6
|
||||
--> FURB101_0.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.py:32:6
|
||||
--> FURB101_0.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.py:36:6
|
||||
--> FURB101_0.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.py:44:6
|
||||
--> FURB101_0.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.py:44:26
|
||||
--> FURB101_0.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.py:49:18
|
||||
--> FURB101_0.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.py:130:6
|
||||
--> FURB101_0.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.py:134:6
|
||||
--> FURB101_0.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.py:138:6
|
||||
--> FURB101_0.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("file.txt").read_text(encoding="utf-8")`
|
||||
--> FURB101.py:141:6
|
||||
FURB101 `open` and `read` should be replaced by `Path("file1.txt").read_text(encoding="utf-8")`
|
||||
--> FURB101_0.py:141:6
|
||||
|
|
||||
139 | contents = process_contents(f.read())
|
||||
140 |
|
||||
141 | with open("file.txt", encoding="utf-8") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
141 | with open("file1.txt", encoding="utf-8") as f:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
142 | contents: str = process_contents(f.read())
|
||||
|
|
||||
help: Replace with `Path("file.txt").read_text(encoding="utf-8")`
|
||||
help: Replace with `Path("file1.txt").read_text(encoding="utf-8")`
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
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()
|
||||
@@ -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.py:12:6
|
||||
--> FURB103_0.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.py:16:6
|
||||
--> FURB103_0.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.py:20:6
|
||||
--> FURB103_0.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.py:24:6
|
||||
--> FURB103_0.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.py:28:6
|
||||
--> FURB103_0.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.py:32:6
|
||||
--> FURB103_0.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.py:36:6
|
||||
--> FURB103_0.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.py:44:6
|
||||
--> FURB103_0.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.py:44:31
|
||||
--> FURB103_0.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.py:49:18
|
||||
--> FURB103_0.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.py:58:6
|
||||
--> FURB103_0.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.py:66:6
|
||||
--> FURB103_0.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.py:74:6
|
||||
--> FURB103_0.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.py:154:6
|
||||
--> FURB103_0.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.py:158:6
|
||||
--> FURB103_0.py:158:6
|
||||
|
|
||||
157 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
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")
|
||||
@@ -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.py:12:6
|
||||
--> FURB103_0.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.py:16:6
|
||||
--> FURB103_0.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.py:20:6
|
||||
--> FURB103_0.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.py:24:6
|
||||
--> FURB103_0.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.py:28:6
|
||||
--> FURB103_0.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.py:32:6
|
||||
--> FURB103_0.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.py:36:6
|
||||
--> FURB103_0.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.py:44:6
|
||||
--> FURB103_0.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.py:44:31
|
||||
--> FURB103_0.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.py:49:18
|
||||
--> FURB103_0.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.py:154:6
|
||||
--> FURB103_0.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.py:158:6
|
||||
--> FURB103_0.py:158:6
|
||||
|
|
||||
157 | # See: https://github.com/astral-sh/ruff/issues/21381
|
||||
158 | with open("tmp_path/pyproject.toml", "w") as f:
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: annotated name `a` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:4:5
|
||||
|
|
||||
2 | def f1():
|
||||
3 | global a
|
||||
4 | a: str = "foo" # error
|
||||
| ^
|
||||
5 |
|
||||
6 | b: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `b` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:10:9
|
||||
|
|
||||
8 | def inner():
|
||||
9 | global b
|
||||
10 | b: str = "nested" # error
|
||||
| ^
|
||||
11 |
|
||||
12 | c: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `c` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:15:5
|
||||
|
|
||||
13 | def f2():
|
||||
14 | global c
|
||||
15 | c: list[str] = [] # error
|
||||
| ^
|
||||
16 |
|
||||
17 | d: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `d` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:20:5
|
||||
|
|
||||
18 | def f3():
|
||||
19 | global d
|
||||
20 | d: str # error
|
||||
| ^
|
||||
21 |
|
||||
22 | e: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `g` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:29:1
|
||||
|
|
||||
27 | f: int = 1 # okay
|
||||
28 |
|
||||
29 | g: int = 1
|
||||
| ^
|
||||
30 | global g # error
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `x` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:33:5
|
||||
|
|
||||
32 | class C:
|
||||
33 | x: str
|
||||
| ^
|
||||
34 | global x # error
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `x` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:38:5
|
||||
|
|
||||
36 | class D:
|
||||
37 | global x # error
|
||||
38 | x: str
|
||||
| ^
|
||||
|
|
||||
@@ -1,5 +1,5 @@
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
|
||||
use crate::AnyNodeRef;
|
||||
use crate::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_node};
|
||||
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(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
|
||||
pub fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
|
||||
struct Visitor<'a> {
|
||||
range: TextRange,
|
||||
found: bool,
|
||||
@@ -48,15 +48,12 @@ pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode
|
||||
ancestors: Vec::new(),
|
||||
};
|
||||
|
||||
root.visit_source_order(&mut visitor);
|
||||
if visitor.ancestors.is_empty() {
|
||||
visitor.ancestors.push(root);
|
||||
}
|
||||
walk_node(&mut visitor, root);
|
||||
CoveringNode::from_ancestors(visitor.ancestors)
|
||||
}
|
||||
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
pub(crate) struct CoveringNode<'a> {
|
||||
pub 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
|
||||
@@ -67,12 +64,12 @@ pub(crate) struct CoveringNode<'a> {
|
||||
impl<'a> CoveringNode<'a> {
|
||||
/// Creates a new `CoveringNode` from a list of ancestor nodes.
|
||||
/// The ancestors should be ordered from root to the covering node.
|
||||
pub(crate) fn from_ancestors(ancestors: Vec<AnyNodeRef<'a>>) -> Self {
|
||||
pub fn from_ancestors(ancestors: Vec<AnyNodeRef<'a>>) -> Self {
|
||||
Self { nodes: ancestors }
|
||||
}
|
||||
|
||||
/// Returns the covering node found.
|
||||
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
|
||||
pub fn node(&self) -> AnyNodeRef<'a> {
|
||||
*self
|
||||
.nodes
|
||||
.last()
|
||||
@@ -80,7 +77,7 @@ impl<'a> CoveringNode<'a> {
|
||||
}
|
||||
|
||||
/// Returns the node's parent.
|
||||
pub(crate) fn parent(&self) -> Option<AnyNodeRef<'a>> {
|
||||
pub fn parent(&self) -> Option<AnyNodeRef<'a>> {
|
||||
let penultimate = self.nodes.len().checked_sub(2)?;
|
||||
self.nodes.get(penultimate).copied()
|
||||
}
|
||||
@@ -90,7 +87,7 @@ impl<'a> CoveringNode<'a> {
|
||||
///
|
||||
/// The "first" here means that the node closest to a leaf is
|
||||
/// returned.
|
||||
pub(crate) fn find_first(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
|
||||
pub 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);
|
||||
};
|
||||
@@ -105,7 +102,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(crate) fn find_last(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
|
||||
pub 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);
|
||||
};
|
||||
@@ -118,7 +115,7 @@ impl<'a> CoveringNode<'a> {
|
||||
|
||||
/// Returns an iterator over the ancestor nodes, starting with the node itself
|
||||
/// and walking towards the root.
|
||||
pub(crate) fn ancestors(&self) -> impl DoubleEndedIterator<Item = AnyNodeRef<'a>> + '_ {
|
||||
pub fn ancestors(&self) -> impl DoubleEndedIterator<Item = AnyNodeRef<'a>> + '_ {
|
||||
self.nodes.iter().copied().rev()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub use python_version::*;
|
||||
pub mod comparable;
|
||||
pub mod docstrings;
|
||||
mod expression;
|
||||
pub mod find_node;
|
||||
mod generated;
|
||||
pub mod helpers;
|
||||
pub mod identifier;
|
||||
|
||||
@@ -2,18 +2,25 @@
|
||||
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: &'a Stmt, parent: &'a Stmt) -> Option<EnclosingSuite<'a>> {
|
||||
pub fn suite<'a>(
|
||||
stmt: impl Into<AnyNodeRef<'a>>,
|
||||
parent: impl Into<AnyNodeRef<'a>>,
|
||||
) -> Option<EnclosingSuite<'a>> {
|
||||
// TODO: refactor this to work without a parent, ie when `stmt` is at the top level
|
||||
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]
|
||||
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]
|
||||
.iter()
|
||||
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
|
||||
Stmt::While(ast::StmtWhile { body, orelse, .. }) => [body, orelse]
|
||||
AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => [body, orelse]
|
||||
.iter()
|
||||
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
|
||||
Stmt::If(ast::StmtIf {
|
||||
AnyNodeRef::StmtIf(ast::StmtIf {
|
||||
body,
|
||||
elif_else_clauses,
|
||||
..
|
||||
@@ -21,12 +28,12 @@ pub fn suite<'a>(stmt: &'a Stmt, parent: &'a Stmt) -> Option<EnclosingSuite<'a>>
|
||||
.into_iter()
|
||||
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
|
||||
.find_map(|suite| EnclosingSuite::new(suite, stmt)),
|
||||
Stmt::With(ast::StmtWith { body, .. }) => EnclosingSuite::new(body, stmt),
|
||||
Stmt::Match(ast::StmtMatch { cases, .. }) => cases
|
||||
AnyNodeRef::StmtWith(ast::StmtWith { body, .. }) => EnclosingSuite::new(body, stmt),
|
||||
AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => cases
|
||||
.iter()
|
||||
.map(|case| &case.body)
|
||||
.find_map(|body| EnclosingSuite::new(body, stmt)),
|
||||
Stmt::Try(ast::StmtTry {
|
||||
AnyNodeRef::StmtTry(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
@@ -51,10 +58,10 @@ pub struct EnclosingSuite<'a> {
|
||||
}
|
||||
|
||||
impl<'a> EnclosingSuite<'a> {
|
||||
pub fn new(suite: &'a [Stmt], stmt: &'a Stmt) -> Option<Self> {
|
||||
pub fn new(suite: &'a [Stmt], stmt: AnyNodeRef<'a>) -> Option<Self> {
|
||||
let position = suite
|
||||
.iter()
|
||||
.position(|sibling| AnyNodeRef::ptr_eq(sibling.into(), stmt.into()))?;
|
||||
.position(|sibling| AnyNodeRef::ptr_eq(sibling.into(), stmt))?;
|
||||
|
||||
Some(EnclosingSuite { suite, position })
|
||||
}
|
||||
|
||||
@@ -222,6 +222,17 @@ 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,
|
||||
|
||||
@@ -592,11 +592,23 @@ impl FormatString {
|
||||
fn parse_literal(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
|
||||
let mut cur_text = text;
|
||||
let mut result_string = String::new();
|
||||
let mut pending_escape = false;
|
||||
while !cur_text.is_empty() {
|
||||
if pending_escape
|
||||
&& let Some((unicode_string, remaining)) =
|
||||
FormatString::parse_escaped_unicode_string(cur_text)
|
||||
{
|
||||
result_string.push_str(unicode_string);
|
||||
cur_text = remaining;
|
||||
pending_escape = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
match FormatString::parse_literal_single(cur_text) {
|
||||
Ok((next_char, remaining)) => {
|
||||
result_string.push(next_char);
|
||||
cur_text = remaining;
|
||||
pending_escape = next_char == '\\' && !pending_escape;
|
||||
}
|
||||
Err(err) => {
|
||||
return if result_string.is_empty() {
|
||||
@@ -678,6 +690,13 @@ impl FormatString {
|
||||
}
|
||||
Err(FormatParseError::UnmatchedBracket)
|
||||
}
|
||||
|
||||
fn parse_escaped_unicode_string(text: &str) -> Option<(&str, &str)> {
|
||||
text.strip_prefix("N{")?.find('}').map(|idx| {
|
||||
let end_idx = idx + 3; // 3 for "N{"
|
||||
(&text[..end_idx], &text[end_idx..])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FromTemplate<'a>: Sized {
|
||||
@@ -1020,4 +1039,48 @@ mod tests {
|
||||
Err(FormatParseError::InvalidCharacterAfterRightBracket)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_unicode_escape() {
|
||||
let expected = Ok(FormatString {
|
||||
format_parts: vec![FormatPart::Literal("I am a \\N{snowman}".to_owned())],
|
||||
});
|
||||
|
||||
assert_eq!(FormatString::from_str("I am a \\N{snowman}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_unicode_escape_with_field() {
|
||||
let expected = Ok(FormatString {
|
||||
format_parts: vec![
|
||||
FormatPart::Literal("I am a \\N{snowman}".to_owned()),
|
||||
FormatPart::Field {
|
||||
field_name: "snowman".to_owned(),
|
||||
conversion_spec: None,
|
||||
format_spec: String::new(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
FormatString::from_str("I am a \\N{snowman}{snowman}"),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_multiple_escape_with_field() {
|
||||
let expected = Ok(FormatString {
|
||||
format_parts: vec![
|
||||
FormatPart::Literal("I am a \\\\N".to_owned()),
|
||||
FormatPart::Field {
|
||||
field_name: "snowman".to_owned(),
|
||||
conversion_spec: None,
|
||||
format_spec: String::new(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert_eq!(FormatString::from_str("I am a \\\\N{snowman}"), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,9 @@ impl SemanticSyntaxChecker {
|
||||
|
||||
fn check_annotation<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) {
|
||||
match stmt {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) => {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
target, annotation, ..
|
||||
}) => {
|
||||
if ctx.python_version() > PythonVersion::PY313 {
|
||||
// test_ok valid_annotation_py313
|
||||
// # parse_options: {"target-version": "3.13"}
|
||||
@@ -297,6 +299,18 @@ impl SemanticSyntaxChecker {
|
||||
};
|
||||
visitor.visit_expr(annotation);
|
||||
}
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
if let Some(global_stmt) = ctx.global(id.as_str()) {
|
||||
let global_start = global_stmt.start();
|
||||
if !ctx.in_module_scope() || target.start() < global_start {
|
||||
Self::add_error(
|
||||
ctx,
|
||||
SemanticSyntaxErrorKind::AnnotatedGlobal(id.to_string()),
|
||||
target.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
type_params,
|
||||
|
||||
@@ -179,42 +179,45 @@ 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 SourceLocation {
|
||||
line,
|
||||
character_offset: OneIndexed::from_zero_indexed((offset - line_start).to_usize()),
|
||||
};
|
||||
return (range.end() - range.start()).to_usize();
|
||||
}
|
||||
|
||||
match encoding {
|
||||
PositionEncoding::Utf8 => {
|
||||
let character_offset = offset - line_start;
|
||||
SourceLocation {
|
||||
line,
|
||||
character_offset: OneIndexed::from_zero_indexed(character_offset.to_usize()),
|
||||
}
|
||||
}
|
||||
PositionEncoding::Utf8 => (range.end() - range.start()).to_usize(),
|
||||
PositionEncoding::Utf16 => {
|
||||
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),
|
||||
}
|
||||
let up_to_character = &text[range];
|
||||
up_to_character.encode_utf16().count()
|
||||
}
|
||||
PositionEncoding::Utf32 => {
|
||||
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),
|
||||
}
|
||||
let up_to_character = &text[range];
|
||||
up_to_character.chars().count()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.9"
|
||||
version = "0.14.10"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -2703,3 +2703,51 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test behavior when `VIRTUAL_ENV` is set but points to a non-existent path.
|
||||
#[test]
|
||||
fn missing_virtual_env() -> anyhow::Result<()> {
|
||||
let working_venv_package1_path = if cfg!(windows) {
|
||||
"project/.venv/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"project/.venv/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/test.py",
|
||||
r#"
|
||||
from package1 import WorkingVenv
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/.venv/pyvenv.cfg",
|
||||
r#"
|
||||
home = ./
|
||||
|
||||
"#,
|
||||
),
|
||||
(
|
||||
working_venv_package1_path,
|
||||
r#"
|
||||
class WorkingVenv: ...
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("VIRTUAL_ENV", case.root().join("nonexistent-venv")), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ty failed
|
||||
Cause: Failed to discover local Python environment
|
||||
Cause: Invalid `VIRTUAL_ENV` environment variable `<temp_dir>/nonexistent-venv`: does not point to a directory on disk
|
||||
Cause: No such file or directory (os error 2)
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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,2
|
||||
none-completion,main.py,0,1
|
||||
numpy-array,main.py,0,159
|
||||
numpy-array,main.py,1,1
|
||||
object-attr-instance-methods,main.py,0,1
|
||||
|
||||
|
@@ -1,7 +1,8 @@
|
||||
use crate::{completion, find_node::covering_node};
|
||||
use crate::completion;
|
||||
|
||||
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
@@ -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: &crate::find_node::CoveringNode<'a>,
|
||||
covering_node: &CoveringNode<'a>,
|
||||
offset: TextSize,
|
||||
tokens: &Tokens,
|
||||
) -> Option<GotoTarget<'a>> {
|
||||
|
||||
@@ -386,6 +386,29 @@ 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()
|
||||
|
||||
@@ -187,29 +187,42 @@ 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:4:1
|
||||
--> main.py:8:1
|
||||
|
|
||||
2 | a = 10
|
||||
3 |
|
||||
4 | a
|
||||
6 | """
|
||||
7 |
|
||||
8 | a
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -703,6 +716,10 @@ 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()
|
||||
@@ -718,10 +735,10 @@ mod tests {
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:10:5
|
||||
--> main.py:14:5
|
||||
|
|
||||
9 | foo = Foo()
|
||||
10 | foo.a
|
||||
13 | foo = Foo()
|
||||
14 | foo.a
|
||||
| -
|
||||
| |
|
||||
| source
|
||||
@@ -1183,13 +1200,13 @@ def ab(a: str): ...
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(a: int) -> Unknown
|
||||
def ab(a: int) -> Unknown
|
||||
---------------------------------------------
|
||||
the int overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: int) -> Unknown
|
||||
def ab(a: int) -> Unknown
|
||||
```
|
||||
---
|
||||
the int overload
|
||||
@@ -1243,13 +1260,13 @@ def ab(a: str):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
(a: str) -> Unknown
|
||||
def ab(a: str) -> Unknown
|
||||
---------------------------------------------
|
||||
the int overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: str) -> Unknown
|
||||
def ab(a: str) -> Unknown
|
||||
```
|
||||
---
|
||||
the int overload
|
||||
@@ -1303,7 +1320,7 @@ def ab(a: int):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
b: int
|
||||
) -> Unknown
|
||||
@@ -1312,7 +1329,7 @@ def ab(a: int):
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
b: int
|
||||
) -> Unknown
|
||||
@@ -1369,13 +1386,13 @@ def ab(a: int):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(a: int) -> Unknown
|
||||
def ab(a: int) -> Unknown
|
||||
---------------------------------------------
|
||||
the two arg overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: int) -> Unknown
|
||||
def ab(a: int) -> Unknown
|
||||
```
|
||||
---
|
||||
the two arg overload
|
||||
@@ -1433,7 +1450,7 @@ def ab(a: int, *, c: int):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
*,
|
||||
b: int
|
||||
@@ -1443,7 +1460,7 @@ def ab(a: int, *, c: int):
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
*,
|
||||
b: int
|
||||
@@ -1505,7 +1522,7 @@ def ab(a: int, *, c: int):
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
*,
|
||||
c: int
|
||||
@@ -1515,7 +1532,7 @@ def ab(a: int, *, c: int):
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
def ab(
|
||||
a: int,
|
||||
*,
|
||||
c: int
|
||||
@@ -1564,11 +1581,11 @@ def ab(a: int, *, c: int):
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
(
|
||||
def foo(
|
||||
a: int,
|
||||
b
|
||||
) -> Unknown
|
||||
(
|
||||
def foo(
|
||||
a: str,
|
||||
b
|
||||
) -> Unknown
|
||||
@@ -1577,11 +1594,11 @@ def ab(a: int, *, c: int):
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
def foo(
|
||||
a: int,
|
||||
b
|
||||
) -> Unknown
|
||||
(
|
||||
def foo(
|
||||
a: str,
|
||||
b
|
||||
) -> Unknown
|
||||
@@ -1628,15 +1645,15 @@ def ab(a: int, *, c: int):
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
(a: int) -> Unknown
|
||||
(a: str) -> Unknown
|
||||
def foo(a: int) -> Unknown
|
||||
def foo(a: str) -> Unknown
|
||||
---------------------------------------------
|
||||
The first overload
|
||||
|
||||
---------------------------------------------
|
||||
```python
|
||||
(a: int) -> Unknown
|
||||
(a: str) -> Unknown
|
||||
def foo(a: int) -> Unknown
|
||||
def foo(a: str) -> Unknown
|
||||
```
|
||||
---
|
||||
The first overload
|
||||
@@ -2345,15 +2362,28 @@ 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
|
||||
@@ -2362,8 +2392,9 @@ def function():
|
||||
| ^^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
3 | """This is the docs for this value
|
||||
|
|
||||
");
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2371,7 +2402,15 @@ 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???
|
||||
"""
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -2379,23 +2418,34 @@ 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:3:1
|
||||
--> main.py:7:1
|
||||
|
|
||||
2 | value = 1
|
||||
3 | value += 2
|
||||
5 | Wow these are good docs!
|
||||
6 | """
|
||||
7 | value += 2
|
||||
| ^^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
8 | """Other docs???
|
||||
|
|
||||
");
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2404,29 +2454,47 @@ 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:5:3
|
||||
|
|
||||
3 | attr: int = 1
|
||||
4 |
|
||||
5 | C.attr = 2
|
||||
| ^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
--> main.py:9:3
|
||||
|
|
||||
7 | """
|
||||
8 |
|
||||
9 | C.attr = 2
|
||||
| ^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
10 | """Other docs???
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2435,31 +2503,49 @@ 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:5:3
|
||||
|
|
||||
3 | attr = 1
|
||||
4 |
|
||||
5 | C.attr += 2
|
||||
| ^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
--> main.py:9:3
|
||||
|
|
||||
7 | """
|
||||
8 |
|
||||
9 | C.attr += 2
|
||||
| ^^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
10 | """Other docs???
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2468,15 +2554,28 @@ 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
|
||||
@@ -2486,8 +2585,9 @@ def function():
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
4 | """This is the docs for this value
|
||||
|
|
||||
");
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2496,15 +2596,28 @@ 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
|
||||
@@ -2514,7 +2627,52 @@ 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
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
@@ -2525,15 +2683,28 @@ 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
|
||||
@@ -2544,7 +2715,53 @@ 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
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3233,12 +3450,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"
|
||||
(other: Test, /) -> Test
|
||||
(other: Other, /) -> Test
|
||||
def __add__(other: Test, /) -> Test
|
||||
def __add__(other: Other, /) -> Test
|
||||
---------------------------------------------
|
||||
```python
|
||||
(other: Test, /) -> Test
|
||||
(other: Other, /) -> Test
|
||||
def __add__(other: Test, /) -> Test
|
||||
def __add__(other: Other, /) -> Test
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
|
||||
@@ -869,7 +869,6 @@ 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};
|
||||
@@ -877,6 +876,7 @@ 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;
|
||||
|
||||
@@ -8,7 +8,6 @@ mod completion;
|
||||
mod doc_highlights;
|
||||
mod docstring;
|
||||
mod document_symbols;
|
||||
mod find_node;
|
||||
mod find_references;
|
||||
mod goto;
|
||||
mod goto_declaration;
|
||||
|
||||
@@ -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,10 +334,7 @@ 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: &crate::find_node::CoveringNode<'_>,
|
||||
) {
|
||||
fn check_reference_from_covering_node(&mut self, covering_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.
|
||||
|
||||
@@ -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,20 +66,28 @@ 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 1
|
||||
info[selection-range]: Selection Range 2
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | x = 1 + 2
|
||||
| ^^^^^
|
||||
|
|
||||
|
||||
info[selection-range]: Selection Range 2
|
||||
info[selection-range]: Selection Range 3
|
||||
--> main.py:2:9
|
||||
|
|
||||
2 | x = 1 + 2
|
||||
@@ -102,20 +110,28 @@ 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 1
|
||||
info[selection-range]: Selection Range 2
|
||||
--> main.py:2:6
|
||||
|
|
||||
2 | print("hello")
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
|
||||
info[selection-range]: Selection Range 2
|
||||
info[selection-range]: Selection Range 3
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | print("hello")
|
||||
@@ -139,6 +155,15 @@ 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():
|
||||
@@ -146,7 +171,7 @@ def my_<CURSOR>function():
|
||||
| |_____________^
|
||||
|
|
||||
|
||||
info[selection-range]: Selection Range 1
|
||||
info[selection-range]: Selection Range 2
|
||||
--> main.py:2:5
|
||||
|
|
||||
2 | def my_function():
|
||||
@@ -172,6 +197,16 @@ 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:
|
||||
@@ -180,7 +215,7 @@ class My<CURSOR>Class:
|
||||
| |______________________^
|
||||
|
|
||||
|
||||
info[selection-range]: Selection Range 1
|
||||
info[selection-range]: Selection Range 2
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
@@ -205,48 +240,56 @@ 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 1
|
||||
info[selection-range]: Selection Range 2
|
||||
--> 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 2
|
||||
info[selection-range]: Selection Range 3
|
||||
--> 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 3
|
||||
info[selection-range]: Selection Range 4
|
||||
--> 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 4
|
||||
info[selection-range]: Selection Range 5
|
||||
--> 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 5
|
||||
info[selection-range]: Selection Range 6
|
||||
--> 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 6
|
||||
info[selection-range]: Selection Range 7
|
||||
--> main.py:2:28
|
||||
|
|
||||
2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None]
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
//! 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};
|
||||
|
||||
@@ -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::ProgramSettings;
|
||||
use ty_python_semantic::{MisconfigurationMode, ProgramSettings};
|
||||
|
||||
use crate::metadata::options::ProjectOptionsOverrides;
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
|
||||
@@ -37,6 +37,9 @@ 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 {
|
||||
@@ -47,6 +50,7 @@ impl ProjectMetadata {
|
||||
root,
|
||||
extra_configuration_paths: Vec::default(),
|
||||
options: Options::default(),
|
||||
misconfiguration_mode: MisconfigurationMode::Fail,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +74,7 @@ impl ProjectMetadata {
|
||||
root: system.current_directory().to_path_buf(),
|
||||
options,
|
||||
extra_configuration_paths: vec![path],
|
||||
misconfiguration_mode: MisconfigurationMode::Fail,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,6 +87,7 @@ impl ProjectMetadata {
|
||||
pyproject.tool.and_then(|tool| tool.ty).unwrap_or_default(),
|
||||
root,
|
||||
pyproject.project.as_ref(),
|
||||
MisconfigurationMode::Fail,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,6 +96,7 @@ 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())
|
||||
@@ -117,6 +124,7 @@ impl ProjectMetadata {
|
||||
root,
|
||||
options,
|
||||
extra_configuration_paths: Vec::new(),
|
||||
misconfiguration_mode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,6 +202,7 @@ impl ProjectMetadata {
|
||||
pyproject
|
||||
.as_ref()
|
||||
.and_then(|pyproject| pyproject.project.as_ref()),
|
||||
MisconfigurationMode::Fail,
|
||||
)
|
||||
.map_err(|err| {
|
||||
ProjectMetadataError::InvalidRequiresPythonConstraint {
|
||||
@@ -273,8 +282,13 @@ impl ProjectMetadata {
|
||||
system: &dyn System,
|
||||
vendored: &VendoredFileSystem,
|
||||
) -> anyhow::Result<ProgramSettings> {
|
||||
self.options
|
||||
.to_program_settings(self.root(), self.name(), system, vendored)
|
||||
self.options.to_program_settings(
|
||||
self.root(),
|
||||
self.name(),
|
||||
system,
|
||||
vendored,
|
||||
self.misconfiguration_mode,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn apply_overrides(&mut self, overrides: &ProjectOptionsOverrides) {
|
||||
|
||||
@@ -30,9 +30,9 @@ use thiserror::Error;
|
||||
use ty_combine::Combine;
|
||||
use ty_python_semantic::lint::{Level, LintSource, RuleSelection};
|
||||
use ty_python_semantic::{
|
||||
ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionFileSource,
|
||||
PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SearchPathValidationError,
|
||||
SearchPaths, SitePackagesPaths, SysPrefixPathOrigin,
|
||||
MisconfigurationMode, ProgramSettings, PythonEnvironment, PythonPlatform,
|
||||
PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SearchPathSettings,
|
||||
SearchPathValidationError, SearchPaths, SitePackagesPaths, SysPrefixPathOrigin,
|
||||
};
|
||||
use ty_static::EnvVars;
|
||||
|
||||
@@ -117,6 +117,7 @@ impl Options {
|
||||
project_name: &str,
|
||||
system: &dyn System,
|
||||
vendored: &VendoredFileSystem,
|
||||
misconfiguration_mode: MisconfigurationMode,
|
||||
) -> anyhow::Result<ProgramSettings> {
|
||||
let environment = self.environment.or_default();
|
||||
|
||||
@@ -154,14 +155,25 @@ impl Options {
|
||||
ValueSource::Editor => SysPrefixPathOrigin::Editor,
|
||||
};
|
||||
|
||||
Some(PythonEnvironment::new(
|
||||
python_path.absolute(project_root, system),
|
||||
origin,
|
||||
system,
|
||||
)?)
|
||||
PythonEnvironment::new(python_path.absolute(project_root, system), origin, system)
|
||||
.map_err(anyhow::Error::from)
|
||||
.map(Some)
|
||||
} else {
|
||||
PythonEnvironment::discover(project_root, system)
|
||||
.context("Failed to discover local Python environment")?
|
||||
.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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let self_site_packages = self_environment_search_paths(
|
||||
@@ -174,11 +186,23 @@ impl Options {
|
||||
.unwrap_or_default();
|
||||
|
||||
let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() {
|
||||
self_site_packages.concatenate(
|
||||
python_environment
|
||||
.site_packages_paths(system)
|
||||
.context("Failed to discover the site-packages directory")?,
|
||||
)
|
||||
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)
|
||||
} else {
|
||||
tracing::debug!("No virtual environment found");
|
||||
self_site_packages
|
||||
@@ -201,6 +225,7 @@ 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,
|
||||
@@ -208,6 +233,7 @@ impl Options {
|
||||
real_stdlib_path,
|
||||
system,
|
||||
vendored,
|
||||
misconfiguration_mode,
|
||||
)?;
|
||||
|
||||
tracing::info!(
|
||||
@@ -222,6 +248,7 @@ impl Options {
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn to_search_paths(
|
||||
&self,
|
||||
project_root: &SystemPath,
|
||||
@@ -230,6 +257,7 @@ 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();
|
||||
@@ -344,6 +372,7 @@ 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)
|
||||
|
||||
@@ -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 }
|
||||
dir-test = { workspace = true }
|
||||
datatest-stable = { workspace = true }
|
||||
glob = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
@@ -76,5 +76,9 @@ 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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/// Rebuild the crate if a test file is added or removed from
|
||||
pub fn main() {
|
||||
println!("cargo::rerun-if-changed=resources/mdtest");
|
||||
}
|
||||
@@ -129,17 +129,8 @@ 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:
|
||||
path_mangled = self._mangle_path(markdown_file)
|
||||
test_name = f"mdtest__{path_mangled}"
|
||||
test_name = f"mdtest::{markdown_file}"
|
||||
|
||||
output = self._run_mdtest(["--exact", test_name], capture_output=True)
|
||||
|
||||
@@ -245,16 +236,10 @@ 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:
|
||||
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..."
|
||||
)
|
||||
elif vendored_typeshed_has_changed and self._recompile_tests(
|
||||
"Vendored typeshed has changed, recompiling tests..."
|
||||
):
|
||||
self._run_mdtest(self.filters)
|
||||
|
||||
for path in new_md_files | changed_md_files:
|
||||
self._run_mdtests_for_file(path)
|
||||
|
||||
@@ -205,3 +205,93 @@ 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
|
||||
```
|
||||
|
||||
@@ -1208,7 +1208,7 @@ def _(flag: bool):
|
||||
reveal_type(C1.y) # revealed: int | str
|
||||
|
||||
C1.y = 100
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'C1'> | <class 'C1'>`"
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8'>`"
|
||||
C1.y = "problematic"
|
||||
|
||||
class C2:
|
||||
|
||||
@@ -615,6 +615,22 @@ 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.
|
||||
|
||||
@@ -643,6 +643,91 @@ reveal_type(Person.__init__) # revealed: (self: Person, name: str) -> None
|
||||
Person(name="Alice")
|
||||
```
|
||||
|
||||
### Field specifiers using `**kwargs`
|
||||
|
||||
Some field specifiers may use `**kwargs` to pass through standard parameters like `default`,
|
||||
`default_factory`, `init`, `kw_only`, and `alias`. This section tests that all these parameters work
|
||||
correctly when passed via `**kwargs` for all three kinds of transformers.
|
||||
|
||||
#### Function-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
def create_model[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
@create_model
|
||||
class Person:
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
#### Metaclass-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
class ModelMeta(type): ...
|
||||
|
||||
class ModelBase(metaclass=ModelMeta): ...
|
||||
|
||||
class Person(ModelBase):
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
#### Base-class-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
class ModelBase: ...
|
||||
|
||||
class Person(ModelBase):
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
### Support for `alias`
|
||||
|
||||
The `alias` parameter in field specifiers allows providing an alternative name for the parameter in
|
||||
@@ -868,4 +953,83 @@ reveal_type(t.key) # revealed: int
|
||||
reveal_type(t.name) # revealed: str
|
||||
```
|
||||
|
||||
## `__dataclass_fields__` and `DataclassInstance` protocol
|
||||
|
||||
Classes created via `dataclass_transform` should have `__dataclass_fields__` and
|
||||
`__dataclass_params__` attributes, allowing them to satisfy the `DataclassInstance` protocol. This
|
||||
enables use of `dataclasses.fields`, `dataclasses.asdict`, `dataclasses.replace`, etc.
|
||||
|
||||
### Function-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
def create_model[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
@create_model
|
||||
class Person:
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
### Metaclass-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
class ModelMeta(type): ...
|
||||
|
||||
class ModelBase(metaclass=ModelMeta): ...
|
||||
|
||||
class Person(ModelBase):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
### Base-class-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
class ModelBase: ...
|
||||
|
||||
class Person(ModelBase):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
||||
|
||||
@@ -195,3 +195,52 @@ class C:
|
||||
c = C()
|
||||
c.square("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Types with the same name but from different files
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from module import needs_a_foo
|
||||
|
||||
class Foo: ...
|
||||
|
||||
needs_a_foo(Foo()) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## TypeVars with bounds that have the same name but are from different files
|
||||
|
||||
In this case, using fully qualified names is *not* necessary.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from module import needs_a_foo
|
||||
|
||||
class Foo: ...
|
||||
|
||||
def f[T: Foo](x: T) -> T:
|
||||
needs_a_foo(x) # error: [invalid-argument-type]
|
||||
return x
|
||||
```
|
||||
|
||||
@@ -738,6 +738,8 @@ 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.
|
||||
|
||||
@@ -891,3 +893,46 @@ 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
|
||||
```
|
||||
|
||||
@@ -289,6 +289,14 @@ 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
|
||||
@@ -393,7 +401,7 @@ else:
|
||||
# revealed: (<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
|
||||
reveal_mro(B)
|
||||
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'B'> | <class 'B'>`"
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28'>`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_mro(Z) # revealed: (<class 'Z'>, Unknown, <class 'object'>)
|
||||
|
||||
@@ -104,6 +104,8 @@ 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
|
||||
|
||||
@@ -114,7 +116,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)
|
||||
# TODO: should be # `str & ~AlwaysFalsy`
|
||||
# `c.value is not None` narrows to `str`, but `str` is not narrowable by len().
|
||||
reveal_type(c.value) # revealed: str
|
||||
```
|
||||
|
||||
|
||||
131
crates/ty_python_semantic/resources/mdtest/narrow/len.md
Normal file
131
crates/ty_python_semantic/resources/mdtest/narrow/len.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 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]
|
||||
```
|
||||
@@ -37,7 +37,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'C1'> | <class 'C1'>`
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7'>`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - TypeVars with bounds that have the same name but are from different files
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## module.py
|
||||
|
||||
```
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | from module import needs_a_foo
|
||||
2 |
|
||||
3 | class Foo: ...
|
||||
4 |
|
||||
5 | def f[T: Foo](x: T) -> T:
|
||||
6 | needs_a_foo(x) # error: [invalid-argument-type]
|
||||
7 | return x
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect
|
||||
--> src/main.py:6:17
|
||||
|
|
||||
5 | def f[T: Foo](x: T) -> T:
|
||||
6 | needs_a_foo(x) # error: [invalid-argument-type]
|
||||
| ^ Expected `Foo`, found `T@f`
|
||||
7 | return x
|
||||
|
|
||||
info: Function defined here
|
||||
--> src/module.py:3:5
|
||||
|
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
| ^^^^^^^^^^^ ------ Parameter declared here
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Types with the same name but from different files
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## module.py
|
||||
|
||||
```
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | from module import needs_a_foo
|
||||
2 |
|
||||
3 | class Foo: ...
|
||||
4 |
|
||||
5 | needs_a_foo(Foo()) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect
|
||||
--> src/main.py:5:13
|
||||
|
|
||||
3 | class Foo: ...
|
||||
4 |
|
||||
5 | needs_a_foo(Foo()) # error: [invalid-argument-type]
|
||||
| ^^^^^ Expected `module.Foo`, found `main.Foo`
|
||||
|
|
||||
info: Function defined here
|
||||
--> src/module.py:3:5
|
||||
|
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
| ^^^^^^^^^^^ ------ Parameter declared here
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
@@ -31,17 +31,25 @@ 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 with type `<class 'A'> | <class 'B'>`
|
||||
warning[unsupported-base]: Unsupported class base
|
||||
--> 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'>)
|
||||
|
|
||||
@@ -50,3 +58,18 @@ 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
|
||||
|
||||
```
|
||||
|
||||
@@ -47,13 +47,13 @@ info: rule `invalid-base` is enabled by default
|
||||
```
|
||||
|
||||
```
|
||||
warning[unsupported-base]: Unsupported class base with type `Foo`
|
||||
warning[unsupported-base]: Unsupported class base
|
||||
--> 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):
|
||||
|
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
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
|
||||
|
||||
```
|
||||
@@ -17,7 +17,7 @@ pub use module_resolver::{
|
||||
resolve_real_module_confident, resolve_real_shadowable_module, system_module_search_paths,
|
||||
};
|
||||
pub use program::{
|
||||
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
|
||||
MisconfigurationMode, Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
|
||||
PythonVersionWithSource, SearchPathSettings,
|
||||
};
|
||||
pub use python_platform::PythonPlatform;
|
||||
|
||||
@@ -50,6 +50,7 @@ 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};
|
||||
@@ -570,6 +571,7 @@ impl SearchPaths {
|
||||
custom_typeshed: typeshed,
|
||||
site_packages_paths,
|
||||
real_stdlib_path,
|
||||
misconfiguration_mode,
|
||||
} = settings;
|
||||
|
||||
let mut static_paths = vec![];
|
||||
@@ -578,12 +580,30 @@ impl SearchPaths {
|
||||
let path = canonicalize(path, system);
|
||||
tracing::debug!("Adding extra search-path `{path}`");
|
||||
|
||||
static_paths.push(SearchPath::extra(system, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for src_root in src_roots {
|
||||
tracing::debug!("Adding first-party search path `{src_root}`");
|
||||
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed {
|
||||
@@ -592,18 +612,31 @@ impl SearchPaths {
|
||||
|
||||
let versions_path = typeshed.join("stdlib/VERSIONS");
|
||||
|
||||
let versions_content = system.read_to_string(&versions_path).map_err(|error| {
|
||||
SearchPathValidationError::FailedToReadVersionsFile {
|
||||
path: versions_path,
|
||||
error,
|
||||
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 parsed: TypeshedVersions = versions_content.parse()?;
|
||||
|
||||
let search_path = SearchPath::custom_stdlib(system, &typeshed)?;
|
||||
|
||||
(parsed, search_path)
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("Using vendored stdlib");
|
||||
(
|
||||
@@ -613,7 +646,17 @@ impl SearchPaths {
|
||||
};
|
||||
|
||||
let real_stdlib_path = if let Some(path) = real_stdlib_path {
|
||||
Some(SearchPath::real_stdlib(system, path.clone())?)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -622,7 +665,16 @@ impl SearchPaths {
|
||||
|
||||
for path in site_packages_paths {
|
||||
tracing::debug!("Adding site-packages search path `{path}`");
|
||||
site_packages.push(SearchPath::site_packages(system, path.clone())?);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO vendor typeshed's third-party stubs as well as the stdlib and
|
||||
|
||||
@@ -163,6 +163,17 @@ 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 {
|
||||
@@ -187,6 +198,9 @@ 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 {
|
||||
@@ -204,6 +218,7 @@ impl SearchPathSettings {
|
||||
custom_typeshed: None,
|
||||
site_packages_paths: vec![],
|
||||
real_stdlib_path: None,
|
||||
misconfiguration_mode: MisconfigurationMode::Fail,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ use std::ops::Deref;
|
||||
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_python_ast as ast;
|
||||
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_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::Db;
|
||||
@@ -101,7 +103,7 @@ impl<'db> Definition<'db> {
|
||||
}
|
||||
|
||||
/// Extract a docstring from this definition, if applicable.
|
||||
/// This method returns a docstring for function and class definitions.
|
||||
/// This method returns a docstring for function, class, and attribute 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);
|
||||
@@ -109,6 +111,16 @@ 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)
|
||||
@@ -124,7 +136,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())
|
||||
@@ -147,6 +159,44 @@ 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> {
|
||||
|
||||
@@ -369,6 +369,61 @@ 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 {
|
||||
@@ -801,46 +856,6 @@ 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();
|
||||
|
||||
@@ -908,7 +923,7 @@ impl ReachabilityConstraints {
|
||||
}
|
||||
.negate_if(!predicate.is_positive)
|
||||
}
|
||||
PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner),
|
||||
PredicateNode::Pattern(inner) => analyze_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));
|
||||
|
||||
@@ -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, Eq, PartialEq, PartialOrd, Ord)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub enum NameKind {
|
||||
Normal,
|
||||
Dunder,
|
||||
|
||||
@@ -76,15 +76,25 @@ 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()),
|
||||
)),
|
||||
_ => builder.into_diagnostic(format_args!(
|
||||
"`{pivot_class}` is not a valid class",
|
||||
pivot_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()),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
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()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BoundSuperError::FailingConditionCheck {
|
||||
|
||||
@@ -21,7 +21,6 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use smallvec::{SmallVec, smallvec, smallvec_inline};
|
||||
|
||||
use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type};
|
||||
use crate::Program;
|
||||
use crate::db::Db;
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::module_resolver::KnownModule;
|
||||
@@ -52,6 +51,7 @@ use crate::types::{
|
||||
enums, list_members, todo_type,
|
||||
};
|
||||
use crate::unpack::EvaluationMode;
|
||||
use crate::{DisplaySettings, Program};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
|
||||
|
||||
@@ -214,7 +214,7 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
self.evaluate_known_cases(db, dataclass_field_specifiers);
|
||||
self.evaluate_known_cases(db, argument_types, dataclass_field_specifiers);
|
||||
|
||||
// In order of precedence:
|
||||
//
|
||||
@@ -337,7 +337,12 @@ impl<'db> Bindings<'db> {
|
||||
|
||||
/// Evaluates the return type of certain known callables, where we have special-case logic to
|
||||
/// determine the return type in a way that isn't directly expressible in the type system.
|
||||
fn evaluate_known_cases(&mut self, db: &'db dyn Db, dataclass_field_specifiers: &[Type<'db>]) {
|
||||
fn evaluate_known_cases(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
argument_types: &CallArguments<'_, 'db>,
|
||||
dataclass_field_specifiers: &[Type<'db>],
|
||||
) {
|
||||
let to_bool = |ty: &Option<Type<'_>>, default: bool| -> bool {
|
||||
if let Some(Type::BooleanLiteral(value)) = ty {
|
||||
*value
|
||||
@@ -666,25 +671,32 @@ impl<'db> Bindings<'db> {
|
||||
if dataclass_field_specifiers.contains(&function)
|
||||
|| function_type.is_known(db, KnownFunction::Field) =>
|
||||
{
|
||||
let has_default_value = overload
|
||||
.parameter_type_by_name("default", false)
|
||||
.is_ok_and(|ty| ty.is_some())
|
||||
|| overload
|
||||
.parameter_type_by_name("default_factory", false)
|
||||
.is_ok_and(|ty| ty.is_some())
|
||||
|| overload
|
||||
.parameter_type_by_name("factory", false)
|
||||
.is_ok_and(|ty| ty.is_some());
|
||||
// Helper to get the type of a keyword argument by name. We first try to get it from
|
||||
// the parameter binding (for explicit parameters), and then fall back to checking the
|
||||
// call site arguments (for field-specifier functions that use a `**kwargs` parameter,
|
||||
// instead of specifying `init`, `default` etc. explicitly).
|
||||
let get_argument_type = |name, fallback_to_default| -> Option<Type<'db>> {
|
||||
if let Ok(ty) =
|
||||
overload.parameter_type_by_name(name, fallback_to_default)
|
||||
{
|
||||
return ty;
|
||||
}
|
||||
argument_types.iter().find_map(|(arg, ty)| {
|
||||
if matches!(arg, Argument::Keyword(arg_name) if arg_name == name) {
|
||||
ty
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let init = overload
|
||||
.parameter_type_by_name("init", true)
|
||||
.unwrap_or(None);
|
||||
let kw_only = overload
|
||||
.parameter_type_by_name("kw_only", true)
|
||||
.unwrap_or(None);
|
||||
let alias = overload
|
||||
.parameter_type_by_name("alias", true)
|
||||
.unwrap_or(None);
|
||||
let has_default_value = get_argument_type("default", false).is_some()
|
||||
|| get_argument_type("default_factory", false).is_some()
|
||||
|| get_argument_type("factory", false).is_some();
|
||||
|
||||
let init = get_argument_type("init", true);
|
||||
let kw_only = get_argument_type("kw_only", true);
|
||||
let alias = get_argument_type("alias", true);
|
||||
|
||||
// `dataclasses.field` and field-specifier functions of commonly used
|
||||
// libraries like `pydantic`, `attrs`, and `SQLAlchemy` all return
|
||||
@@ -4156,8 +4168,13 @@ impl<'db> BindingError<'db> {
|
||||
return;
|
||||
};
|
||||
|
||||
let provided_ty_display = provided_ty.display(context.db());
|
||||
let expected_ty_display = expected_ty.display(context.db());
|
||||
let display_settings = DisplaySettings::from_possibly_ambiguous_types(
|
||||
context.db(),
|
||||
[provided_ty, expected_ty],
|
||||
);
|
||||
let provided_ty_display =
|
||||
provided_ty.display_with(context.db(), display_settings.clone());
|
||||
let expected_ty_display = expected_ty.display_with(context.db(), display_settings);
|
||||
|
||||
let mut diag = builder.into_diagnostic(format_args!(
|
||||
"Argument{} is incorrect",
|
||||
|
||||
@@ -2277,7 +2277,11 @@ impl<'db> ClassLiteral<'db> {
|
||||
specialization: Option<Specialization<'db>>,
|
||||
name: &str,
|
||||
) -> Member<'db> {
|
||||
if self.dataclass_params(db).is_some() {
|
||||
// Check if this class is dataclass-like (either via @dataclass or via dataclass_transform)
|
||||
if matches!(
|
||||
CodeGeneratorKind::from_class(db, self, specialization),
|
||||
Some(CodeGeneratorKind::DataclassLike(_))
|
||||
) {
|
||||
if name == "__dataclass_fields__" {
|
||||
// Make this class look like a subclass of the `DataClassInstance` protocol
|
||||
return Member {
|
||||
|
||||
@@ -3478,13 +3478,16 @@ fn report_unsupported_base(
|
||||
let Some(builder) = context.report_lint(&UNSUPPORTED_BASE, base_node) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
let db = context.db();
|
||||
let mut diagnostic = builder.into_diagnostic("Unsupported class base");
|
||||
diagnostic.set_primary_message(format_args!("Has type `{}`", base_type.display(db)));
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"Unsupported class base with type `{}`",
|
||||
base_type.display(context.db())
|
||||
base_type.display(db)
|
||||
));
|
||||
diagnostic.info(format_args!(
|
||||
"ty cannot resolve a consistent MRO for class `{}` due to this base",
|
||||
class.name(context.db())
|
||||
class.name(db)
|
||||
));
|
||||
diagnostic.info("Only class objects or `Any` are supported as class bases");
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user