Compare commits

..

3 Commits

Author SHA1 Message Date
Aria Desires
8e12163754 more narrow 2025-10-31 11:21:33 -04:00
Aria Desires
5473636256 fix latent bug in MdtestSystem::canonicalize_path 2025-10-23 19:40:58 -04:00
Aria Desires
b96e1b9759 Do not crash if a FileRoot is a symlink 2025-10-23 18:20:39 -04:00
239 changed files with 3278 additions and 8830 deletions

View File

@@ -438,7 +438,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9
uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7
- name: "Install cargo-fuzz"
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
@@ -531,7 +531,8 @@ jobs:
persist-credentials: false
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
python-version: ${{ env.PYTHON_VERSION }}
# TODO: figure out why `ruff-ecosystem` crashes on Python 3.14
python-version: "3.13"
activate-environment: true
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
@@ -698,7 +699,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9
- uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear

View File

@@ -70,7 +70,7 @@ jobs:
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.0/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
@@ -86,7 +86,7 @@ jobs:
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
@@ -128,14 +128,14 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
pattern: artifacts-*
path: target/distrib/
@@ -153,7 +153,7 @@ jobs:
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: artifacts-build-global
path: |
@@ -179,14 +179,14 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
pattern: artifacts-*
path: target/distrib/
@@ -200,7 +200,7 @@ jobs:
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
@@ -256,7 +256,7 @@ jobs:
submodules: recursive
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
with:
pattern: artifacts-*
path: artifacts

2
.github/zizmor.yml vendored
View File

@@ -1,5 +1,5 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://docs.zizmor.sh/configuration/
# https://woodruffw.github.io/zizmor/configuration/
#
# TODO: can we remove the ignores here so that our workflows are more secure?
rules:

View File

@@ -102,7 +102,7 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.16.0
rev: v1.15.2
hooks:
- id: zizmor

33
Cargo.lock generated
View File

@@ -295,9 +295,9 @@ checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
[[package]]
name = "bstr"
version = "1.12.1"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata",
@@ -433,9 +433,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.50"
version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f"
dependencies = [
"clap_builder",
"clap_derive",
@@ -443,9 +443,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.50"
version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730"
dependencies = [
"anstream",
"anstyle",
@@ -1007,7 +1007,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -1224,9 +1224,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.7.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f"
checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843"
dependencies = [
"attribute-derive",
"quote",
@@ -1235,9 +1235,9 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.7.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9"
checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed"
dependencies = [
"compact_str",
"get-size-derive2",
@@ -1523,9 +1523,9 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.24"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403"
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
dependencies = [
"crossbeam-deque",
"globset",
@@ -3563,7 +3563,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
dependencies = [
"boxcar",
"compact_str",
@@ -3587,12 +3587,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
dependencies = [
"proc-macro2",
"quote",
@@ -4521,6 +4521,7 @@ name = "ty_test"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
"camino",
"colored 3.0.0",
"insta",

View File

@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "cdd0b85516a52c18b8a6d17a2279a96ed6c3e198", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d38145c29574758de7ffbe8a13cd4584c3b09161", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -9,7 +9,9 @@ use itertools::{Itertools, iterate};
use ruff_linter::linter::FixTable;
use serde::Serialize;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, SecondaryCode};
use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
};
use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{EmitterContext, render_diagnostics};
@@ -388,18 +390,21 @@ impl Printer {
let context = EmitterContext::new(&diagnostics.notebook_indexes);
let format = if preview {
self.format
DiagnosticFormat::Full
} else {
OutputFormat::Concise
DiagnosticFormat::Concise
};
let config = DisplayDiagnosticConfig::default()
.preview(preview)
.hide_severity(true)
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize())
.with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref()))
.with_fix_applicability(self.unsafe_fixes.required_applicability())
.show_fix_diff(preview);
render_diagnostics(writer, format, config, &context, &diagnostics.inner)?;
.format(format)
.with_fix_applicability(self.unsafe_fixes.required_applicability());
write!(
writer,
"{}",
DisplayDiagnostics::new(&context, &config, &diagnostics.inner)
)?;
}
writer.flush()?;

View File

@@ -226,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
800,
750,
);
#[track_caller]

View File

@@ -190,6 +190,7 @@ impl Files {
let roots = self.inner.roots.read().unwrap();
let absolute = SystemPath::absolute(path, db.system().current_directory());
roots.at(&absolute)
}
@@ -211,7 +212,10 @@ impl Files {
let mut roots = self.inner.roots.write().unwrap();
let absolute = SystemPath::absolute(path, db.system().current_directory());
roots.try_add(db, absolute, kind)
// We need to resolve away symlinks here to avoid getting confused about subdirectories.
let canonicalized = db.system().canonicalize_path(&absolute).unwrap_or(absolute);
roots.try_add(db, canonicalized, kind)
}
/// Updates the revision of the root for `path`.

View File

@@ -200,12 +200,7 @@ impl System for OsSystem {
/// The walker ignores files according to [`ignore::WalkBuilder::standard_filters`]
/// when setting [`WalkDirectoryBuilder::standard_filters`] to true.
fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder {
WalkDirectoryBuilder::new(
path,
OsDirectoryWalker {
cwd: self.current_directory().to_path_buf(),
},
)
WalkDirectoryBuilder::new(path, OsDirectoryWalker {})
}
fn glob(
@@ -459,9 +454,7 @@ struct ListedDirectory {
}
#[derive(Debug)]
struct OsDirectoryWalker {
cwd: SystemPathBuf,
}
struct OsDirectoryWalker;
impl DirectoryWalker for OsDirectoryWalker {
fn walk(
@@ -480,7 +473,6 @@ impl DirectoryWalker for OsDirectoryWalker {
};
let mut builder = ignore::WalkBuilder::new(first.as_std_path());
builder.current_dir(self.cwd.as_std_path());
builder.standard_filters(standard_filters);
builder.hidden(hidden);

View File

@@ -10,7 +10,6 @@ from airflow.datasets import (
)
from airflow.datasets.manager import DatasetManager
from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector
from airflow.models.dag import DAG
from airflow.providers.amazon.aws.auth_manager.aws_auth_manager import AwsAuthManager
from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError
from airflow.providers.google.cloud.secrets.secret_manager import (
@@ -21,7 +20,6 @@ from airflow.providers_manager import ProvidersManager
from airflow.secrets.base_secrets import BaseSecretsBackend
from airflow.secrets.local_filesystem import LocalFilesystemBackend
# airflow.Dataset
dataset_from_root = DatasetFromRoot()
dataset_from_root.iter_datasets()
@@ -58,10 +56,6 @@ hlc.add_input_dataset()
hlc.add_output_dataset()
hlc.collected_datasets()
# airflow.models.dag.DAG
test_dag = DAG(dag_id="test_dag")
test_dag.create_dagrun()
# airflow.providers.amazon.auth_manager.aws_auth_manager
aam = AwsAuthManager()
aam.is_authorized_dataset()
@@ -102,15 +96,3 @@ base_secret_backend.get_connections()
# airflow.secrets.local_filesystem
lfb = LocalFilesystemBackend()
lfb.get_connections()
from airflow.models import DAG
# airflow.DAG
test_dag = DAG(dag_id="test_dag")
test_dag.create_dagrun()
from airflow import DAG
# airflow.DAG
test_dag = DAG(dag_id="test_dag")
test_dag.create_dagrun()

View File

@@ -46,9 +46,3 @@ class CorrectModel(models.Model):
max_length=255, null=True, blank=True, unique=True
)
urlfieldu = models.URLField(max_length=255, null=True, blank=True, unique=True)
class IncorrectModelWithSimpleAnnotations(models.Model):
charfield: models.CharField = models.CharField(max_length=255, null=True)
textfield: models.TextField = models.TextField(max_length=255, null=True)
slugfield: models.SlugField = models.SlugField(max_length=255, null=True)

View File

@@ -1,7 +0,0 @@
# Regression test for https://github.com/astral-sh/ruff/issues/21023
'' '
"" ""
'' '' '
"" "" "
f"" f"
f"" f"" f"

View File

@@ -359,29 +359,3 @@ class Generic5(list[PotentialTypeVar]):
def __new__(cls: type[Generic5]) -> Generic5: ...
def __enter__(self: Generic5) -> Generic5: ...
# Test cases based on issue #20781 - metaclasses that triggers IsMetaclass::Maybe
class MetaclassInWhichSelfCannotBeUsed5(type(Protocol)):
def __new__(
cls, name: str, bases: tuple[type[Any], ...], attrs: dict[str, Any], **kwargs: Any
) -> MetaclassInWhichSelfCannotBeUsed5:
new_class = super().__new__(cls, name, bases, attrs, **kwargs)
return new_class
import django.db.models.base
class MetaclassInWhichSelfCannotBeUsed6(django.db.models.base.ModelBase):
def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MetaclassInWhichSelfCannotBeUsed6:
...
class MetaclassInWhichSelfCannotBeUsed7(django.db.models.base.ModelBase):
def __new__(cls, /, name: str, bases: tuple[object, ...], attrs: dict[str, object], **kwds: object) -> MetaclassInWhichSelfCannotBeUsed7:
...
class MetaclassInWhichSelfCannotBeUsed8(django.db.models.base.ModelBase):
def __new__(cls, name: builtins.str, bases: tuple, attributes: dict, /, **kw) -> MetaclassInWhichSelfCannotBeUsed8:
...

View File

@@ -252,28 +252,3 @@ from some_module import PotentialTypeVar
class Generic5(list[PotentialTypeVar]):
def __new__(cls: type[Generic5]) -> Generic5: ...
def __enter__(self: Generic5) -> Generic5: ...
# Test case based on issue #20781 - metaclass that triggers IsMetaclass::Maybe
class MetaclassInWhichSelfCannotBeUsed5(type(Protocol)):
def __new__(
cls, name: str, bases: tuple[type[Any], ...], attrs: dict[str, Any], **kwargs: Any
) -> MetaclassInWhichSelfCannotBeUsed5: ...
import django.db.models.base
class MetaclassInWhichSelfCannotBeUsed6(django.db.models.base.ModelBase):
def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MetaclassInWhichSelfCannotBeUsed6:
...
class MetaclassInWhichSelfCannotBeUsed7(django.db.models.base.ModelBase):
def __new__(cls, /, name: str, bases: tuple[object, ...], attrs: dict[str, object], **kwds: object) -> MetaclassInWhichSelfCannotBeUsed7:
...
class MetaclassInWhichSelfCannotBeUsed8(django.db.models.base.ModelBase):
def __new__(cls, name: builtins.str, bases: tuple, attributes: dict, /, **kw) -> MetaclassInWhichSelfCannotBeUsed8:
...

View File

@@ -14,14 +14,3 @@ def f():
import os
print(os)
# regression test for https://github.com/astral-sh/ruff/issues/21121
from dataclasses import KW_ONLY, dataclass
@dataclass
class DataClass:
a: int
_: KW_ONLY # should be an exception to TC003, even with future-annotations
b: int

View File

@@ -370,22 +370,3 @@ class Foo:
The flag converter instance with all flags parsed.
"""
return
# OK
def baz(x: int) -> int:
"""
Show a `Warnings` DOC102 false positive.
Parameters
----------
x : int
Warnings
--------
This function demonstrates a DOC102 false positive
Returns
-------
int
"""
return x

View File

@@ -81,55 +81,3 @@ def calculate_speed(distance: float, time: float) -> float:
except TypeError:
print("Not a number? Shame on you!")
raise
# This should NOT trigger DOC502 because OSError is explicitly re-raised
def f():
"""Do nothing.
Raises:
OSError: If the OS errors.
"""
try:
pass
except OSError as e:
raise e
# This should NOT trigger DOC502 because OSError is explicitly re-raised with from None
def g():
"""Do nothing.
Raises:
OSError: If the OS errors.
"""
try:
pass
except OSError as e:
raise e from None
# This should NOT trigger DOC502 because ValueError is explicitly re-raised from tuple exception
def h():
"""Do nothing.
Raises:
ValueError: If something goes wrong.
"""
try:
pass
except (ValueError, TypeError) as e:
raise e
# This should NOT trigger DOC502 because TypeError is explicitly re-raised from tuple exception
def i():
"""Do nothing.
Raises:
TypeError: If something goes wrong.
"""
try:
pass
except (ValueError, TypeError) as e:
raise e

View File

@@ -1,21 +0,0 @@
class C:
f = lambda self: __class__
print(C().f().__name__)
# Test: nested lambda
class D:
g = lambda self: (lambda: __class__)
print(D().g()().__name__)
# Test: lambda outside class (should still fail)
h = lambda: __class__
# Test: lambda referencing module-level variable (should not be flagged as F821)
import uuid
class E:
uuid = lambda: str(uuid.uuid4())

View File

@@ -1,131 +0,0 @@
"""Test cases for PLR1708 stop-iteration-return."""
# Valid cases - should not trigger the rule
def normal_function():
raise StopIteration # Not a generator, should not trigger
def normal_function_with_value():
raise StopIteration("value") # Not a generator, should not trigger
def generator_with_return():
yield 1
yield 2
return "finished" # This is the correct way
def generator_with_yield_from():
yield from [1, 2, 3]
def generator_without_stop_iteration():
yield 1
yield 2
# No explicit termination
def generator_with_other_exception():
yield 1
raise ValueError("something else") # Different exception
# Invalid cases - should trigger the rule
def generator_with_stop_iteration():
yield 1
yield 2
raise StopIteration # Should trigger
def generator_with_stop_iteration_value():
yield 1
yield 2
raise StopIteration("finished") # Should trigger
def generator_with_stop_iteration_expr():
yield 1
yield 2
raise StopIteration(1 + 2) # Should trigger
def async_generator_with_stop_iteration():
yield 1
yield 2
raise StopIteration("async") # Should trigger
def nested_generator():
def inner_gen():
yield 1
raise StopIteration("inner") # Should trigger
yield from inner_gen()
def generator_in_class():
class MyClass:
def generator_method(self):
yield 1
raise StopIteration("method") # Should trigger
return MyClass
# Complex cases
def complex_generator():
try:
yield 1
yield 2
raise StopIteration("complex") # Should trigger
except ValueError:
yield 3
finally:
pass
def generator_with_conditional_stop_iteration(condition):
yield 1
if condition:
raise StopIteration("conditional") # Should trigger
yield 2
# Edge cases
def generator_with_bare_stop_iteration():
yield 1
raise StopIteration # Should trigger (no arguments)
def generator_with_stop_iteration_in_loop():
for i in range(5):
yield i
if i == 3:
raise StopIteration("loop") # Should trigger
# Should not trigger - different exceptions
def generator_with_runtime_error():
yield 1
raise RuntimeError("not StopIteration") # Should not trigger
def generator_with_custom_exception():
yield 1
raise CustomException("custom") # Should not trigger
class CustomException(Exception):
pass
# Generator comprehensions should not be affected
list_comp = [x for x in range(10)] # Should not trigger
# Lambda in generator context
def generator_with_lambda():
yield 1
func = lambda x: x # Just a regular lambda
yield 2

View File

@@ -1,17 +0,0 @@
"""
Regression test for an ecosystem hit on
https://github.com/astral-sh/ruff/pull/21125.
We should mark all of the components of special dataclass annotations as
runtime-required, not just the first layer.
"""
from dataclasses import dataclass
from typing import ClassVar, Optional
@dataclass(frozen=True)
class EmptyCell:
_singleton: ClassVar[Optional["EmptyCell"]] = None
# the behavior of _singleton above should match a non-ClassVar
_doubleton: "EmptyCell"

View File

@@ -1,13 +0,0 @@
"""This is placed in a separate fixture as `TypeVar` needs to be imported
from `typing_extensions` to support default arguments in Python version < 3.13.
We verify that UP046 doesn't apply in this case.
"""
from typing import Generic
from typing_extensions import TypeVar
T = TypeVar("T", default=str)
class DefaultTypeVar(Generic[T]):
var: T

View File

@@ -1,12 +0,0 @@
"""This is placed in a separate fixture as `TypeVar` needs to be imported
from `typing_extensions` to support default arguments in Python version < 3.13.
We verify that UP047 doesn't apply in this case.
"""
from typing_extensions import TypeVar
T = TypeVar("T", default=int)
def default_var(var: T) -> T:
return var

View File

@@ -69,19 +69,3 @@ Decimal(float("\N{space}\N{hyPHen-MINus}nan"))
Decimal(float("\x20\N{character tabulation}\N{hyphen-minus}nan"))
Decimal(float(" -" "nan"))
Decimal(float("-nAn"))
# Test cases for digit separators (safe fixes)
# https://github.com/astral-sh/ruff/issues/20572
Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
# Test cases for non-thousands separators
Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators
Decimal("1234_5678") # Safe fix: preserves non-thousands separators
# Separators _and_ leading zeros
Decimal("0001_2345")
Decimal("000_1_2345")
Decimal("000_000")

View File

@@ -43,29 +43,3 @@ logging.warning("Value: %r", repr(42))
logging.error("Error: %r", repr([1, 2, 3]))
logging.info("Debug info: %s", repr("test\nstring"))
logging.warning("Value: %s", repr(42))
# %s + ascii()
logging.info("ASCII: %s", ascii("Hello\nWorld"))
logging.warning("ASCII: %s", ascii("test"))
# %s + oct()
logging.info("Octal: %s", oct(42))
logging.warning("Octal: %s", oct(255))
# %s + hex()
logging.info("Hex: %s", hex(42))
logging.warning("Hex: %s", hex(255))
# Test with imported functions
from logging import info, log
info("ASCII: %s", ascii("Hello\nWorld"))
log(logging.INFO, "ASCII: %s", ascii("test"))
info("Octal: %s", oct(42))
log(logging.INFO, "Octal: %s", oct(255))
info("Hex: %s", hex(42))
log(logging.INFO, "Hex: %s", hex(255))

View File

@@ -1,12 +0,0 @@
async def f(): return [[x async for x in foo(n)] for n in range(3)]
async def test(): return [[x async for x in elements(n)] async for n in range(3)]
async def f(): [x for x in foo()] and [x async for x in foo()]
async def f():
def g(): ...
[x async for x in foo()]
[x async for x in y]

View File

@@ -1,3 +0,0 @@
match x:
case Point(x=1, x=2):
pass

View File

@@ -1,3 +0,0 @@
match x:
case {'key': 1, 'key': 2}:
pass

View File

@@ -1 +0,0 @@
class C[T, T]: pass

View File

@@ -1,29 +0,0 @@
def f(a):
global a
def g(a):
if True:
global a
def h(a):
def inner():
global a
def i(a):
try:
global a
except Exception:
pass
def f(a):
a = 1
global a
def f(a):
a = 1
a = 2
global a
def f(a):
class Inner:
global a # ok

View File

@@ -1,8 +0,0 @@
type X[T: (yield 1)] = int
type Y = (yield 1)
def f[T](x: int) -> (y := 3): return x
class C[T]((yield from [object])):
pass

View File

@@ -1,8 +0,0 @@
def func():
return *x
for *x in range(10):
pass
def func():
yield *x

View File

@@ -1,11 +0,0 @@
match value:
case _:
pass
case 1:
pass
match value:
case irrefutable:
pass
case 1:
pass

View File

@@ -1,5 +0,0 @@
match x:
case [a, a]:
pass
case _:
pass

View File

@@ -1 +0,0 @@
[x:= 2 for x in range(2)]

View File

@@ -1,7 +0,0 @@
__debug__ = False
def process(__debug__):
pass
class Generic[__debug__]:
pass

View File

@@ -951,9 +951,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::MisplacedBareRaise) {
pylint::rules::misplaced_bare_raise(checker, raise);
}
if checker.is_rule_enabled(Rule::StopIterationReturn) {
pylint::rules::stop_iteration_return(checker, raise);
}
}
Stmt::AugAssign(aug_assign @ ast::StmtAugAssign { target, .. }) => {
if checker.is_rule_enabled(Rule::GlobalStatement) {

View File

@@ -1400,14 +1400,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(annotation);
}
AnnotationContext::RuntimeEvaluated
if flake8_type_checking::helpers::is_dataclass_meta_annotation(
annotation,
self.semantic(),
) =>
{
self.visit_runtime_required_annotation(annotation);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(annotation);
}
@@ -2124,7 +2116,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
| Expr::DictComp(_)
| Expr::SetComp(_) => {
self.analyze.scopes.push(self.semantic.scope_id);
self.semantic.pop_scope(); // Lambda/Generator/Comprehension scope
self.semantic.pop_scope();
}
_ => {}
}
@@ -3049,35 +3041,7 @@ impl<'a> Checker<'a> {
if let Some(parameters) = parameters {
self.visit_parameters(parameters);
}
// Here we add the implicit scope surrounding a lambda which allows code in the
// lambda to access `__class__` at runtime when the lambda is defined within a class.
// See the `ScopeKind::DunderClassCell` docs for more information.
let added_dunder_class_scope = if self
.semantic
.current_scopes()
.any(|scope| scope.kind.is_class())
{
self.semantic.push_scope(ScopeKind::DunderClassCell);
let binding_id = self.semantic.push_binding(
TextRange::default(),
BindingKind::DunderClassCell,
BindingFlags::empty(),
);
self.semantic
.current_scope_mut()
.add("__class__", binding_id);
true
} else {
false
};
self.visit_expr(body);
// Pop the DunderClassCell scope if it was added
if added_dunder_class_scope {
self.semantic.pop_scope();
}
}
}
self.semantic.restore(snapshot);

View File

@@ -286,7 +286,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1702") => rules::pylint::rules::TooManyNestedBlocks,
(Pylint, "R1704") => rules::pylint::rules::RedefinedArgumentFromLocal,
(Pylint, "R1706") => rules::pylint::rules::AndOrTernary,
(Pylint, "R1708") => rules::pylint::rules::StopIterationReturn,
(Pylint, "R1711") => rules::pylint::rules::UselessReturn,
(Pylint, "R1714") => rules::pylint::rules::RepeatedEqualityComparison,
(Pylint, "R1722") => rules::pylint::rules::SysExitAlias,

View File

@@ -11,8 +11,6 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[
SectionKind::References,
SectionKind::Returns,
SectionKind::SeeAlso,
SectionKind::Warnings,
SectionKind::Warns,
SectionKind::Yields,
// Google-only
SectionKind::Args,
@@ -34,5 +32,7 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[
SectionKind::Tip,
SectionKind::Todo,
SectionKind::Warning,
SectionKind::Warnings,
SectionKind::Warns,
SectionKind::Yield,
];

View File

@@ -11,14 +11,11 @@ pub(crate) static NUMPY_SECTIONS: &[SectionKind] = &[
SectionKind::References,
SectionKind::Returns,
SectionKind::SeeAlso,
SectionKind::Warnings,
SectionKind::Warns,
SectionKind::Yields,
// NumPy-only
SectionKind::ExtendedSummary,
SectionKind::OtherParams,
SectionKind::OtherParameters,
SectionKind::Parameters,
SectionKind::Receives,
SectionKind::ShortSummary,
];

View File

@@ -36,7 +36,6 @@ pub(crate) enum SectionKind {
OtherParameters,
Parameters,
Raises,
Receives,
References,
Return,
Returns,
@@ -77,7 +76,6 @@ impl SectionKind {
"other parameters" => Some(Self::OtherParameters),
"parameters" => Some(Self::Parameters),
"raises" => Some(Self::Raises),
"receives" => Some(Self::Receives),
"references" => Some(Self::References),
"return" => Some(Self::Return),
"returns" => Some(Self::Returns),
@@ -119,7 +117,6 @@ impl SectionKind {
Self::OtherParameters => "Other Parameters",
Self::Parameters => "Parameters",
Self::Raises => "Raises",
Self::Receives => "Receives",
Self::References => "References",
Self::Return => "Return",
Self::Returns => "Returns",

View File

@@ -14,7 +14,7 @@ use ruff_text_size::TextSize;
/// The length of a line of text that is considered too long.
///
/// The allowed range of values is 1..=320
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LineLength(
#[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))] NonZeroU16,
@@ -46,21 +46,6 @@ impl fmt::Display for LineLength {
}
}
impl<'de> serde::Deserialize<'de> for LineLength {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = u16::deserialize(deserializer)?;
Self::try_from(value).map_err(|_| {
serde::de::Error::custom(format!(
"line-length must be between 1 and {} (got {value})",
Self::MAX,
))
})
}
}
impl CacheKey for LineLength {
fn cache_key(&self, state: &mut CacheKeyHasher) {
state.write_u16(self.0.get());

View File

@@ -919,6 +919,17 @@ mod tests {
Ok(())
}
/// Wrapper around `test_contents_syntax_errors` for testing a snippet of code instead of a
/// file.
fn test_snippet_syntax_errors(contents: &str, settings: &LinterSettings) -> Vec<Diagnostic> {
let contents = dedent(contents);
test_contents_syntax_errors(
&SourceKind::Python(contents.to_string()),
Path::new("<filename>"),
settings,
)
}
/// A custom test runner that prints syntax errors in addition to other diagnostics. Adapted
/// from `flakes` in pyflakes/mod.rs.
fn test_contents_syntax_errors(
@@ -961,38 +972,245 @@ mod tests {
}
#[test_case(
Path::new("async_comprehension_outside_async_function.py"),
PythonVersion::PY311
"async_in_sync_error_on_310",
"async def f(): return [[x async for x in foo(n)] for n in range(3)]",
PythonVersion::PY310,
"AsyncComprehensionOutsideAsyncFunction"
)]
#[test_case(
Path::new("async_comprehension_outside_async_function.py"),
PythonVersion::PY310
"async_in_sync_okay_on_311",
"async def f(): return [[x async for x in foo(n)] for n in range(3)]",
PythonVersion::PY311,
"AsyncComprehensionOutsideAsyncFunction"
)]
#[test_case(Path::new("rebound_comprehension.py"), PythonVersion::PY310)]
#[test_case(Path::new("duplicate_type_parameter.py"), PythonVersion::PY312)]
#[test_case(Path::new("multiple_case_assignment.py"), PythonVersion::PY310)]
#[test_case(Path::new("duplicate_match_key.py"), PythonVersion::PY310)]
#[test_case(Path::new("duplicate_match_class_attribute.py"), PythonVersion::PY310)]
#[test_case(Path::new("invalid_star_expression.py"), PythonVersion::PY310)]
#[test_case(Path::new("irrefutable_case_pattern.py"), PythonVersion::PY310)]
#[test_case(Path::new("single_starred_assignment.py"), PythonVersion::PY310)]
#[test_case(Path::new("write_to_debug.py"), PythonVersion::PY312)]
#[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)]
fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> {
let snapshot = format!(
"semantic_syntax_error_{}_{}",
path.to_string_lossy(),
python_version
);
let path = Path::new("resources/test/fixtures/semantic_errors").join(path);
let contents = std::fs::read_to_string(&path)?;
let source_kind = SourceKind::Python(contents);
#[test_case(
"async_in_sync_okay_on_310",
"async def test(): return [[x async for x in elements(n)] async for n in range(3)]",
PythonVersion::PY310,
"AsyncComprehensionOutsideAsyncFunction"
)]
#[test_case(
"deferred_function_body",
"
async def f(): [x for x in foo()] and [x async for x in foo()]
async def f():
def g(): ...
[x async for x in foo()]
",
PythonVersion::PY310,
"AsyncComprehensionOutsideAsyncFunction"
)]
#[test_case(
"async_in_sync_false_positive",
"[x async for x in y]",
PythonVersion::PY310,
"AsyncComprehensionOutsideAsyncFunction"
)]
#[test_case(
"rebound_comprehension",
"[x:= 2 for x in range(2)]",
PythonVersion::PY310,
"ReboundComprehensionVariable"
)]
#[test_case(
"duplicate_type_param",
"class C[T, T]: pass",
PythonVersion::PY312,
"DuplicateTypeParameter"
)]
#[test_case(
"multiple_case_assignment",
"
match x:
case [a, a]:
pass
case _:
pass
",
PythonVersion::PY310,
"MultipleCaseAssignment"
)]
#[test_case(
"duplicate_match_key",
"
match x:
case {'key': 1, 'key': 2}:
pass
",
PythonVersion::PY310,
"DuplicateMatchKey"
)]
#[test_case(
"global_parameter",
"
def f(a):
global a
let diagnostics = test_contents_syntax_errors(
&source_kind,
&path,
def g(a):
if True:
global a
def h(a):
def inner():
global a
def i(a):
try:
global a
except Exception:
pass
def f(a):
a = 1
global a
def f(a):
a = 1
a = 2
global a
def f(a):
class Inner:
global a # ok
",
PythonVersion::PY310,
"GlobalParameter"
)]
#[test_case(
"duplicate_match_class_attribute",
"
match x:
case Point(x=1, x=2):
pass
",
PythonVersion::PY310,
"DuplicateMatchClassAttribute"
)]
#[test_case(
"invalid_star_expression",
"
def func():
return *x
",
PythonVersion::PY310,
"InvalidStarExpression"
)]
#[test_case(
"invalid_star_expression_for",
"
for *x in range(10):
pass
",
PythonVersion::PY310,
"InvalidStarExpression"
)]
#[test_case(
"invalid_star_expression_yield",
"
def func():
yield *x
",
PythonVersion::PY310,
"InvalidStarExpression"
)]
#[test_case(
"irrefutable_case_pattern_wildcard",
"
match value:
case _:
pass
case 1:
pass
",
PythonVersion::PY310,
"IrrefutableCasePattern"
)]
#[test_case(
"irrefutable_case_pattern_capture",
"
match value:
case irrefutable:
pass
case 1:
pass
",
PythonVersion::PY310,
"IrrefutableCasePattern"
)]
#[test_case(
"single_starred_assignment",
"*a = [1, 2, 3, 4]",
PythonVersion::PY310,
"SingleStarredAssignment"
)]
#[test_case(
"write_to_debug",
"
__debug__ = False
",
PythonVersion::PY310,
"WriteToDebug"
)]
#[test_case(
"write_to_debug_in_function_param",
"
def process(__debug__):
pass
",
PythonVersion::PY310,
"WriteToDebug"
)]
#[test_case(
"write_to_debug_class_type_param",
"
class Generic[__debug__]:
pass
",
PythonVersion::PY312,
"WriteToDebug"
)]
#[test_case(
"invalid_expression_yield_in_type_param",
"
type X[T: (yield 1)] = int
",
PythonVersion::PY312,
"InvalidExpression"
)]
#[test_case(
"invalid_expression_yield_in_type_alias",
"
type Y = (yield 1)
",
PythonVersion::PY312,
"InvalidExpression"
)]
#[test_case(
"invalid_expression_walrus_in_return_annotation",
"
def f[T](x: int) -> (y := 3): return x
",
PythonVersion::PY312,
"InvalidExpression"
)]
#[test_case(
"invalid_expression_yield_from_in_base_class",
"
class C[T]((yield from [object])):
pass
",
PythonVersion::PY312,
"InvalidExpression"
)]
fn test_semantic_errors(
name: &str,
contents: &str,
python_version: PythonVersion,
error_type: &str,
) {
let snapshot = format!("semantic_syntax_error_{error_type}_{name}_{python_version}");
let diagnostics = test_snippet_syntax_errors(
contents,
&LinterSettings {
rules: settings::rule_table::RuleTable::empty(),
unresolved_target_version: python_version.into(),
@@ -1000,11 +1218,7 @@ mod tests {
..Default::default()
},
);
insta::with_settings!({filters => vec![(r"\\", "/")]}, {
assert_diagnostics!(format!("{snapshot}"), diagnostics);
});
Ok(())
assert_diagnostics!(snapshot, diagnostics);
}
#[test_case(PythonVersion::PY310)]

View File

@@ -492,12 +492,6 @@ fn check_method(checker: &Checker, call_expr: &ExprCall) {
"collected_datasets" => Replacement::AttrName("collected_assets"),
_ => return,
},
["airflow", "models", "dag", "DAG"] | ["airflow", "models", "DAG"] | ["airflow", "DAG"] => {
match attr.as_str() {
"create_dagrun" => Replacement::None,
_ => return,
}
}
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
"initialize_providers_dataset_uri_resources" => {
Replacement::AttrName("initialize_providers_asset_uri_resources")

View File

@@ -288,12 +288,10 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
},
// airflow.model..DAG
["airflow", "models", "dag", "DAG"] | ["airflow", "models", "DAG"] | ["airflow", "DAG"] => {
Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: "DAG".to_string(),
}
}
["airflow", "models", .., "DAG"] => Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: "DAG".to_string(),
},
// airflow.sensors.base
[

View File

@@ -1,25 +1,6 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
---
AIR311 [*] `airflow.DAG` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_args.py:13:1
|
13 | DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback)
| ^^^
|
help: Use `DAG` from `airflow.sdk` instead.
2 |
3 | from datetime import timedelta
4 |
- from airflow import DAG, dag
5 + from airflow import dag
6 | from airflow.operators.datetime import BranchDateTimeOperator
7 + from airflow.sdk import DAG
8 |
9 |
10 | def sla_callback(*arg, **kwargs):
note: This is an unsafe fix and may change runtime behavior
AIR311 `sla_miss_callback` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_args.py:13:34
|

View File

@@ -375,7 +375,7 @@ impl Violation for SuspiciousEvalUsage {
///
///
/// def render_username(username):
/// return format_html("<i>{}</i>", username) # username is escaped.
/// return django.utils.html.format_html("<i>{}</i>", username) # username is escaped.
/// ```
///
/// ## References

View File

@@ -61,14 +61,9 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) {
}
for statement in body {
let value = match statement {
Stmt::Assign(ast::StmtAssign { value, .. }) => value,
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
}) => value,
_ => continue,
let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
continue;
};
if let Some(field_name) = is_nullable_field(value, checker.semantic()) {
checker.report_diagnostic(
DjangoNullableModelStringField {

View File

@@ -186,32 +186,3 @@ DJ001 Avoid using `null=True` on string-based fields such as `URLField`
30 | urlfield = models.URLField(max_length=255, null=True)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
DJ001 Avoid using `null=True` on string-based fields such as `CharField`
--> DJ001.py:52:35
|
51 | class IncorrectModelWithSimpleAnnotations(models.Model):
52 | charfield: models.CharField = models.CharField(max_length=255, null=True)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53 | textfield: models.TextField = models.TextField(max_length=255, null=True)
54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True)
|
DJ001 Avoid using `null=True` on string-based fields such as `TextField`
--> DJ001.py:53:35
|
51 | class IncorrectModelWithSimpleAnnotations(models.Model):
52 | charfield: models.CharField = models.CharField(max_length=255, null=True)
53 | textfield: models.TextField = models.TextField(max_length=255, null=True)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True)
|
DJ001 Avoid using `null=True` on string-based fields such as `SlugField`
--> DJ001.py:54:35
|
52 | charfield: models.CharField = models.CharField(max_length=255, null=True)
53 | textfield: models.TextField = models.TextField(max_length=255, null=True)
54 | slugfield: models.SlugField = models.SlugField(max_length=255, null=True)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -23,14 +23,6 @@ mod tests {
Rule::MultiLineImplicitStringConcatenation,
Path::new("ISC_syntax_error.py")
)]
#[test_case(
Rule::SingleLineImplicitStringConcatenation,
Path::new("ISC_syntax_error_2.py")
)]
#[test_case(
Rule::MultiLineImplicitStringConcatenation,
Path::new("ISC_syntax_error_2.py")
)]
#[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());

View File

@@ -1,12 +1,13 @@
use std::borrow::Cow;
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::StringFlags;
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_index::Indexer;
use ruff_python_parser::{Token, TokenKind, Tokens};
use ruff_python_parser::{TokenKind, Tokens};
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange};
use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use crate::checkers::ast::LintContext;
@@ -168,8 +169,7 @@ pub(crate) fn implicit(
SingleLineImplicitStringConcatenation,
TextRange::new(a_range.start(), b_range.end()),
) {
if let Some(fix) = concatenate_strings(a_token, b_token, a_range, b_range, locator)
{
if let Some(fix) = concatenate_strings(a_range, b_range, locator) {
diagnostic.set_fix(fix);
}
}
@@ -177,55 +177,38 @@ pub(crate) fn implicit(
}
}
/// Concatenates two strings
///
/// The `a_string_range` and `b_string_range` are the range of the entire string,
/// not just of the string token itself (important for interpolated strings where
/// the start token doesn't span the entire token).
fn concatenate_strings(
a_token: &Token,
b_token: &Token,
a_string_range: TextRange,
b_string_range: TextRange,
locator: &Locator,
) -> Option<Fix> {
if a_token.string_flags()?.is_unclosed() || b_token.string_flags()?.is_unclosed() {
fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option<Fix> {
let a_text = locator.slice(a_range);
let b_text = locator.slice(b_range);
let a_leading_quote = leading_quote(a_text)?;
let b_leading_quote = leading_quote(b_text)?;
// Require, for now, that the leading quotes are the same.
if a_leading_quote != b_leading_quote {
return None;
}
let a_string_flags = a_token.string_flags()?;
let b_string_flags = b_token.string_flags()?;
let a_trailing_quote = trailing_quote(a_text)?;
let b_trailing_quote = trailing_quote(b_text)?;
let a_prefix = a_string_flags.prefix();
let b_prefix = b_string_flags.prefix();
// Require, for now, that the strings have the same prefix,
// quote style, and number of quotes
if a_prefix != b_prefix
|| a_string_flags.quote_style() != b_string_flags.quote_style()
|| a_string_flags.is_triple_quoted() != b_string_flags.is_triple_quoted()
{
// Require, for now, that the trailing quotes are the same.
if a_trailing_quote != b_trailing_quote {
return None;
}
let a_text = locator.slice(a_string_range);
let b_text = locator.slice(b_string_range);
let quotes = a_string_flags.quote_str();
let opener_len = a_string_flags.opener_len();
let closer_len = a_string_flags.closer_len();
let mut a_body =
Cow::Borrowed(&a_text[TextRange::new(opener_len, a_text.text_len() - closer_len)]);
let b_body = &b_text[TextRange::new(opener_len, b_text.text_len() - closer_len)];
Cow::Borrowed(&a_text[a_leading_quote.len()..a_text.len() - a_trailing_quote.len()]);
let b_body = &b_text[b_leading_quote.len()..b_text.len() - b_trailing_quote.len()];
if !a_string_flags.is_raw_string() && matches!(b_body.bytes().next(), Some(b'0'..=b'7')) {
if a_leading_quote.find(['r', 'R']).is_none()
&& matches!(b_body.bytes().next(), Some(b'0'..=b'7'))
{
normalize_ending_octal(&mut a_body);
}
let concatenation = format!("{a_prefix}{quotes}{a_body}{b_body}{quotes}");
let range = TextRange::new(a_string_range.start(), b_string_range.end());
let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}");
let range = TextRange::new(a_range.start(), b_range.end());
Some(Fix::safe_edit(Edit::range_replacement(
concatenation,

View File

@@ -1,134 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
---
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error_2.py:2:1
|
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
2 | '' '
| ^^^^
3 | "" ""
4 | '' '' '
|
help: Combine string literals
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error_2.py:2:4
|
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
2 | '' '
| ^
3 | "" ""
4 | '' '' '
|
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error_2.py:3:1
|
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
2 | '' '
3 | "" ""
| ^^^^^
4 | '' '' '
5 | "" "" "
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error_2.py:4:1
|
2 | '' '
3 | "" ""
4 | '' '' '
| ^^^^^
5 | "" "" "
6 | f"" f"
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error_2.py:4:4
|
2 | '' '
3 | "" ""
4 | '' '' '
| ^^^^
5 | "" "" "
6 | f"" f"
|
help: Combine string literals
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error_2.py:4:7
|
2 | '' '
3 | "" ""
4 | '' '' '
| ^
5 | "" "" "
6 | f"" f"
|
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error_2.py:5:1
|
3 | "" ""
4 | '' '' '
5 | "" "" "
| ^^^^^
6 | f"" f"
7 | f"" f"" f"
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error_2.py:5:4
|
3 | "" ""
4 | '' '' '
5 | "" "" "
| ^^^^
6 | f"" f"
7 | f"" f"" f"
|
help: Combine string literals
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error_2.py:5:7
|
3 | "" ""
4 | '' '' '
5 | "" "" "
| ^
6 | f"" f"
7 | f"" f"" f"
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error_2.py:6:7
|
4 | '' '' '
5 | "" "" "
6 | f"" f"
| ^
7 | f"" f"" f"
|
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error_2.py:7:1
|
5 | "" "" "
6 | f"" f"
7 | f"" f"" f"
| ^^^^^^^
|
help: Combine string literals
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error_2.py:7:11
|
5 | "" "" "
6 | f"" f"
7 | f"" f"" f"
| ^
|

View File

@@ -1,53 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
---
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error_2.py:2:4
|
1 | # Regression test for https://github.com/astral-sh/ruff/issues/21023
2 | '' '
| ^
3 | "" ""
4 | '' '' '
|
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error_2.py:4:7
|
2 | '' '
3 | "" ""
4 | '' '' '
| ^
5 | "" "" "
6 | f"" f"
|
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error_2.py:5:7
|
3 | "" ""
4 | '' '' '
5 | "" "" "
| ^
6 | f"" f"
7 | f"" f"" f"
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error_2.py:6:7
|
4 | '' '' '
5 | "" "" "
6 | f"" f"
| ^
7 | f"" f"" f"
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error_2.py:7:11
|
5 | "" "" "
6 | f"" f"
7 | f"" f"" f"
| ^
|

View File

@@ -50,29 +50,6 @@ use ruff_text_size::Ranged;
/// 1. `__aiter__` methods that return `AsyncIterator`, despite the class
/// inheriting directly from `AsyncIterator`.
///
/// The rule attempts to avoid flagging methods on metaclasses, since
/// [PEP 673] specifies that `Self` is disallowed in metaclasses. Ruff can
/// detect a class as being a metaclass if it inherits from a stdlib
/// metaclass such as `builtins.type` or `abc.ABCMeta`, and additionally
/// infers that a class may be a metaclass if it has a `__new__` method
/// with a similar signature to `type.__new__`. The heuristic used to
/// identify a metaclass-like `__new__` method signature is that it:
///
/// 1. Has exactly 5 parameters (including `cls`)
/// 1. Has a second parameter annotated with `str`
/// 1. Has a third parameter annotated with a `tuple` type
/// 1. Has a fourth parameter annotated with a `dict` type
/// 1. Has a fifth parameter is keyword-variadic (`**kwargs`)
///
/// For example, the following class would be detected as a metaclass, disabling
/// the rule:
///
/// ```python
/// class MyMetaclass(django.db.models.base.ModelBase):
/// def __new__(cls, name: str, bases: tuple[Any, ...], attrs: dict[str, Any], **kwargs: Any) -> MyMetaclass:
/// ...
/// ```
///
/// ## Example
///
/// ```pyi
@@ -110,8 +87,6 @@ use ruff_text_size::Ranged;
///
/// ## References
/// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self)
///
/// [PEP 673]: https://peps.python.org/pep-0673/#valid-locations-for-self
#[derive(ViolationMetadata)]
#[violation_metadata(stable_since = "v0.0.271")]
pub(crate) struct NonSelfReturnType {
@@ -168,10 +143,7 @@ pub(crate) fn non_self_return_type(
};
// PEP 673 forbids the use of `typing(_extensions).Self` in metaclasses.
if !matches!(
analyze::class::is_metaclass(class_def, semantic),
analyze::class::IsMetaclass::No
) {
if analyze::class::is_metaclass(class_def, semantic).is_yes() {
return;
}

View File

@@ -451,7 +451,6 @@ help: Use `Self` as return type
359 + def __new__(cls) -> typing.Self: ...
360 | def __enter__(self: Generic5) -> Generic5: ...
361 |
362 |
note: This is an unsafe fix and may change runtime behavior
PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` at runtime
@@ -469,6 +468,4 @@ help: Use `Self` as return type
- def __enter__(self: Generic5) -> Generic5: ...
360 + def __enter__(self) -> typing.Self: ...
361 |
362 |
363 | # Test cases based on issue #20781 - metaclasses that triggers IsMetaclass::Maybe
note: This is an unsafe fix and may change runtime behavior

View File

@@ -431,8 +431,6 @@ help: Use `Self` as return type
- def __new__(cls: type[Generic5]) -> Generic5: ...
253 + def __new__(cls) -> typing.Self: ...
254 | def __enter__(self: Generic5) -> Generic5: ...
255 |
256 |
note: This is a display-only fix and is likely to be incorrect
PYI034 [*] `__enter__` methods in classes like `Generic5` usually return `self` at runtime
@@ -449,7 +447,4 @@ help: Use `Self` as return type
253 | def __new__(cls: type[Generic5]) -> Generic5: ...
- def __enter__(self: Generic5) -> Generic5: ...
254 + def __enter__(self) -> typing.Self: ...
255 |
256 |
257 | # Test case based on issue #20781 - metaclass that triggers IsMetaclass::Maybe
note: This is a display-only fix and is likely to be incorrect

View File

@@ -98,26 +98,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TC003.py"))]
fn add_future_import_dataclass_kw_only_py313(rule: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"add_future_import_kw_only__{}_{}",
rule.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
future_annotations: true,
// The issue in #21121 also didn't trigger on Python 3.14
unresolved_target_version: PythonVersion::PY313.into(),
..settings::LinterSettings::for_rule(rule)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
// we test these rules as a pair, since they're opposites of one another
// so we want to make sure their fixes are not going around in circles.
#[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))]

View File

@@ -1,28 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC003 [*] Move standard library import `os` into a type-checking block
--> TC003.py:8:12
|
7 | def f():
8 | import os
| ^^
9 |
10 | x: os
|
help: Move into type-checking block
2 |
3 | For typing-only import detection tests, see `TC002.py`.
4 | """
5 + from typing import TYPE_CHECKING
6 +
7 + if TYPE_CHECKING:
8 + import os
9 |
10 |
11 | def f():
- import os
12 |
13 | x: os
14 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -9,7 +9,6 @@ use ruff_python_semantic::{Definition, SemanticModel};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use rustc_hash::FxHashMap;
use crate::Violation;
use crate::checkers::ast::Checker;
@@ -824,8 +823,6 @@ struct BodyVisitor<'a> {
currently_suspended_exceptions: Option<&'a ast::Expr>,
raised_exceptions: Vec<ExceptionEntry<'a>>,
semantic: &'a SemanticModel<'a>,
/// Maps exception variable names to their exception expressions in the current except clause
exception_variables: FxHashMap<&'a str, &'a ast::Expr>,
}
impl<'a> BodyVisitor<'a> {
@@ -836,7 +833,6 @@ impl<'a> BodyVisitor<'a> {
currently_suspended_exceptions: None,
raised_exceptions: Vec::new(),
semantic,
exception_variables: FxHashMap::default(),
}
}
@@ -868,47 +864,20 @@ impl<'a> BodyVisitor<'a> {
raised_exceptions,
}
}
/// Store `exception` if its qualified name does not correspond to one of the exempt types.
fn maybe_store_exception(&mut self, exception: &'a Expr, range: TextRange) {
let Some(qualified_name) = self.semantic.resolve_qualified_name(exception) else {
return;
};
if is_exception_or_base_exception(&qualified_name) {
return;
}
self.raised_exceptions.push(ExceptionEntry {
qualified_name,
range,
});
}
}
impl<'a> Visitor<'a> for BodyVisitor<'a> {
fn visit_except_handler(&mut self, handler: &'a ast::ExceptHandler) {
let ast::ExceptHandler::ExceptHandler(handler_inner) = handler;
self.currently_suspended_exceptions = handler_inner.type_.as_deref();
// Track exception variable bindings
if let Some(name) = handler_inner.name.as_ref() {
if let Some(exceptions) = self.currently_suspended_exceptions {
// Store the exception expression(s) for later resolution
self.exception_variables
.insert(name.id.as_str(), exceptions);
}
}
visitor::walk_except_handler(self, handler);
self.currently_suspended_exceptions = None;
// Clear exception variables when leaving the except handler
self.exception_variables.clear();
}
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::Raise(ast::StmtRaise { exc, .. }) => {
if let Some(exc) = exc.as_ref() {
// First try to resolve the exception directly
if let Some(qualified_name) =
self.semantic.resolve_qualified_name(map_callable(exc))
{
@@ -916,27 +885,28 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> {
qualified_name,
range: exc.range(),
});
} else if let ast::Expr::Name(name) = exc.as_ref() {
// If it's a variable name, check if it's bound to an exception in the
// current except clause
if let Some(exception_expr) = self.exception_variables.get(name.id.as_str())
{
if let ast::Expr::Tuple(tuple) = exception_expr {
for exception in tuple {
self.maybe_store_exception(exception, stmt.range());
}
} else {
self.maybe_store_exception(exception_expr, stmt.range());
}
}
}
} else if let Some(exceptions) = self.currently_suspended_exceptions {
let mut maybe_store_exception = |exception| {
let Some(qualified_name) = self.semantic.resolve_qualified_name(exception)
else {
return;
};
if is_exception_or_base_exception(&qualified_name) {
return;
}
self.raised_exceptions.push(ExceptionEntry {
qualified_name,
range: stmt.range(),
});
};
if let ast::Expr::Tuple(tuple) = exceptions {
for exception in tuple {
self.maybe_store_exception(exception, stmt.range());
maybe_store_exception(exception);
}
} else {
self.maybe_store_exception(exceptions, stmt.range());
maybe_store_exception(exceptions);
}
}
}

View File

@@ -61,66 +61,6 @@ DOC501 Raised exception `FasterThanLightError` missing from docstring
|
help: Add `FasterThanLightError` to the docstring
DOC501 Raised exception `ZeroDivisionError` missing from docstring
--> DOC501_google.py:70:5
|
68 | # DOC501
69 | def calculate_speed(distance: float, time: float) -> float:
70 | / """Calculate speed as distance divided by time.
71 | |
72 | | Args:
73 | | distance: Distance traveled.
74 | | time: Time spent traveling.
75 | |
76 | | Returns:
77 | | Speed as distance divided by time.
78 | | """
| |_______^
79 | try:
80 | return distance / time
|
help: Add `ZeroDivisionError` to the docstring
DOC501 Raised exception `ValueError` missing from docstring
--> DOC501_google.py:88:5
|
86 | # DOC501
87 | def calculate_speed(distance: float, time: float) -> float:
88 | / """Calculate speed as distance divided by time.
89 | |
90 | | Args:
91 | | distance: Distance traveled.
92 | | time: Time spent traveling.
93 | |
94 | | Returns:
95 | | Speed as distance divided by time.
96 | | """
| |_______^
97 | try:
98 | return distance / time
|
help: Add `ValueError` to the docstring
DOC501 Raised exception `ZeroDivisionError` missing from docstring
--> DOC501_google.py:88:5
|
86 | # DOC501
87 | def calculate_speed(distance: float, time: float) -> float:
88 | / """Calculate speed as distance divided by time.
89 | |
90 | | Args:
91 | | distance: Distance traveled.
92 | | time: Time spent traveling.
93 | |
94 | | Returns:
95 | | Speed as distance divided by time.
96 | | """
| |_______^
97 | try:
98 | return distance / time
|
help: Add `ZeroDivisionError` to the docstring
DOC501 Raised exception `AnotherError` missing from docstring
--> DOC501_google.py:106:5
|

View File

@@ -61,66 +61,6 @@ DOC501 Raised exception `FasterThanLightError` missing from docstring
|
help: Add `FasterThanLightError` to the docstring
DOC501 Raised exception `ZeroDivisionError` missing from docstring
--> DOC501_google.py:70:5
|
68 | # DOC501
69 | def calculate_speed(distance: float, time: float) -> float:
70 | / """Calculate speed as distance divided by time.
71 | |
72 | | Args:
73 | | distance: Distance traveled.
74 | | time: Time spent traveling.
75 | |
76 | | Returns:
77 | | Speed as distance divided by time.
78 | | """
| |_______^
79 | try:
80 | return distance / time
|
help: Add `ZeroDivisionError` to the docstring
DOC501 Raised exception `ValueError` missing from docstring
--> DOC501_google.py:88:5
|
86 | # DOC501
87 | def calculate_speed(distance: float, time: float) -> float:
88 | / """Calculate speed as distance divided by time.
89 | |
90 | | Args:
91 | | distance: Distance traveled.
92 | | time: Time spent traveling.
93 | |
94 | | Returns:
95 | | Speed as distance divided by time.
96 | | """
| |_______^
97 | try:
98 | return distance / time
|
help: Add `ValueError` to the docstring
DOC501 Raised exception `ZeroDivisionError` missing from docstring
--> DOC501_google.py:88:5
|
86 | # DOC501
87 | def calculate_speed(distance: float, time: float) -> float:
88 | / """Calculate speed as distance divided by time.
89 | |
90 | | Args:
91 | | distance: Distance traveled.
92 | | time: Time spent traveling.
93 | |
94 | | Returns:
95 | | Speed as distance divided by time.
96 | | """
| |_______^
97 | try:
98 | return distance / time
|
help: Add `ZeroDivisionError` to the docstring
DOC501 Raised exception `AnotherError` missing from docstring
--> DOC501_google.py:106:5
|

View File

@@ -166,7 +166,6 @@ mod tests {
#[test_case(Rule::UndefinedName, Path::new("F821_30.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_31.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_32.pyi"))]
#[test_case(Rule::UndefinedName, Path::new("F821_33.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
@@ -528,38 +527,6 @@ mod tests {
import a",
"f401_use_in_between_imports"
)]
#[test_case(
r"
if cond:
import a
import a.b
a.foo()
",
"f401_same_branch"
)]
#[test_case(
r"
try:
import a.b.c
except ImportError:
import argparse
import a
a.b = argparse.Namespace()
",
"f401_different_branch"
)]
#[test_case(
r"
import mlflow.pyfunc.loaders.chat_agent
import mlflow.pyfunc.loaders.chat_model
import mlflow.pyfunc.loaders.code_model
from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER
if IS_PYDANTIC_V2_OR_NEWER:
import mlflow.pyfunc.loaders.responses_agent
",
"f401_type_checking"
)]
fn f401_preview_refined_submodule_handling(contents: &str, snapshot: &str) {
let diagnostics = test_contents(
&SourceKind::Python(dedent(contents).to_string()),

View File

@@ -898,10 +898,6 @@ fn best_match<'a, 'b>(
#[inline]
fn has_simple_shadowed_bindings(scope: &Scope, id: BindingId, semantic: &SemanticModel) -> bool {
let Some(binding_node) = semantic.binding(id).source else {
return false;
};
scope.shadowed_bindings(id).enumerate().all(|(i, shadow)| {
let shadowed_binding = semantic.binding(shadow);
// Bail if one of the shadowed bindings is
@@ -916,34 +912,6 @@ fn has_simple_shadowed_bindings(scope: &Scope, id: BindingId, semantic: &Semanti
if i > 0 && shadowed_binding.is_used() {
return false;
}
// We want to allow a situation like this:
//
// ```python
// import a.b
// if TYPE_CHECKING:
// import a.b.c
// ```
// but bail in a situation like this:
//
// ```python
// try:
// import a.b
// except ImportError:
// import argparse
// import a
// a.b = argparse.Namespace()
// ```
//
// So we require that all the shadowed bindings dominate the
// last live binding for the import. That is: if the last live
// binding is executed it should imply that all the shadowed
// bindings were executed as well.
if shadowed_binding
.source
.is_none_or(|node_id| !semantic.dominates(node_id, binding_node))
{
return false;
}
matches!(
shadowed_binding.kind,
BindingKind::Import(_) | BindingKind::SubmoduleImport(_)

View File

@@ -1,12 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F821 Undefined name `__class__`
--> F821_33.py:15:13
|
14 | # Test: lambda outside class (should still fail)
15 | h = lambda: __class__
| ^^^^^^^^^
16 |
17 | # Test: lambda referencing module-level variable (should not be flagged as F821)
|

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -1,18 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F401 [*] `a.b` imported but unused
--> f401_preview_submodule.py:4:12
|
2 | if cond:
3 | import a
4 | import a.b
| ^^^
5 | a.foo()
|
help: Remove unused import: `a.b`
1 |
2 | if cond:
3 | import a
- import a.b
4 | a.foo()

View File

@@ -1,66 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F401 [*] `mlflow.pyfunc.loaders.chat_agent` imported but unused
--> f401_preview_submodule.py:2:8
|
2 | import mlflow.pyfunc.loaders.chat_agent
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 | import mlflow.pyfunc.loaders.chat_model
4 | import mlflow.pyfunc.loaders.code_model
|
help: Remove unused import: `mlflow.pyfunc.loaders.chat_agent`
1 |
- import mlflow.pyfunc.loaders.chat_agent
2 | import mlflow.pyfunc.loaders.chat_model
3 | import mlflow.pyfunc.loaders.code_model
4 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER
F401 [*] `mlflow.pyfunc.loaders.chat_model` imported but unused
--> f401_preview_submodule.py:3:8
|
2 | import mlflow.pyfunc.loaders.chat_agent
3 | import mlflow.pyfunc.loaders.chat_model
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 | import mlflow.pyfunc.loaders.code_model
5 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER
|
help: Remove unused import: `mlflow.pyfunc.loaders.chat_model`
1 |
2 | import mlflow.pyfunc.loaders.chat_agent
- import mlflow.pyfunc.loaders.chat_model
3 | import mlflow.pyfunc.loaders.code_model
4 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER
5 |
F401 [*] `mlflow.pyfunc.loaders.code_model` imported but unused
--> f401_preview_submodule.py:4:8
|
2 | import mlflow.pyfunc.loaders.chat_agent
3 | import mlflow.pyfunc.loaders.chat_model
4 | import mlflow.pyfunc.loaders.code_model
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER
|
help: Remove unused import: `mlflow.pyfunc.loaders.code_model`
1 |
2 | import mlflow.pyfunc.loaders.chat_agent
3 | import mlflow.pyfunc.loaders.chat_model
- import mlflow.pyfunc.loaders.code_model
4 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER
5 |
6 | if IS_PYDANTIC_V2_OR_NEWER:
F401 [*] `mlflow.pyfunc.loaders.responses_agent` imported but unused
--> f401_preview_submodule.py:8:12
|
7 | if IS_PYDANTIC_V2_OR_NEWER:
8 | import mlflow.pyfunc.loaders.responses_agent
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Remove unused import: `mlflow.pyfunc.loaders.responses_agent`
5 | from mlflow.utils.pydantic_utils import IS_PYDANTIC_V2_OR_NEWER
6 |
7 | if IS_PYDANTIC_V2_OR_NEWER:
- import mlflow.pyfunc.loaders.responses_agent
8 + pass

View File

@@ -52,7 +52,6 @@ mod tests {
#[test_case(Rule::ManualFromImport, Path::new("import_aliasing.py"))]
#[test_case(Rule::IfStmtMinMax, Path::new("if_stmt_min_max.py"))]
#[test_case(Rule::SingleStringSlots, Path::new("single_string_slots.py"))]
#[test_case(Rule::StopIterationReturn, Path::new("stop_iteration_return.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_0.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_1.py"))]
#[test_case(Rule::SysExitAlias, Path::new("sys_exit_alias_2.py"))]

View File

@@ -75,7 +75,6 @@ pub(crate) use shallow_copy_environ::*;
pub(crate) use single_string_slots::*;
pub(crate) use singledispatch_method::*;
pub(crate) use singledispatchmethod_function::*;
pub(crate) use stop_iteration_return::*;
pub(crate) use subprocess_popen_preexec_fn::*;
pub(crate) use subprocess_run_without_check::*;
pub(crate) use super_without_brackets::*;
@@ -186,7 +185,6 @@ mod shallow_copy_environ;
mod single_string_slots;
mod singledispatch_method;
mod singledispatchmethod_function;
mod stop_iteration_return;
mod subprocess_popen_preexec_fn;
mod subprocess_run_without_check;
mod super_without_brackets;

View File

@@ -1,114 +0,0 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::visitor::{Visitor, walk_expr, walk_stmt};
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for explicit `raise StopIteration` in generator functions.
///
/// ## Why is this bad?
/// Raising `StopIteration` in a generator function causes a `RuntimeError`
/// when the generator is iterated over.
///
/// Instead of `raise StopIteration`, use `return` in generator functions.
///
/// ## Example
/// ```python
/// def my_generator():
/// yield 1
/// yield 2
/// raise StopIteration # This causes RuntimeError at runtime
/// ```
///
/// Use instead:
/// ```python
/// def my_generator():
/// yield 1
/// yield 2
/// return # Use return instead
/// ```
///
/// ## References
/// - [PEP 479](https://peps.python.org/pep-0479/)
/// - [Python documentation](https://docs.python.org/3/library/exceptions.html#StopIteration)
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "0.14.3")]
pub(crate) struct StopIterationReturn;
impl Violation for StopIterationReturn {
#[derive_message_formats]
fn message(&self) -> String {
"Explicit `raise StopIteration` in generator".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Use `return` instead".to_string())
}
}
/// PLR1708
pub(crate) fn stop_iteration_return(checker: &Checker, raise_stmt: &ast::StmtRaise) {
// Fast-path: only continue if this is `raise StopIteration` (with or without args)
let Some(exc) = &raise_stmt.exc else {
return;
};
let is_stop_iteration = match exc.as_ref() {
ast::Expr::Call(ast::ExprCall { func, .. }) => {
checker.semantic().match_builtin_expr(func, "StopIteration")
}
expr => checker.semantic().match_builtin_expr(expr, "StopIteration"),
};
if !is_stop_iteration {
return;
}
// Now check the (more expensive) generator context
if !in_generator_context(checker) {
return;
}
checker.report_diagnostic(StopIterationReturn, raise_stmt.range());
}
/// Returns true if we're inside a function that contains any `yield`/`yield from`.
fn in_generator_context(checker: &Checker) -> bool {
for scope in checker.semantic().current_scopes() {
if let ruff_python_semantic::ScopeKind::Function(function_def) = scope.kind {
if contains_yield_statement(&function_def.body) {
return true;
}
}
}
false
}
/// Check if a statement list contains any yield statements
fn contains_yield_statement(body: &[ast::Stmt]) -> bool {
struct YieldFinder {
found: bool,
}
impl Visitor<'_> for YieldFinder {
fn visit_expr(&mut self, expr: &ast::Expr) {
if matches!(expr, ast::Expr::Yield(_) | ast::Expr::YieldFrom(_)) {
self.found = true;
} else {
walk_expr(self, expr);
}
}
}
let mut finder = YieldFinder { found: false };
for stmt in body {
walk_stmt(&mut finder, stmt);
if finder.found {
return true;
}
}
false
}

View File

@@ -1,109 +0,0 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:38:5
|
36 | yield 1
37 | yield 2
38 | raise StopIteration # Should trigger
| ^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:44:5
|
42 | yield 1
43 | yield 2
44 | raise StopIteration("finished") # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:50:5
|
48 | yield 1
49 | yield 2
50 | raise StopIteration(1 + 2) # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:56:5
|
54 | yield 1
55 | yield 2
56 | raise StopIteration("async") # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:62:9
|
60 | def inner_gen():
61 | yield 1
62 | raise StopIteration("inner") # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
63 |
64 | yield from inner_gen()
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:71:13
|
69 | def generator_method(self):
70 | yield 1
71 | raise StopIteration("method") # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72 |
73 | return MyClass
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:81:9
|
79 | yield 1
80 | yield 2
81 | raise StopIteration("complex") # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 | except ValueError:
83 | yield 3
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:91:9
|
89 | yield 1
90 | if condition:
91 | raise StopIteration("conditional") # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
92 | yield 2
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:98:5
|
96 | def generator_with_bare_stop_iteration():
97 | yield 1
98 | raise StopIteration # Should trigger (no arguments)
| ^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead
PLR1708 Explicit `raise StopIteration` in generator
--> stop_iteration_return.py:105:13
|
103 | yield i
104 | if i == 3:
105 | raise StopIteration("loop") # Should trigger
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `return` instead

View File

@@ -64,7 +64,6 @@ mod tests {
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_2.pyi"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_3.py"))]
#[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))]
#[test_case(Rule::RedundantOpenModes, Path::new("UP015_1.py"))]
#[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))]
@@ -112,7 +111,7 @@ mod tests {
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))]
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_0.py"))]
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))]
#[test_case(Rule::PrivateTypeParameter, Path::new("UP049_0.py"))]
#[test_case(Rule::PrivateTypeParameter, Path::new("UP049_1.py"))]
#[test_case(Rule::UselessClassMetaclassType, Path::new("UP050.py"))]
@@ -126,22 +125,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_2.py"))]
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_1.py"))]
fn rules_not_applied_default_typevar_backported(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = path.to_string_lossy().to_string();
let diagnostics = test_path(
Path::new("pyupgrade").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
unresolved_target_version: PythonVersion::PY312.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))]
#[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
@@ -157,25 +140,11 @@ mod tests {
Ok(())
}
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_3.py"))]
fn rules_py313(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("rules_py313__{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("pyupgrade").join(path).as_path(),
&settings::LinterSettings {
unresolved_target_version: PythonVersion::PY313.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))]
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))]
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_0.py"))]
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))]
fn type_var_default_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}__preview_diff", path.to_string_lossy());
assert_diagnostics_diff!(

View File

@@ -6,8 +6,8 @@ use std::fmt::Display;
use itertools::Itertools;
use ruff_python_ast::{
self as ast, Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, PythonVersion,
Stmt, StmtAssign, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
self as ast, Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign,
TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
name::Name,
visitor::{self, Visitor},
};
@@ -369,19 +369,15 @@ fn in_nested_context(checker: &Checker) -> bool {
}
/// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found.
/// Also returns `None` if any `TypeVar` has a default value and the target Python version
/// is below 3.13 or preview mode is not enabled. Note that `typing_extensions` backports
/// the default argument, but the rule should be skipped in that case.
/// Also returns `None` if any `TypeVar` has a default value and preview mode is not enabled.
fn check_type_vars<'a>(vars: Vec<TypeVar<'a>>, checker: &Checker) -> Option<Vec<TypeVar<'a>>> {
if vars.is_empty() {
return None;
}
// If any type variables have defaults, skip the rule unless
// running with preview mode enabled and targeting Python 3.13+.
// If any type variables have defaults and preview mode is not enabled, skip the rule
if vars.iter().any(|tv| tv.default.is_some())
&& (checker.target_version() < PythonVersion::PY313
|| !is_type_var_default_enabled(checker.settings()))
&& !is_type_var_default_enabled(checker.settings())
{
return None;
}

View File

@@ -1,36 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP037 [*] Remove quotes from type annotation
--> UP037_3.py:15:35
|
13 | @dataclass(frozen=True)
14 | class EmptyCell:
15 | _singleton: ClassVar[Optional["EmptyCell"]] = None
| ^^^^^^^^^^^
16 | # the behavior of _singleton above should match a non-ClassVar
17 | _doubleton: "EmptyCell"
|
help: Remove quotes
12 |
13 | @dataclass(frozen=True)
14 | class EmptyCell:
- _singleton: ClassVar[Optional["EmptyCell"]] = None
15 + _singleton: ClassVar[Optional[EmptyCell]] = None
16 | # the behavior of _singleton above should match a non-ClassVar
17 | _doubleton: "EmptyCell"
UP037 [*] Remove quotes from type annotation
--> UP037_3.py:17:17
|
15 | _singleton: ClassVar[Optional["EmptyCell"]] = None
16 | # the behavior of _singleton above should match a non-ClassVar
17 | _doubleton: "EmptyCell"
| ^^^^^^^^^^^
|
help: Remove quotes
14 | class EmptyCell:
15 | _singleton: ClassVar[Optional["EmptyCell"]] = None
16 | # the behavior of _singleton above should match a non-ClassVar
- _doubleton: "EmptyCell"
17 + _doubleton: EmptyCell

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP047 [*] Generic function `f` should use type parameters
--> UP047_0.py:12:5
--> UP047.py:12:5
|
12 | def f(t: T) -> T:
| ^^^^^^^
@@ -20,7 +20,7 @@ help: Use type parameters
note: This is an unsafe fix and may change runtime behavior
UP047 [*] Generic function `g` should use type parameters
--> UP047_0.py:16:5
--> UP047.py:16:5
|
16 | def g(ts: tuple[*Ts]) -> tuple[*Ts]:
| ^^^^^^^^^^^^^^^^^
@@ -38,7 +38,7 @@ help: Use type parameters
note: This is an unsafe fix and may change runtime behavior
UP047 [*] Generic function `h` should use type parameters
--> UP047_0.py:20:5
--> UP047.py:20:5
|
20 | def h(
| _____^
@@ -62,7 +62,7 @@ help: Use type parameters
note: This is an unsafe fix and may change runtime behavior
UP047 [*] Generic function `i` should use type parameters
--> UP047_0.py:29:5
--> UP047.py:29:5
|
29 | def i(s: S) -> S:
| ^^^^^^^
@@ -80,7 +80,7 @@ help: Use type parameters
note: This is an unsafe fix and may change runtime behavior
UP047 [*] Generic function `broken_fix` should use type parameters
--> UP047_0.py:39:5
--> UP047.py:39:5
|
37 | # TypeVars with the new-style generic syntax and will be rejected by type
38 | # checkers
@@ -100,7 +100,7 @@ help: Use type parameters
note: This is an unsafe fix and may change runtime behavior
UP047 [*] Generic function `any_str_param` should use type parameters
--> UP047_0.py:43:5
--> UP047.py:43:5
|
43 | def any_str_param(s: AnyStr) -> AnyStr:
| ^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -11,7 +11,7 @@ Added: 1
--- Added ---
UP047 [*] Generic function `default_var` should use type parameters
--> UP047_0.py:51:5
--> UP047.py:51:5
|
51 | def default_var(v: V) -> V:
| ^^^^^^^^^^^^^^^^^

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---

View File

@@ -1,6 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use itertools::Itertools;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_trivia::PythonWhitespace;
use ruff_text_size::Ranged;
@@ -92,20 +91,18 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
// using this regex:
// https://github.com/python/cpython/blob/ac556a2ad1213b8bb81372fe6fb762f5fcb076de/Lib/_pydecimal.py#L6060-L6077
// _after_ trimming whitespace from the string and removing all occurrences of "_".
let original_str = str_literal.to_str().trim_whitespace();
// Extract the unary sign, if any.
let (unary, original_str) = if let Some(trimmed) = original_str.strip_prefix('+') {
("+", trimmed)
} else if let Some(trimmed) = original_str.strip_prefix('-') {
("-", trimmed)
} else {
("", original_str)
};
let mut rest = Cow::from(original_str);
let has_digit_separators = memchr::memchr(b'_', rest.as_bytes()).is_some();
if has_digit_separators {
rest = Cow::from(rest.replace('_', ""));
let mut trimmed = Cow::from(str_literal.to_str().trim_whitespace());
if memchr::memchr(b'_', trimmed.as_bytes()).is_some() {
trimmed = Cow::from(trimmed.replace('_', ""));
}
// Extract the unary sign, if any.
let (unary, rest) = if let Some(trimmed) = trimmed.strip_prefix('+') {
("+", Cow::from(trimmed))
} else if let Some(trimmed) = trimmed.strip_prefix('-') {
("-", Cow::from(trimmed))
} else {
("", trimmed)
};
// Early return if we now have an empty string
// or a very long string:
@@ -121,13 +118,6 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
return;
}
// If the original string had digit separators, normalize them
let rest = if has_digit_separators {
Cow::from(normalize_digit_separators(original_str))
} else {
Cow::from(rest)
};
// If all the characters are zeros, then the value is zero.
let rest = match (unary, rest.is_empty()) {
// `Decimal("-0")` is not the same as `Decimal("0")`
@@ -136,11 +126,10 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
return;
}
(_, true) => "0",
_ => &rest,
_ => rest,
};
let replacement = format!("{unary}{rest}");
let mut diagnostic = checker.report_diagnostic(
VerboseDecimalConstructor {
replacement: replacement.clone(),
@@ -197,22 +186,6 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
}
}
/// Normalizes digit separators in a numeric string by:
/// - Stripping leading and trailing underscores
/// - Collapsing medial underscore sequences to single underscores
fn normalize_digit_separators(original_str: &str) -> String {
// Strip leading and trailing underscores
let trimmed = original_str
.trim_start_matches(['_', '0'])
.trim_end_matches('_');
// Collapse medial underscore sequences to single underscores
trimmed
.chars()
.dedup_by(|a, b| *a == '_' && a == b)
.collect()
}
// ```console
// $ python
// >>> import sys

View File

@@ -167,12 +167,12 @@ FURB157 [*] Verbose expression in `Decimal` constructor
| ^^^^^^^
24 | Decimal("__1____000")
|
help: Replace with `1_000`
help: Replace with `1000`
20 | # See https://github.com/astral-sh/ruff/issues/13807
21 |
22 | # Errors
- Decimal("1_000")
23 + Decimal(1_000)
23 + Decimal(1000)
24 | Decimal("__1____000")
25 |
26 | # Ok
@@ -187,12 +187,12 @@ FURB157 [*] Verbose expression in `Decimal` constructor
25 |
26 | # Ok
|
help: Replace with `1_000`
help: Replace with `1000`
21 |
22 | # Errors
23 | Decimal("1_000")
- Decimal("__1____000")
24 + Decimal(1_000)
24 + Decimal(1000)
25 |
26 | # Ok
27 | Decimal("2e-4")
@@ -494,7 +494,6 @@ help: Replace with `"nan"`
69 + Decimal("nan")
70 | Decimal(float(" -" "nan"))
71 | Decimal(float("-nAn"))
72 |
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:70:9
@@ -512,8 +511,6 @@ help: Replace with `"nan"`
- Decimal(float(" -" "nan"))
70 + Decimal("nan")
71 | Decimal(float("-nAn"))
72 |
73 | # Test cases for digit separators (safe fixes)
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:71:9
@@ -522,8 +519,6 @@ FURB157 [*] Verbose expression in `Decimal` constructor
70 | Decimal(float(" -" "nan"))
71 | Decimal(float("-nAn"))
| ^^^^^^^^^^^^^
72 |
73 | # Test cases for digit separators (safe fixes)
|
help: Replace with `"nan"`
68 | Decimal(float("\N{space}\N{hyPHen-MINus}nan"))
@@ -531,173 +526,3 @@ help: Replace with `"nan"`
70 | Decimal(float(" -" "nan"))
- Decimal(float("-nAn"))
71 + Decimal("nan")
72 |
73 | # Test cases for digit separators (safe fixes)
74 | # https://github.com/astral-sh/ruff/issues/20572
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:75:9
|
73 | # Test cases for digit separators (safe fixes)
74 | # https://github.com/astral-sh/ruff/issues/20572
75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
| ^^^^^^^^^^^^
76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
|
help: Replace with `15_000_000`
72 |
73 | # Test cases for digit separators (safe fixes)
74 | # https://github.com/astral-sh/ruff/issues/20572
- Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
75 + Decimal(15_000_000) # Safe fix: normalizes separators, becomes Decimal(15_000_000)
76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:76:9
|
74 | # https://github.com/astral-sh/ruff/issues/20572
75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
| ^^^^^^^^^^^
77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
|
help: Replace with `1_234_567`
73 | # Test cases for digit separators (safe fixes)
74 | # https://github.com/astral-sh/ruff/issues/20572
75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
- Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
76 + Decimal(1_234_567) # Safe fix: normalizes separators, becomes Decimal(1_234_567)
77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
79 |
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:77:9
|
75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
| ^^^^^^^^
78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
|
help: Replace with `-5_000`
74 | # https://github.com/astral-sh/ruff/issues/20572
75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
- Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
77 + Decimal(-5_000) # Safe fix: normalizes separators, becomes Decimal(-5_000)
78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
79 |
80 | # Test cases for non-thousands separators
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:78:9
|
76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
| ^^^^^^^^
79 |
80 | # Test cases for non-thousands separators
|
help: Replace with `+9_999`
75 | Decimal("15_000_000") # Safe fix: normalizes separators, becomes Decimal(15_000_000)
76 | Decimal("1_234_567") # Safe fix: normalizes separators, becomes Decimal(1_234_567)
77 | Decimal("-5_000") # Safe fix: normalizes separators, becomes Decimal(-5_000)
- Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
78 + Decimal(+9_999) # Safe fix: normalizes separators, becomes Decimal(+9_999)
79 |
80 | # Test cases for non-thousands separators
81 | Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:81:9
|
80 | # Test cases for non-thousands separators
81 | Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators
| ^^^^^^^^^^^^^
82 | Decimal("1234_5678") # Safe fix: preserves non-thousands separators
|
help: Replace with `12_34_56_78`
78 | Decimal("+9_999") # Safe fix: normalizes separators, becomes Decimal(+9_999)
79 |
80 | # Test cases for non-thousands separators
- Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators
81 + Decimal(12_34_56_78) # Safe fix: preserves non-thousands separators
82 | Decimal("1234_5678") # Safe fix: preserves non-thousands separators
83 |
84 | # Separators _and_ leading zeros
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:82:9
|
80 | # Test cases for non-thousands separators
81 | Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators
82 | Decimal("1234_5678") # Safe fix: preserves non-thousands separators
| ^^^^^^^^^^^
83 |
84 | # Separators _and_ leading zeros
|
help: Replace with `1234_5678`
79 |
80 | # Test cases for non-thousands separators
81 | Decimal("12_34_56_78") # Safe fix: preserves non-thousands separators
- Decimal("1234_5678") # Safe fix: preserves non-thousands separators
82 + Decimal(1234_5678) # Safe fix: preserves non-thousands separators
83 |
84 | # Separators _and_ leading zeros
85 | Decimal("0001_2345")
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:85:9
|
84 | # Separators _and_ leading zeros
85 | Decimal("0001_2345")
| ^^^^^^^^^^^
86 | Decimal("000_1_2345")
87 | Decimal("000_000")
|
help: Replace with `1_2345`
82 | Decimal("1234_5678") # Safe fix: preserves non-thousands separators
83 |
84 | # Separators _and_ leading zeros
- Decimal("0001_2345")
85 + Decimal(1_2345)
86 | Decimal("000_1_2345")
87 | Decimal("000_000")
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:86:9
|
84 | # Separators _and_ leading zeros
85 | Decimal("0001_2345")
86 | Decimal("000_1_2345")
| ^^^^^^^^^^^^
87 | Decimal("000_000")
|
help: Replace with `1_2345`
83 |
84 | # Separators _and_ leading zeros
85 | Decimal("0001_2345")
- Decimal("000_1_2345")
86 + Decimal(1_2345)
87 | Decimal("000_000")
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:87:9
|
85 | Decimal("0001_2345")
86 | Decimal("000_1_2345")
87 | Decimal("000_000")
| ^^^^^^^^^
|
help: Replace with `0`
84 | # Separators _and_ leading zeros
85 | Decimal("0001_2345")
86 | Decimal("000_1_2345")
- Decimal("000_000")
87 + Decimal(0)

View File

@@ -63,44 +63,19 @@ use crate::rules::flake8_logging_format::rules::{LoggingCallType, find_logging_c
#[violation_metadata(preview_since = "0.13.2")]
pub(crate) struct LoggingEagerConversion {
pub(crate) format_conversion: FormatConversion,
pub(crate) function_name: Option<&'static str>,
}
impl Violation for LoggingEagerConversion {
#[derive_message_formats]
fn message(&self) -> String {
let LoggingEagerConversion {
format_conversion,
function_name,
} = self;
match (format_conversion, function_name.as_deref()) {
(FormatConversion::Str, Some("oct")) => {
"Unnecessary `oct()` conversion when formatting with `%s`. \
Use `%#o` instead of `%s`"
.to_string()
}
(FormatConversion::Str, Some("hex")) => {
"Unnecessary `hex()` conversion when formatting with `%s`. \
Use `%#x` instead of `%s`"
.to_string()
}
(FormatConversion::Str, _) => {
"Unnecessary `str()` conversion when formatting with `%s`".to_string()
}
(FormatConversion::Repr, _) => {
"Unnecessary `repr()` conversion when formatting with `%s`. \
Use `%r` instead of `%s`"
.to_string()
}
(FormatConversion::Ascii, _) => {
"Unnecessary `ascii()` conversion when formatting with `%s`. \
Use `%a` instead of `%s`"
.to_string()
}
(FormatConversion::Bytes, _) => {
"Unnecessary `bytes()` conversion when formatting with `%b`".to_string()
}
}
let LoggingEagerConversion { format_conversion } = self;
let (format_str, call_arg) = match format_conversion {
FormatConversion::Str => ("%s", "str()"),
FormatConversion::Repr => ("%r", "repr()"),
FormatConversion::Ascii => ("%a", "ascii()"),
FormatConversion::Bytes => ("%b", "bytes()"),
};
format!("Unnecessary `{call_arg}` conversion when formatting with `{format_str}`")
}
}
@@ -143,71 +118,12 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
continue;
};
// Check for various eager conversion patterns
match format_conversion {
// %s with str() - remove str() call
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "str") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
format_conversion,
function_name: None,
},
arg.range(),
);
}
// %s with repr() - suggest using %r instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "repr") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
format_conversion: FormatConversion::Repr,
function_name: None,
},
arg.range(),
);
}
// %s with ascii() - suggest using %a instead
FormatConversion::Str
if checker
.semantic()
.match_builtin_expr(func.as_ref(), "ascii") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
format_conversion: FormatConversion::Ascii,
function_name: None,
},
arg.range(),
);
}
// %s with oct() - suggest using %#o instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "oct") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
format_conversion: FormatConversion::Str,
function_name: Some("oct"),
},
arg.range(),
);
}
// %s with hex() - suggest using %#x instead
FormatConversion::Str
if checker.semantic().match_builtin_expr(func.as_ref(), "hex") =>
{
checker.report_diagnostic(
LoggingEagerConversion {
format_conversion: FormatConversion::Str,
function_name: Some("hex"),
},
arg.range(),
);
}
_ => {}
// Check for use of %s with str()
if checker.semantic().match_builtin_expr(func.as_ref(), "str")
&& matches!(format_conversion, FormatConversion::Str)
{
checker
.report_diagnostic(LoggingEagerConversion { format_conversion }, arg.range());
}
}
}

View File

@@ -21,26 +21,6 @@ RUF065 Unnecessary `str()` conversion when formatting with `%s`
7 | # %s + repr()
|
RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s`
--> RUF065.py:8:26
|
7 | # %s + repr()
8 | logging.info("Hello %s", repr("World!"))
| ^^^^^^^^^^^^^^
9 | logging.log(logging.INFO, "Hello %s", repr("World!"))
|
RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s`
--> RUF065.py:9:39
|
7 | # %s + repr()
8 | logging.info("Hello %s", repr("World!"))
9 | logging.log(logging.INFO, "Hello %s", repr("World!"))
| ^^^^^^^^^^^^^^
10 |
11 | # %r + str()
|
RUF065 Unnecessary `str()` conversion when formatting with `%s`
--> RUF065.py:22:18
|
@@ -60,160 +40,3 @@ RUF065 Unnecessary `str()` conversion when formatting with `%s`
24 |
25 | # %s + repr()
|
RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s`
--> RUF065.py:26:18
|
25 | # %s + repr()
26 | info("Hello %s", repr("World!"))
| ^^^^^^^^^^^^^^
27 | log(logging.INFO, "Hello %s", repr("World!"))
|
RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s`
--> RUF065.py:27:31
|
25 | # %s + repr()
26 | info("Hello %s", repr("World!"))
27 | log(logging.INFO, "Hello %s", repr("World!"))
| ^^^^^^^^^^^^^^
28 |
29 | # %r + str()
|
RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s`
--> RUF065.py:44:32
|
42 | logging.warning("Value: %r", repr(42))
43 | logging.error("Error: %r", repr([1, 2, 3]))
44 | logging.info("Debug info: %s", repr("test\nstring"))
| ^^^^^^^^^^^^^^^^^^^^
45 | logging.warning("Value: %s", repr(42))
|
RUF065 Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s`
--> RUF065.py:45:30
|
43 | logging.error("Error: %r", repr([1, 2, 3]))
44 | logging.info("Debug info: %s", repr("test\nstring"))
45 | logging.warning("Value: %s", repr(42))
| ^^^^^^^^
46 |
47 | # %s + ascii()
|
RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s`
--> RUF065.py:48:27
|
47 | # %s + ascii()
48 | logging.info("ASCII: %s", ascii("Hello\nWorld"))
| ^^^^^^^^^^^^^^^^^^^^^
49 | logging.warning("ASCII: %s", ascii("test"))
|
RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s`
--> RUF065.py:49:30
|
47 | # %s + ascii()
48 | logging.info("ASCII: %s", ascii("Hello\nWorld"))
49 | logging.warning("ASCII: %s", ascii("test"))
| ^^^^^^^^^^^^^
50 |
51 | # %s + oct()
|
RUF065 Unnecessary `oct()` conversion when formatting with `%s`. Use `%#o` instead of `%s`
--> RUF065.py:52:27
|
51 | # %s + oct()
52 | logging.info("Octal: %s", oct(42))
| ^^^^^^^
53 | logging.warning("Octal: %s", oct(255))
|
RUF065 Unnecessary `oct()` conversion when formatting with `%s`. Use `%#o` instead of `%s`
--> RUF065.py:53:30
|
51 | # %s + oct()
52 | logging.info("Octal: %s", oct(42))
53 | logging.warning("Octal: %s", oct(255))
| ^^^^^^^^
54 |
55 | # %s + hex()
|
RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` instead of `%s`
--> RUF065.py:56:25
|
55 | # %s + hex()
56 | logging.info("Hex: %s", hex(42))
| ^^^^^^^
57 | logging.warning("Hex: %s", hex(255))
|
RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` instead of `%s`
--> RUF065.py:57:28
|
55 | # %s + hex()
56 | logging.info("Hex: %s", hex(42))
57 | logging.warning("Hex: %s", hex(255))
| ^^^^^^^^
|
RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s`
--> RUF065.py:63:19
|
61 | from logging import info, log
62 |
63 | info("ASCII: %s", ascii("Hello\nWorld"))
| ^^^^^^^^^^^^^^^^^^^^^
64 | log(logging.INFO, "ASCII: %s", ascii("test"))
|
RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s`
--> RUF065.py:64:32
|
63 | info("ASCII: %s", ascii("Hello\nWorld"))
64 | log(logging.INFO, "ASCII: %s", ascii("test"))
| ^^^^^^^^^^^^^
65 |
66 | info("Octal: %s", oct(42))
|
RUF065 Unnecessary `oct()` conversion when formatting with `%s`. Use `%#o` instead of `%s`
--> RUF065.py:66:19
|
64 | log(logging.INFO, "ASCII: %s", ascii("test"))
65 |
66 | info("Octal: %s", oct(42))
| ^^^^^^^
67 | log(logging.INFO, "Octal: %s", oct(255))
|
RUF065 Unnecessary `oct()` conversion when formatting with `%s`. Use `%#o` instead of `%s`
--> RUF065.py:67:32
|
66 | info("Octal: %s", oct(42))
67 | log(logging.INFO, "Octal: %s", oct(255))
| ^^^^^^^^
68 |
69 | info("Hex: %s", hex(42))
|
RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` instead of `%s`
--> RUF065.py:69:17
|
67 | log(logging.INFO, "Octal: %s", oct(255))
68 |
69 | info("Hex: %s", hex(42))
| ^^^^^^^
70 | log(logging.INFO, "Hex: %s", hex(255))
|
RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` instead of `%s`
--> RUF065.py:70:30
|
69 | info("Hex: %s", hex(42))
70 | log(logging.INFO, "Hex: %s", hex(255))
| ^^^^^^^^
|

View File

@@ -2,10 +2,8 @@
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
--> resources/test/fixtures/semantic_errors/async_comprehension_outside_async_function.py:1:27
--> <filename>:1:27
|
1 | async def f(): return [[x async for x in foo(n)] for n in range(3)]
| ^^^^^^^^^^^^^^^^^^^^^
2 |
3 | async def test(): return [[x async for x in elements(n)] async for n in range(3)]
|

View File

@@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: attribute name `x` repeated in class pattern
--> <filename>:3:21
|
2 | match x:
3 | case Point(x=1, x=2):
| ^
4 | pass
|

View File

@@ -2,10 +2,10 @@
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: mapping pattern checks duplicate key `'key'`
--> resources/test/fixtures/semantic_errors/duplicate_match_key.py:2:21
--> <filename>:3:21
|
1 | match x:
2 | case {'key': 1, 'key': 2}:
2 | match x:
3 | case {'key': 1, 'key': 2}:
| ^^^^^
3 | pass
4 | pass
|

View File

@@ -2,7 +2,7 @@
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: duplicate type parameter
--> resources/test/fixtures/semantic_errors/duplicate_type_parameter.py:1:12
--> <filename>:1:12
|
1 | class C[T, T]: pass
| ^

View File

@@ -0,0 +1,56 @@
---
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: name `a` is parameter and global
--> <filename>:3:12
|
2 | def f(a):
3 | global a
| ^
4 |
5 | def g(a):
|
invalid-syntax: name `a` is parameter and global
--> <filename>:7:16
|
5 | def g(a):
6 | if True:
7 | global a
| ^
8 |
9 | def h(a):
|
invalid-syntax: name `a` is parameter and global
--> <filename>:15:16
|
13 | def i(a):
14 | try:
15 | global a
| ^
16 | except Exception:
17 | pass
|
invalid-syntax: name `a` is parameter and global
--> <filename>:21:12
|
19 | def f(a):
20 | a = 1
21 | global a
| ^
22 |
23 | def f(a):
|
invalid-syntax: name `a` is parameter and global
--> <filename>:26:12
|
24 | a = 1
25 | a = 2
26 | global a
| ^
27 |
28 | def f(a):
|

View File

@@ -0,0 +1,9 @@
---
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: named expression cannot be used within a generic definition
--> <filename>:2:22
|
2 | def f[T](x: int) -> (y := 3): return x
| ^^^^^^
|

View File

@@ -0,0 +1,10 @@
---
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: yield expression cannot be used within a generic definition
--> <filename>:2:13
|
2 | class C[T]((yield from [object])):
| ^^^^^^^^^^^^^^^^^^^
3 | pass
|

View File

@@ -0,0 +1,9 @@
---
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: yield expression cannot be used within a type alias
--> <filename>:2:11
|
2 | type Y = (yield 1)
| ^^^^^^^
|

View File

@@ -0,0 +1,9 @@
---
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: yield expression cannot be used within a TypeVar bound
--> <filename>:2:12
|
2 | type X[T: (yield 1)] = int
| ^^^^^^^
|

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