Compare commits
43 Commits
ibraheem/g
...
alex/into_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a30934c33 | ||
|
|
13375d0e42 | ||
|
|
c0b04d4b7c | ||
|
|
1c7ea690a8 | ||
|
|
9bacd19c5a | ||
|
|
f0fe6d62fb | ||
|
|
10bda3df00 | ||
|
|
e55bc943e5 | ||
|
|
1b0ee4677e | ||
|
|
1ebedf6df5 | ||
|
|
980b4c55b2 | ||
|
|
5139f76d1f | ||
|
|
aca8ba76a4 | ||
|
|
7045898ffa | ||
|
|
d38a5292d2 | ||
|
|
83a00c0ac8 | ||
|
|
8b22fd1a5f | ||
|
|
765257bdce | ||
|
|
2d4e0edee4 | ||
|
|
9ce3fa3fe3 | ||
|
|
196a68e4c8 | ||
|
|
349061117c | ||
|
|
d0aebaa253 | ||
|
|
17850eee4b | ||
|
|
4d2ee41e24 | ||
|
|
7b959ef44b | ||
|
|
4c4ddc8c29 | ||
|
|
ae0343f848 | ||
|
|
29462ea1d4 | ||
|
|
7fee62b2de | ||
|
|
96b60c11d9 | ||
|
|
fffbe5a879 | ||
|
|
116611bd39 | ||
|
|
db0e921db1 | ||
|
|
3c7f56f582 | ||
|
|
8a73519b25 | ||
|
|
fa12fd0184 | ||
|
|
fdb8ea487c | ||
|
|
d846a0319a | ||
|
|
bca5d33385 | ||
|
|
c83c4d52a4 | ||
|
|
e692b7f1ee | ||
|
|
8e51db3ecd |
7
.github/workflows/ci.yaml
vendored
7
.github/workflows/ci.yaml
vendored
@@ -438,7 +438,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7
|
||||
uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9
|
||||
- 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,8 +531,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
|
||||
with:
|
||||
# TODO: figure out why `ruff-ecosystem` crashes on Python 3.14
|
||||
python-version: "3.13"
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
activate-environment: true
|
||||
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
@@ -699,7 +698,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@a66119fbb1c952daba62640c2609111fe0803621 # v1.15.7
|
||||
- uses: cargo-bins/cargo-binstall@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
|
||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -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@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
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@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
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@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
@@ -179,14 +179,14 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
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@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
|
||||
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -295,9 +295,9 @@ checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.0"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
@@ -433,9 +433,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.49"
|
||||
version = "4.5.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f"
|
||||
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -443,9 +443,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.49"
|
||||
version = "4.5.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730"
|
||||
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1007,7 +1007,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1224,9 +1224,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size-derive2"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3814abc7da8ab18d2fd820f5b540b5e39b6af0a32de1bdd7c47576693074843"
|
||||
checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"quote",
|
||||
@@ -1235,9 +1235,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size2"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfe2cec5b5ce8fb94dcdb16a1708baa4d0609cc3ce305ca5d3f6f2ffb59baed"
|
||||
checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
@@ -1523,9 +1523,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.23"
|
||||
version = "0.4.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
|
||||
checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403"
|
||||
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=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
|
||||
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=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=25b3ef146cfa2615f4ec82760bd0c22b454d0a12#25b3ef146cfa2615f4ec82760bd0c22b454d0a12"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=cdd0b85516a52c18b8a6d17a2279a96ed6c3e198#cdd0b85516a52c18b8a6d17a2279a96ed6c3e198"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -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 = "25b3ef146cfa2615f4ec82760bd0c22b454d0a12", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "cdd0b85516a52c18b8a6d17a2279a96ed6c3e198", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
||||
@@ -9,9 +9,7 @@ use itertools::{Itertools, iterate};
|
||||
use ruff_linter::linter::FixTable;
|
||||
use serde::Serialize;
|
||||
|
||||
use ruff_db::diagnostic::{
|
||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
|
||||
};
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, SecondaryCode};
|
||||
use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::message::{EmitterContext, render_diagnostics};
|
||||
@@ -390,21 +388,18 @@ impl Printer {
|
||||
|
||||
let context = EmitterContext::new(&diagnostics.notebook_indexes);
|
||||
let format = if preview {
|
||||
DiagnosticFormat::Full
|
||||
self.format
|
||||
} else {
|
||||
DiagnosticFormat::Concise
|
||||
OutputFormat::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()))
|
||||
.format(format)
|
||||
.with_fix_applicability(self.unsafe_fixes.required_applicability());
|
||||
write!(
|
||||
writer,
|
||||
"{}",
|
||||
DisplayDiagnostics::new(&context, &config, &diagnostics.inner)
|
||||
)?;
|
||||
.with_fix_applicability(self.unsafe_fixes.required_applicability())
|
||||
.show_fix_diff(preview);
|
||||
render_diagnostics(writer, format, config, &context, &diagnostics.inner)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
750,
|
||||
800,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -200,7 +200,12 @@ 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 {})
|
||||
WalkDirectoryBuilder::new(
|
||||
path,
|
||||
OsDirectoryWalker {
|
||||
cwd: self.current_directory().to_path_buf(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn glob(
|
||||
@@ -454,7 +459,9 @@ struct ListedDirectory {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OsDirectoryWalker;
|
||||
struct OsDirectoryWalker {
|
||||
cwd: SystemPathBuf,
|
||||
}
|
||||
|
||||
impl DirectoryWalker for OsDirectoryWalker {
|
||||
fn walk(
|
||||
@@ -473,6 +480,7 @@ 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);
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 (
|
||||
@@ -20,6 +21,7 @@ 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()
|
||||
@@ -56,6 +58,10 @@ 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()
|
||||
@@ -96,3 +102,15 @@ 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()
|
||||
|
||||
@@ -46,3 +46,9 @@ 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)
|
||||
|
||||
7
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/flake8_implicit_str_concat/ISC_syntax_error_2.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/21023
|
||||
'' '
|
||||
"" ""
|
||||
'' '' '
|
||||
"" "" "
|
||||
f"" f"
|
||||
f"" f"" f"
|
||||
@@ -14,3 +14,14 @@ 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
|
||||
|
||||
17
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py
vendored
Normal file
17
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
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"
|
||||
13
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py
vendored
Normal file
13
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_2.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
"""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
|
||||
12
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_1.py
vendored
Normal file
12
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_1.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"""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
|
||||
@@ -69,3 +69,19 @@ 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")
|
||||
|
||||
@@ -43,3 +43,29 @@ 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))
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
match x:
|
||||
case Point(x=1, x=2):
|
||||
pass
|
||||
3
crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_key.py
vendored
Normal file
3
crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_match_key.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
match x:
|
||||
case {'key': 1, 'key': 2}:
|
||||
pass
|
||||
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_type_parameter.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/duplicate_type_parameter.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
class C[T, T]: pass
|
||||
29
crates/ruff_linter/resources/test/fixtures/semantic_errors/global_parameter.py
vendored
Normal file
29
crates/ruff_linter/resources/test/fixtures/semantic_errors/global_parameter.py
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
8
crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_expression.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_expression.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
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
|
||||
8
crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_star_expression.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/semantic_errors/invalid_star_expression.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
def func():
|
||||
return *x
|
||||
|
||||
for *x in range(10):
|
||||
pass
|
||||
|
||||
def func():
|
||||
yield *x
|
||||
11
crates/ruff_linter/resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py
vendored
Normal file
11
crates/ruff_linter/resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
match value:
|
||||
case _:
|
||||
pass
|
||||
case 1:
|
||||
pass
|
||||
|
||||
match value:
|
||||
case irrefutable:
|
||||
pass
|
||||
case 1:
|
||||
pass
|
||||
5
crates/ruff_linter/resources/test/fixtures/semantic_errors/multiple_case_assignment.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/semantic_errors/multiple_case_assignment.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
match x:
|
||||
case [a, a]:
|
||||
pass
|
||||
case _:
|
||||
pass
|
||||
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/rebound_comprehension.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/rebound_comprehension.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[x:= 2 for x in range(2)]
|
||||
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/single_starred_assignment.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/semantic_errors/single_starred_assignment.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*a = [1, 2, 3, 4]
|
||||
7
crates/ruff_linter/resources/test/fixtures/semantic_errors/write_to_debug.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/semantic_errors/write_to_debug.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
__debug__ = False
|
||||
|
||||
def process(__debug__):
|
||||
pass
|
||||
|
||||
class Generic[__debug__]:
|
||||
pass
|
||||
@@ -1400,6 +1400,14 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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, serde::Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct LineLength(
|
||||
#[cfg_attr(feature = "schemars", schemars(range(min = 1, max = 320)))] NonZeroU16,
|
||||
@@ -46,6 +46,21 @@ 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());
|
||||
|
||||
@@ -919,17 +919,6 @@ 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(
|
||||
@@ -972,245 +961,38 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
"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"
|
||||
Path::new("async_comprehension_outside_async_function.py"),
|
||||
PythonVersion::PY311
|
||||
)]
|
||||
#[test_case(
|
||||
"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"
|
||||
Path::new("async_comprehension_outside_async_function.py"),
|
||||
PythonVersion::PY310
|
||||
)]
|
||||
#[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
|
||||
#[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);
|
||||
|
||||
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,
|
||||
let diagnostics = test_contents_syntax_errors(
|
||||
&source_kind,
|
||||
&path,
|
||||
&LinterSettings {
|
||||
rules: settings::rule_table::RuleTable::empty(),
|
||||
unresolved_target_version: python_version.into(),
|
||||
@@ -1218,7 +1000,11 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
insta::with_settings!({filters => vec![(r"\\", "/")]}, {
|
||||
assert_diagnostics!(format!("{snapshot}"), diagnostics);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(PythonVersion::PY310)]
|
||||
|
||||
@@ -492,6 +492,12 @@ 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")
|
||||
|
||||
@@ -288,10 +288,12 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
|
||||
},
|
||||
|
||||
// airflow.model..DAG
|
||||
["airflow", "models", .., "DAG"] => Replacement::SourceModuleMoved {
|
||||
module: "airflow.sdk",
|
||||
name: "DAG".to_string(),
|
||||
},
|
||||
["airflow", "models", "dag", "DAG"] | ["airflow", "models", "DAG"] | ["airflow", "DAG"] => {
|
||||
Replacement::SourceModuleMoved {
|
||||
module: "airflow.sdk",
|
||||
name: "DAG".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
// airflow.sensors.base
|
||||
[
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,25 @@
|
||||
---
|
||||
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
|
||||
|
|
||||
|
||||
@@ -375,7 +375,7 @@ impl Violation for SuspiciousEvalUsage {
|
||||
///
|
||||
///
|
||||
/// def render_username(username):
|
||||
/// return django.utils.html.format_html("<i>{}</i>", username) # username is escaped.
|
||||
/// return format_html("<i>{}</i>", username) # username is escaped.
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
|
||||
@@ -61,9 +61,14 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) {
|
||||
}
|
||||
|
||||
for statement in body {
|
||||
let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else {
|
||||
continue;
|
||||
let value = match statement {
|
||||
Stmt::Assign(ast::StmtAssign { value, .. }) => value,
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
value: Some(value), ..
|
||||
}) => value,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if let Some(field_name) = is_nullable_field(value, checker.semantic()) {
|
||||
checker.report_diagnostic(
|
||||
DjangoNullableModelStringField {
|
||||
|
||||
@@ -186,3 +186,32 @@ 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)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
@@ -23,6 +23,14 @@ 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());
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
||||
use ruff_python_ast::StringFlags;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::{TokenKind, Tokens};
|
||||
use ruff_python_parser::{Token, TokenKind, Tokens};
|
||||
use ruff_source_file::LineRanges;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
@@ -169,7 +168,8 @@ pub(crate) fn implicit(
|
||||
SingleLineImplicitStringConcatenation,
|
||||
TextRange::new(a_range.start(), b_range.end()),
|
||||
) {
|
||||
if let Some(fix) = concatenate_strings(a_range, b_range, locator) {
|
||||
if let Some(fix) = concatenate_strings(a_token, b_token, a_range, b_range, locator)
|
||||
{
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
@@ -177,38 +177,55 @@ pub(crate) fn implicit(
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
/// 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() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let a_trailing_quote = trailing_quote(a_text)?;
|
||||
let b_trailing_quote = trailing_quote(b_text)?;
|
||||
let a_string_flags = a_token.string_flags()?;
|
||||
let b_string_flags = b_token.string_flags()?;
|
||||
|
||||
// Require, for now, that the trailing quotes are the same.
|
||||
if a_trailing_quote != b_trailing_quote {
|
||||
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()
|
||||
{
|
||||
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[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()];
|
||||
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)];
|
||||
|
||||
if a_leading_quote.find(['r', 'R']).is_none()
|
||||
&& matches!(b_body.bytes().next(), Some(b'0'..=b'7'))
|
||||
{
|
||||
if !a_string_flags.is_raw_string() && matches!(b_body.bytes().next(), Some(b'0'..=b'7')) {
|
||||
normalize_ending_octal(&mut a_body);
|
||||
}
|
||||
|
||||
let concatenation = format!("{a_leading_quote}{a_body}{b_body}{a_trailing_quote}");
|
||||
let range = TextRange::new(a_range.start(), b_range.end());
|
||||
let concatenation = format!("{a_prefix}{quotes}{a_body}{b_body}{quotes}");
|
||||
let range = TextRange::new(a_string_range.start(), b_string_range.end());
|
||||
|
||||
Some(Fix::safe_edit(Edit::range_replacement(
|
||||
concatenation,
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
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"
|
||||
| ^
|
||||
|
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
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"
|
||||
| ^
|
||||
|
|
||||
@@ -98,6 +98,26 @@ 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"))]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
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
|
||||
@@ -528,6 +528,38 @@ 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()),
|
||||
|
||||
@@ -898,6 +898,10 @@ 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
|
||||
@@ -912,6 +916,34 @@ 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(_)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
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()
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
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
|
||||
@@ -64,6 +64,7 @@ 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"))]
|
||||
@@ -111,7 +112,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.py"))]
|
||||
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_0.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"))]
|
||||
@@ -125,6 +126,22 @@ 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<()> {
|
||||
@@ -140,11 +157,25 @@ 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.py"))]
|
||||
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047_0.py"))]
|
||||
fn type_var_default_preview(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}__preview_diff", path.to_string_lossy());
|
||||
assert_diagnostics_diff!(
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::fmt::Display;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, Stmt, StmtAssign,
|
||||
TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
|
||||
self as ast, Arguments, Expr, ExprCall, ExprName, ExprSubscript, Identifier, PythonVersion,
|
||||
Stmt, StmtAssign, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
|
||||
name::Name,
|
||||
visitor::{self, Visitor},
|
||||
};
|
||||
@@ -369,15 +369,19 @@ 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 preview mode is not enabled.
|
||||
/// 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.
|
||||
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 and preview mode is not enabled, skip the rule
|
||||
// If any type variables have defaults, skip the rule unless
|
||||
// running with preview mode enabled and targeting Python 3.13+.
|
||||
if vars.iter().any(|tv| tv.default.is_some())
|
||||
&& !is_type_var_default_enabled(checker.settings())
|
||||
&& (checker.target_version() < PythonVersion::PY313
|
||||
|| !is_type_var_default_enabled(checker.settings()))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP047 [*] Generic function `f` should use type parameters
|
||||
--> UP047.py:12:5
|
||||
--> UP047_0.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.py:16:5
|
||||
--> UP047_0.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.py:20:5
|
||||
--> UP047_0.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.py:29:5
|
||||
--> UP047_0.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.py:39:5
|
||||
--> UP047_0.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.py:43:5
|
||||
--> UP047_0.py:43:5
|
||||
|
|
||||
43 | def any_str_param(s: AnyStr) -> AnyStr:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -11,7 +11,7 @@ Added: 1
|
||||
|
||||
--- Added ---
|
||||
UP047 [*] Generic function `default_var` should use type parameters
|
||||
--> UP047.py:51:5
|
||||
--> UP047_0.py:51:5
|
||||
|
|
||||
51 | def default_var(v: V) -> V:
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
@@ -91,18 +92,20 @@ 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 mut trimmed = Cow::from(str_literal.to_str().trim_whitespace());
|
||||
if memchr::memchr(b'_', trimmed.as_bytes()).is_some() {
|
||||
trimmed = Cow::from(trimmed.replace('_', ""));
|
||||
}
|
||||
let original_str = str_literal.to_str().trim_whitespace();
|
||||
// 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))
|
||||
let (unary, original_str) = if let Some(trimmed) = original_str.strip_prefix('+') {
|
||||
("+", trimmed)
|
||||
} else if let Some(trimmed) = original_str.strip_prefix('-') {
|
||||
("-", trimmed)
|
||||
} else {
|
||||
("", trimmed)
|
||||
("", 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('_', ""));
|
||||
}
|
||||
|
||||
// Early return if we now have an empty string
|
||||
// or a very long string:
|
||||
@@ -118,6 +121,13 @@ 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")`
|
||||
@@ -126,10 +136,11 @@ 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(),
|
||||
@@ -186,6 +197,22 @@ 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
|
||||
|
||||
@@ -167,12 +167,12 @@ FURB157 [*] Verbose expression in `Decimal` constructor
|
||||
| ^^^^^^^
|
||||
24 | Decimal("__1____000")
|
||||
|
|
||||
help: Replace with `1000`
|
||||
help: Replace with `1_000`
|
||||
20 | # See https://github.com/astral-sh/ruff/issues/13807
|
||||
21 |
|
||||
22 | # Errors
|
||||
- Decimal("1_000")
|
||||
23 + Decimal(1000)
|
||||
23 + Decimal(1_000)
|
||||
24 | Decimal("__1____000")
|
||||
25 |
|
||||
26 | # Ok
|
||||
@@ -187,12 +187,12 @@ FURB157 [*] Verbose expression in `Decimal` constructor
|
||||
25 |
|
||||
26 | # Ok
|
||||
|
|
||||
help: Replace with `1000`
|
||||
help: Replace with `1_000`
|
||||
21 |
|
||||
22 | # Errors
|
||||
23 | Decimal("1_000")
|
||||
- Decimal("__1____000")
|
||||
24 + Decimal(1000)
|
||||
24 + Decimal(1_000)
|
||||
25 |
|
||||
26 | # Ok
|
||||
27 | Decimal("2e-4")
|
||||
@@ -494,6 +494,7 @@ 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
|
||||
@@ -511,6 +512,8 @@ 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
|
||||
@@ -519,6 +522,8 @@ 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"))
|
||||
@@ -526,3 +531,173 @@ 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)
|
||||
|
||||
@@ -63,19 +63,44 @@ 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 } = 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}`")
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,12 +143,71 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall)
|
||||
continue;
|
||||
};
|
||||
|
||||
// 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());
|
||||
// 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(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,26 @@ 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
|
||||
|
|
||||
@@ -40,3 +60,160 @@ 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))
|
||||
| ^^^^^^^^
|
||||
|
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
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
|
||||
|
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
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):
|
||||
|
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
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
|
||||
| ^^^^^^
|
||||
|
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
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
|
||||
|
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
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)
|
||||
| ^^^^^^^
|
||||
|
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
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
|
||||
| ^^^^^^^
|
||||
|
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: Starred expression cannot be used here
|
||||
--> <filename>:3:12
|
||||
|
|
||||
2 | def func():
|
||||
3 | return *x
|
||||
| ^^
|
||||
|
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: Starred expression cannot be used here
|
||||
--> <filename>:2:5
|
||||
|
|
||||
2 | for *x in range(10):
|
||||
| ^^
|
||||
3 | pass
|
||||
|
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: Starred expression cannot be used here
|
||||
--> <filename>:3:11
|
||||
|
|
||||
2 | def func():
|
||||
3 | yield *x
|
||||
| ^^
|
||||
|
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: name capture `irrefutable` makes remaining patterns unreachable
|
||||
--> <filename>:3:10
|
||||
|
|
||||
2 | match value:
|
||||
3 | case irrefutable:
|
||||
| ^^^^^^^^^^^
|
||||
4 | pass
|
||||
5 | case 1:
|
||||
|
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: wildcard makes remaining patterns unreachable
|
||||
--> <filename>:3:10
|
||||
|
|
||||
2 | match value:
|
||||
3 | case _:
|
||||
| ^
|
||||
4 | pass
|
||||
5 | case 1:
|
||||
|
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: multiple assignments to name `a` in pattern
|
||||
--> <filename>:3:14
|
||||
|
|
||||
2 | match x:
|
||||
3 | case [a, a]:
|
||||
| ^
|
||||
4 | pass
|
||||
5 | case _:
|
||||
|
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> <filename>:2:1
|
||||
|
|
||||
2 | __debug__ = False
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> <filename>:2:15
|
||||
|
|
||||
2 | class Generic[__debug__]:
|
||||
| ^^^^^^^^^
|
||||
3 | pass
|
||||
|
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> <filename>:2:13
|
||||
|
|
||||
2 | def process(__debug__):
|
||||
| ^^^^^^^^^
|
||||
3 | pass
|
||||
|
|
||||
@@ -2,8 +2,10 @@
|
||||
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)
|
||||
--> <filename>:1:27
|
||||
--> resources/test/fixtures/semantic_errors/async_comprehension_outside_async_function.py: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)]
|
||||
|
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: attribute name `x` repeated in class pattern
|
||||
--> resources/test/fixtures/semantic_errors/duplicate_match_class_attribute.py:2:21
|
||||
|
|
||||
1 | match x:
|
||||
2 | case Point(x=1, x=2):
|
||||
| ^
|
||||
3 | pass
|
||||
|
|
||||
@@ -2,10 +2,10 @@
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: mapping pattern checks duplicate key `'key'`
|
||||
--> <filename>:3:21
|
||||
--> resources/test/fixtures/semantic_errors/duplicate_match_key.py:2:21
|
||||
|
|
||||
2 | match x:
|
||||
3 | case {'key': 1, 'key': 2}:
|
||||
1 | match x:
|
||||
2 | case {'key': 1, 'key': 2}:
|
||||
| ^^^^^
|
||||
4 | pass
|
||||
3 | pass
|
||||
|
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: duplicate type parameter
|
||||
--> <filename>:1:12
|
||||
--> resources/test/fixtures/semantic_errors/duplicate_type_parameter.py:1:12
|
||||
|
|
||||
1 | class C[T, T]: pass
|
||||
| ^
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: name `a` is parameter and global
|
||||
--> resources/test/fixtures/semantic_errors/global_parameter.py:2:12
|
||||
|
|
||||
1 | def f(a):
|
||||
2 | global a
|
||||
| ^
|
||||
3 |
|
||||
4 | def g(a):
|
||||
|
|
||||
|
||||
invalid-syntax: name `a` is parameter and global
|
||||
--> resources/test/fixtures/semantic_errors/global_parameter.py:6:16
|
||||
|
|
||||
4 | def g(a):
|
||||
5 | if True:
|
||||
6 | global a
|
||||
| ^
|
||||
7 |
|
||||
8 | def h(a):
|
||||
|
|
||||
|
||||
invalid-syntax: name `a` is parameter and global
|
||||
--> resources/test/fixtures/semantic_errors/global_parameter.py:14:16
|
||||
|
|
||||
12 | def i(a):
|
||||
13 | try:
|
||||
14 | global a
|
||||
| ^
|
||||
15 | except Exception:
|
||||
16 | pass
|
||||
|
|
||||
|
||||
invalid-syntax: name `a` is parameter and global
|
||||
--> resources/test/fixtures/semantic_errors/global_parameter.py:20:12
|
||||
|
|
||||
18 | def f(a):
|
||||
19 | a = 1
|
||||
20 | global a
|
||||
| ^
|
||||
21 |
|
||||
22 | def f(a):
|
||||
|
|
||||
|
||||
invalid-syntax: name `a` is parameter and global
|
||||
--> resources/test/fixtures/semantic_errors/global_parameter.py:25:12
|
||||
|
|
||||
23 | a = 1
|
||||
24 | a = 2
|
||||
25 | global a
|
||||
| ^
|
||||
26 |
|
||||
27 | def f(a):
|
||||
|
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: yield expression cannot be used within a TypeVar bound
|
||||
--> resources/test/fixtures/semantic_errors/invalid_expression.py:1:12
|
||||
|
|
||||
1 | type X[T: (yield 1)] = int
|
||||
| ^^^^^^^
|
||||
2 |
|
||||
3 | type Y = (yield 1)
|
||||
|
|
||||
|
||||
invalid-syntax: yield expression cannot be used within a type alias
|
||||
--> resources/test/fixtures/semantic_errors/invalid_expression.py:3:11
|
||||
|
|
||||
1 | type X[T: (yield 1)] = int
|
||||
2 |
|
||||
3 | type Y = (yield 1)
|
||||
| ^^^^^^^
|
||||
4 |
|
||||
5 | def f[T](x: int) -> (y := 3): return x
|
||||
|
|
||||
|
||||
invalid-syntax: named expression cannot be used within a generic definition
|
||||
--> resources/test/fixtures/semantic_errors/invalid_expression.py:5:22
|
||||
|
|
||||
3 | type Y = (yield 1)
|
||||
4 |
|
||||
5 | def f[T](x: int) -> (y := 3): return x
|
||||
| ^^^^^^
|
||||
6 |
|
||||
7 | class C[T]((yield from [object])):
|
||||
|
|
||||
|
||||
invalid-syntax: yield expression cannot be used within a generic definition
|
||||
--> resources/test/fixtures/semantic_errors/invalid_expression.py:7:13
|
||||
|
|
||||
5 | def f[T](x: int) -> (y := 3): return x
|
||||
6 |
|
||||
7 | class C[T]((yield from [object])):
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
8 | pass
|
||||
|
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: Starred expression cannot be used here
|
||||
--> resources/test/fixtures/semantic_errors/invalid_star_expression.py:2:12
|
||||
|
|
||||
1 | def func():
|
||||
2 | return *x
|
||||
| ^^
|
||||
3 |
|
||||
4 | for *x in range(10):
|
||||
|
|
||||
|
||||
invalid-syntax: Starred expression cannot be used here
|
||||
--> resources/test/fixtures/semantic_errors/invalid_star_expression.py:4:5
|
||||
|
|
||||
2 | return *x
|
||||
3 |
|
||||
4 | for *x in range(10):
|
||||
| ^^
|
||||
5 | pass
|
||||
|
|
||||
|
||||
invalid-syntax: Starred expression cannot be used here
|
||||
--> resources/test/fixtures/semantic_errors/invalid_star_expression.py:8:11
|
||||
|
|
||||
7 | def func():
|
||||
8 | yield *x
|
||||
| ^^
|
||||
|
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: wildcard makes remaining patterns unreachable
|
||||
--> resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py:2:10
|
||||
|
|
||||
1 | match value:
|
||||
2 | case _:
|
||||
| ^
|
||||
3 | pass
|
||||
4 | case 1:
|
||||
|
|
||||
|
||||
invalid-syntax: name capture `irrefutable` makes remaining patterns unreachable
|
||||
--> resources/test/fixtures/semantic_errors/irrefutable_case_pattern.py:8:10
|
||||
|
|
||||
7 | match value:
|
||||
8 | case irrefutable:
|
||||
| ^^^^^^^^^^^
|
||||
9 | pass
|
||||
10 | case 1:
|
||||
|
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: multiple assignments to name `a` in pattern
|
||||
--> resources/test/fixtures/semantic_errors/multiple_case_assignment.py:2:14
|
||||
|
|
||||
1 | match x:
|
||||
2 | case [a, a]:
|
||||
| ^
|
||||
3 | pass
|
||||
4 | case _:
|
||||
|
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: assignment expression cannot rebind comprehension variable
|
||||
--> <filename>:1:2
|
||||
--> resources/test/fixtures/semantic_errors/rebound_comprehension.py:1:2
|
||||
|
|
||||
1 | [x:= 2 for x in range(2)]
|
||||
| ^
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: starred assignment target must be in a list or tuple
|
||||
--> <filename>:1:1
|
||||
--> resources/test/fixtures/semantic_errors/single_starred_assignment.py:1:1
|
||||
|
|
||||
1 | *a = [1, 2, 3, 4]
|
||||
| ^^
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> resources/test/fixtures/semantic_errors/write_to_debug.py:1:1
|
||||
|
|
||||
1 | __debug__ = False
|
||||
| ^^^^^^^^^
|
||||
2 |
|
||||
3 | def process(__debug__):
|
||||
|
|
||||
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> resources/test/fixtures/semantic_errors/write_to_debug.py:3:13
|
||||
|
|
||||
1 | __debug__ = False
|
||||
2 |
|
||||
3 | def process(__debug__):
|
||||
| ^^^^^^^^^
|
||||
4 | pass
|
||||
|
|
||||
|
||||
invalid-syntax: Cannot use type parameter lists on Python 3.10 (syntax was added in Python 3.12)
|
||||
--> resources/test/fixtures/semantic_errors/write_to_debug.py:6:14
|
||||
|
|
||||
4 | pass
|
||||
5 |
|
||||
6 | class Generic[__debug__]:
|
||||
| ^^^^^^^^^^^
|
||||
7 | pass
|
||||
|
|
||||
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> resources/test/fixtures/semantic_errors/write_to_debug.py:6:15
|
||||
|
|
||||
4 | pass
|
||||
5 |
|
||||
6 | class Generic[__debug__]:
|
||||
| ^^^^^^^^^
|
||||
7 | pass
|
||||
|
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> resources/test/fixtures/semantic_errors/write_to_debug.py:1:1
|
||||
|
|
||||
1 | __debug__ = False
|
||||
| ^^^^^^^^^
|
||||
2 |
|
||||
3 | def process(__debug__):
|
||||
|
|
||||
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> resources/test/fixtures/semantic_errors/write_to_debug.py:3:13
|
||||
|
|
||||
1 | __debug__ = False
|
||||
2 |
|
||||
3 | def process(__debug__):
|
||||
| ^^^^^^^^^
|
||||
4 | pass
|
||||
|
|
||||
|
||||
invalid-syntax: cannot assign to `__debug__`
|
||||
--> resources/test/fixtures/semantic_errors/write_to_debug.py:6:15
|
||||
|
|
||||
4 | pass
|
||||
5 |
|
||||
6 | class Generic[__debug__]:
|
||||
| ^^^^^^^^^
|
||||
7 | pass
|
||||
|
|
||||
@@ -0,0 +1,55 @@
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
@@ -42,3 +42,11 @@ def test4(<RANGE_START> a):
|
||||
|
||||
<RANGE_START>if b + c :<RANGE_END> # trailing clause header comment
|
||||
print("Not formatted" )
|
||||
|
||||
def test5():
|
||||
x = 1
|
||||
<RANGE_START>try:
|
||||
a;
|
||||
finally:
|
||||
b
|
||||
<RANGE_END>
|
||||
|
||||
@@ -216,7 +216,11 @@ impl ClauseHeader<'_> {
|
||||
.decorator_list
|
||||
.last()
|
||||
.map_or_else(|| header.start(), Ranged::end);
|
||||
find_keyword(start_position, SimpleTokenKind::Class, source)
|
||||
find_keyword(
|
||||
StartPosition::ClauseStart(start_position),
|
||||
SimpleTokenKind::Class,
|
||||
source,
|
||||
)
|
||||
}
|
||||
ClauseHeader::Function(header) => {
|
||||
let start_position = header
|
||||
@@ -228,21 +232,39 @@ impl ClauseHeader<'_> {
|
||||
} else {
|
||||
SimpleTokenKind::Def
|
||||
};
|
||||
find_keyword(start_position, keyword, source)
|
||||
find_keyword(StartPosition::ClauseStart(start_position), keyword, source)
|
||||
}
|
||||
ClauseHeader::If(header) => find_keyword(header.start(), SimpleTokenKind::If, source),
|
||||
ClauseHeader::If(header) => find_keyword(
|
||||
StartPosition::clause_start(header),
|
||||
SimpleTokenKind::If,
|
||||
source,
|
||||
),
|
||||
ClauseHeader::ElifElse(ElifElseClause {
|
||||
test: None, range, ..
|
||||
}) => find_keyword(range.start(), SimpleTokenKind::Else, source),
|
||||
}) => find_keyword(
|
||||
StartPosition::clause_start(range),
|
||||
SimpleTokenKind::Else,
|
||||
source,
|
||||
),
|
||||
ClauseHeader::ElifElse(ElifElseClause {
|
||||
test: Some(_),
|
||||
range,
|
||||
..
|
||||
}) => find_keyword(range.start(), SimpleTokenKind::Elif, source),
|
||||
ClauseHeader::Try(header) => find_keyword(header.start(), SimpleTokenKind::Try, source),
|
||||
ClauseHeader::ExceptHandler(header) => {
|
||||
find_keyword(header.start(), SimpleTokenKind::Except, source)
|
||||
}
|
||||
}) => find_keyword(
|
||||
StartPosition::clause_start(range),
|
||||
SimpleTokenKind::Elif,
|
||||
source,
|
||||
),
|
||||
ClauseHeader::Try(header) => find_keyword(
|
||||
StartPosition::clause_start(header),
|
||||
SimpleTokenKind::Try,
|
||||
source,
|
||||
),
|
||||
ClauseHeader::ExceptHandler(header) => find_keyword(
|
||||
StartPosition::clause_start(header),
|
||||
SimpleTokenKind::Except,
|
||||
source,
|
||||
),
|
||||
ClauseHeader::TryFinally(header) => {
|
||||
let last_statement = header
|
||||
.orelse
|
||||
@@ -252,25 +274,35 @@ impl ClauseHeader<'_> {
|
||||
.or_else(|| header.body.last().map(AnyNodeRef::from))
|
||||
.unwrap();
|
||||
|
||||
find_keyword(last_statement.end(), SimpleTokenKind::Finally, source)
|
||||
}
|
||||
ClauseHeader::Match(header) => {
|
||||
find_keyword(header.start(), SimpleTokenKind::Match, source)
|
||||
}
|
||||
ClauseHeader::MatchCase(header) => {
|
||||
find_keyword(header.start(), SimpleTokenKind::Case, source)
|
||||
find_keyword(
|
||||
StartPosition::LastStatement(last_statement.end()),
|
||||
SimpleTokenKind::Finally,
|
||||
source,
|
||||
)
|
||||
}
|
||||
ClauseHeader::Match(header) => find_keyword(
|
||||
StartPosition::clause_start(header),
|
||||
SimpleTokenKind::Match,
|
||||
source,
|
||||
),
|
||||
ClauseHeader::MatchCase(header) => find_keyword(
|
||||
StartPosition::clause_start(header),
|
||||
SimpleTokenKind::Case,
|
||||
source,
|
||||
),
|
||||
ClauseHeader::For(header) => {
|
||||
let keyword = if header.is_async {
|
||||
SimpleTokenKind::Async
|
||||
} else {
|
||||
SimpleTokenKind::For
|
||||
};
|
||||
find_keyword(header.start(), keyword, source)
|
||||
}
|
||||
ClauseHeader::While(header) => {
|
||||
find_keyword(header.start(), SimpleTokenKind::While, source)
|
||||
find_keyword(StartPosition::clause_start(header), keyword, source)
|
||||
}
|
||||
ClauseHeader::While(header) => find_keyword(
|
||||
StartPosition::clause_start(header),
|
||||
SimpleTokenKind::While,
|
||||
source,
|
||||
),
|
||||
ClauseHeader::With(header) => {
|
||||
let keyword = if header.is_async {
|
||||
SimpleTokenKind::Async
|
||||
@@ -278,7 +310,7 @@ impl ClauseHeader<'_> {
|
||||
SimpleTokenKind::With
|
||||
};
|
||||
|
||||
find_keyword(header.start(), keyword, source)
|
||||
find_keyword(StartPosition::clause_start(header), keyword, source)
|
||||
}
|
||||
ClauseHeader::OrElse(header) => match header {
|
||||
ElseClause::Try(try_stmt) => {
|
||||
@@ -289,12 +321,18 @@ impl ClauseHeader<'_> {
|
||||
.or_else(|| try_stmt.body.last().map(AnyNodeRef::from))
|
||||
.unwrap();
|
||||
|
||||
find_keyword(last_statement.end(), SimpleTokenKind::Else, source)
|
||||
find_keyword(
|
||||
StartPosition::LastStatement(last_statement.end()),
|
||||
SimpleTokenKind::Else,
|
||||
source,
|
||||
)
|
||||
}
|
||||
ElseClause::For(StmtFor { body, .. })
|
||||
| ElseClause::While(StmtWhile { body, .. }) => {
|
||||
find_keyword(body.last().unwrap().end(), SimpleTokenKind::Else, source)
|
||||
}
|
||||
| ElseClause::While(StmtWhile { body, .. }) => find_keyword(
|
||||
StartPosition::LastStatement(body.last().unwrap().end()),
|
||||
SimpleTokenKind::Else,
|
||||
source,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -434,16 +472,41 @@ impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the range of `keyword` starting the search at `start_position`. Expects only comments and `(` between
|
||||
/// the `start_position` and the `keyword` token.
|
||||
/// Finds the range of `keyword` starting the search at `start_position`.
|
||||
///
|
||||
/// If the start position is at the end of the previous statement, the
|
||||
/// search will skip the optional semi-colon at the end of that statement.
|
||||
/// Other than this, we expect only trivia between the `start_position`
|
||||
/// and the keyword.
|
||||
fn find_keyword(
|
||||
start_position: TextSize,
|
||||
start_position: StartPosition,
|
||||
keyword: SimpleTokenKind,
|
||||
source: &str,
|
||||
) -> FormatResult<TextRange> {
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(start_position, source).skip_trivia();
|
||||
let next_token = match start_position {
|
||||
StartPosition::ClauseStart(text_size) => SimpleTokenizer::starts_at(text_size, source)
|
||||
.skip_trivia()
|
||||
.next(),
|
||||
StartPosition::LastStatement(text_size) => {
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(text_size, source).skip_trivia();
|
||||
|
||||
match tokenizer.next() {
|
||||
let mut token = tokenizer.next();
|
||||
|
||||
// If the last statement ends with a semi-colon, skip it.
|
||||
if matches!(
|
||||
token,
|
||||
Some(SimpleToken {
|
||||
kind: SimpleTokenKind::Semi,
|
||||
..
|
||||
})
|
||||
) {
|
||||
token = tokenizer.next();
|
||||
}
|
||||
token
|
||||
}
|
||||
};
|
||||
|
||||
match next_token {
|
||||
Some(token) if token.kind() == keyword => Ok(token.range()),
|
||||
Some(other) => {
|
||||
debug_assert!(
|
||||
@@ -466,6 +529,35 @@ fn find_keyword(
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset directly before clause header.
|
||||
///
|
||||
/// Can either be the beginning of the clause header
|
||||
/// or the end of the last statement preceding the clause.
|
||||
#[derive(Clone, Copy)]
|
||||
enum StartPosition {
|
||||
/// The beginning of a clause header
|
||||
ClauseStart(TextSize),
|
||||
/// The end of the last statement in the suite preceding a clause.
|
||||
///
|
||||
/// For example:
|
||||
/// ```python
|
||||
/// if cond:
|
||||
/// a
|
||||
/// b
|
||||
/// c;
|
||||
/// # ...^here
|
||||
/// else:
|
||||
/// d
|
||||
/// ```
|
||||
LastStatement(TextSize),
|
||||
}
|
||||
|
||||
impl StartPosition {
|
||||
fn clause_start(ranged: impl Ranged) -> Self {
|
||||
Self::ClauseStart(ranged.start())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the range of the `:` ending the clause header or `Err` if the colon can't be found.
|
||||
fn colon_range(after_keyword_or_condition: TextSize, source: &str) -> FormatResult<TextRange> {
|
||||
let mut tokenizer = SimpleTokenizer::starts_at(after_keyword_or_condition, source)
|
||||
|
||||
@@ -4,6 +4,61 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
```
|
||||
|
||||
## Outputs
|
||||
@@ -23,6 +78,63 @@ source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
|
||||
@@ -42,6 +154,63 @@ source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
|
||||
@@ -61,4 +230,61 @@ source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/range_formatting/clause_header.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
@@ -49,6 +48,14 @@ def test4(<RANGE_START> a):
|
||||
|
||||
<RANGE_START>if b + c :<RANGE_END> # trailing clause header comment
|
||||
print("Not formatted" )
|
||||
|
||||
def test5():
|
||||
x = 1
|
||||
<RANGE_START>try:
|
||||
a;
|
||||
finally:
|
||||
b
|
||||
<RANGE_END>
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -96,4 +103,11 @@ if a + b: # trailing clause header comment
|
||||
|
||||
if b + c: # trailing clause header comment
|
||||
print("Not formatted" )
|
||||
|
||||
def test5():
|
||||
x = 1
|
||||
try:
|
||||
a
|
||||
finally:
|
||||
b
|
||||
```
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
match ruff:
|
||||
case {"lint": {"select": x} | {"extend-select": x}} | {"select": x}:
|
||||
...
|
||||
match 42:
|
||||
case [[x] | [x]] | x: ...
|
||||
match 42:
|
||||
case [[x | x] | [x]] | x: ...
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user