Compare commits
2 Commits
0.13.1
...
gankra/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ef86c9637 | ||
|
|
793d0d0dd4 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,65 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.13.1
|
||||
|
||||
Released on 2025-09-18.
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Detect unnecessary `None` default for additional key expression types (`SIM910`) ([#20343](https://github.com/astral-sh/ruff/pull/20343))
|
||||
- \[`flake8-use-pathlib`\] Add fix for `PTH123` ([#20169](https://github.com/astral-sh/ruff/pull/20169))
|
||||
- \[`flake8-use-pathlib`\] Fix `PTH101`, `PTH104`, `PTH105`, `PTH121` fixes ([#20143](https://github.com/astral-sh/ruff/pull/20143))
|
||||
- \[`flake8-use-pathlib`\] Make `PTH111` fix unsafe because it can change behavior ([#20215](https://github.com/astral-sh/ruff/pull/20215))
|
||||
- \[`pycodestyle`\] Fix `E301` to only trigger for functions immediately within a class ([#19768](https://github.com/astral-sh/ruff/pull/19768))
|
||||
- \[`refurb`\] Mark `single-item-membership-test` fix as always unsafe (`FURB171`) ([#20279](https://github.com/astral-sh/ruff/pull/20279))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Handle t-strings for token-based rules and suppression comments ([#20357](https://github.com/astral-sh/ruff/pull/20357))
|
||||
- \[`flake8-bandit`\] Fix truthiness: dict-only `**` displays not truthy for `shell` (`S602`, `S604`, `S609`) ([#20177](https://github.com/astral-sh/ruff/pull/20177))
|
||||
- \[`flake8-simplify`\] Fix diagnostic to show correct method name for `str.rsplit` calls (`SIM905`) ([#20459](https://github.com/astral-sh/ruff/pull/20459))
|
||||
- \[`flynt`\] Use triple quotes for joined raw strings with newlines (`FLY002`) ([#20197](https://github.com/astral-sh/ruff/pull/20197))
|
||||
- \[`pyupgrade`\] Fix false positive when class name is shadowed by local variable (`UP008`) ([#20427](https://github.com/astral-sh/ruff/pull/20427))
|
||||
- \[`pyupgrade`\] Prevent infinite loop with `I002` and `UP026` ([#20327](https://github.com/astral-sh/ruff/pull/20327))
|
||||
- \[`ruff`\] Recognize t-strings, generators, and lambdas in `invalid-index-type` (`RUF016`) ([#20213](https://github.com/astral-sh/ruff/pull/20213))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`RUF102`\] Respect rule redirects in invalid rule code detection ([#20245](https://github.com/astral-sh/ruff/pull/20245))
|
||||
- \[`flake8-bugbear`\] Mark the fix for `unreliable-callable-check` as always unsafe (`B004`) ([#20318](https://github.com/astral-sh/ruff/pull/20318))
|
||||
- \[`ruff`\] Allow dataclass attribute value instantiation from nested frozen dataclass (`RUF009`) ([#20352](https://github.com/astral-sh/ruff/pull/20352))
|
||||
|
||||
### CLI
|
||||
|
||||
- Add fixes to `output-format=sarif` ([#20300](https://github.com/astral-sh/ruff/pull/20300))
|
||||
- Treat panics as fatal diagnostics, sort panics last ([#20258](https://github.com/astral-sh/ruff/pull/20258))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`ruff`\] Add `analyze.string-imports-min-dots` to settings ([#20375](https://github.com/astral-sh/ruff/pull/20375))
|
||||
- Update README.md with Albumentations new repository URL ([#20415](https://github.com/astral-sh/ruff/pull/20415))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Bump MSRV to Rust 1.88 ([#20470](https://github.com/astral-sh/ruff/pull/20470))
|
||||
- Enable inline noqa for multiline strings in playground ([#20442](https://github.com/astral-sh/ruff/pull/20442))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@chirizxc](https://github.com/chirizxc)
|
||||
- [@danparizher](https://github.com/danparizher)
|
||||
- [@IDrokin117](https://github.com/IDrokin117)
|
||||
- [@amyreese](https://github.com/amyreese)
|
||||
- [@AlexWaygood](https://github.com/AlexWaygood)
|
||||
- [@dylwil3](https://github.com/dylwil3)
|
||||
- [@njhearp](https://github.com/njhearp)
|
||||
- [@woodruffw](https://github.com/woodruffw)
|
||||
- [@dcreager](https://github.com/dcreager)
|
||||
- [@TaKO8Ki](https://github.com/TaKO8Ki)
|
||||
- [@BurntSushi](https://github.com/BurntSushi)
|
||||
- [@salahelfarissi](https://github.com/salahelfarissi)
|
||||
- [@MichaReiser](https://github.com/MichaReiser)
|
||||
|
||||
## 0.13.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.13.0) for a migration
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2728,7 +2728,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.13.1"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2984,7 +2984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.13.1"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3338,7 +3338,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.13.1"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
||||
@@ -5,7 +5,7 @@ resolver = "2"
|
||||
[workspace.package]
|
||||
# Please update rustfmt.toml when bumping the Rust edition
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
rust-version = "1.87"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
|
||||
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.13.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.13.1/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.13.0/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.13.0/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.13.1
|
||||
rev: v0.13.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.13.1"
|
||||
version = "0.13.0"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -22,30 +22,6 @@ exit_code: 1
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"fixes": [
|
||||
{
|
||||
"artifactChanges": [
|
||||
{
|
||||
"artifactLocation": {
|
||||
"uri": "[TMP]/input.py"
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"deletedRegion": {
|
||||
"endColumn": 1,
|
||||
"endLine": 2,
|
||||
"startColumn": 1,
|
||||
"startLine": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": {
|
||||
"text": "Remove unused import: `os`"
|
||||
}
|
||||
}
|
||||
],
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
|
||||
@@ -58,11 +58,10 @@ impl<'a> FullRenderer<'a> {
|
||||
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
|
||||
}
|
||||
|
||||
if self.config.show_fix_diff
|
||||
&& diag.has_applicable_fix(self.config)
|
||||
&& let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver)
|
||||
{
|
||||
write!(f, "{diff}")?;
|
||||
if self.config.show_fix_diff && diag.has_applicable_fix(self.config) {
|
||||
if let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver) {
|
||||
write!(f, "{diff}")?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
|
||||
@@ -459,6 +459,12 @@ impl File {
|
||||
self.source_type(db).is_stub()
|
||||
}
|
||||
|
||||
/// Returns `true` if the file is an `__init__.py(i)`
|
||||
pub fn is_init(self, db: &dyn Db) -> bool {
|
||||
let path = self.path(db).as_str();
|
||||
path.ends_with("__init__.py") || path.ends_with("__init__.pyi")
|
||||
}
|
||||
|
||||
pub fn source_type(self, db: &dyn Db) -> PySourceType {
|
||||
match self.path(db) {
|
||||
FilePath::System(path) => path
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.13.1"
|
||||
version = "0.13.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -125,15 +125,3 @@ os.makedirs("name", 0o777, False)
|
||||
os.makedirs(name="name", mode=0o777, exist_ok=False)
|
||||
|
||||
os.makedirs("name", unknown_kwarg=True)
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/20134
|
||||
os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
|
||||
os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
||||
|
||||
# Only diagnostic
|
||||
os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
|
||||
|
||||
os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
|
||||
os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
@@ -16,9 +16,7 @@ nok4 = "a".join([a, a, *a]) # Not OK (not a static length)
|
||||
nok5 = "a".join([choice("flarp")]) # Not OK (not a simple call)
|
||||
nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
|
||||
nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
|
||||
# https://github.com/astral-sh/ruff/issues/19887
|
||||
nok8 = '\n'.join([r'line1','line2'])
|
||||
nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
|
||||
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
def create_file_public_url(url, filename):
|
||||
|
||||
@@ -271,19 +271,3 @@ class ChildI9(ParentI):
|
||||
if False: super
|
||||
if False: __class__
|
||||
builtins.super(ChildI9, self).f()
|
||||
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/20422
|
||||
# UP008 should not apply when the class variable is shadowed
|
||||
class A:
|
||||
def f(self):
|
||||
return 1
|
||||
|
||||
class B(A):
|
||||
def f(self):
|
||||
return 2
|
||||
|
||||
class C(B):
|
||||
def f(self):
|
||||
C = B # Local variable C shadows the class name
|
||||
return super(C, self).f() # Should NOT trigger UP008
|
||||
|
||||
@@ -117,31 +117,3 @@ def _():
|
||||
# After
|
||||
] and \
|
||||
0 < 1: ...
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/20255
|
||||
import math
|
||||
|
||||
# NaN behavior differences
|
||||
if math.nan in [math.nan]:
|
||||
print("This is True")
|
||||
|
||||
if math.nan in (math.nan,):
|
||||
print("This is True")
|
||||
|
||||
if math.nan in {math.nan}:
|
||||
print("This is True")
|
||||
|
||||
# Potential type differences with custom __eq__ methods
|
||||
class CustomEq:
|
||||
def __eq__(self, other):
|
||||
return "custom"
|
||||
|
||||
obj = CustomEq()
|
||||
if obj in [CustomEq()]:
|
||||
pass
|
||||
|
||||
if obj in (CustomEq(),):
|
||||
pass
|
||||
|
||||
if obj in {CustomEq()}:
|
||||
pass
|
||||
|
||||
@@ -51,13 +51,3 @@ if 1 in set(1,2):
|
||||
|
||||
if 1 in set((x for x in range(2))):
|
||||
pass
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/20255
|
||||
import math
|
||||
|
||||
# set() and frozenset() with NaN
|
||||
if math.nan in set([math.nan]):
|
||||
print("This should be marked unsafe")
|
||||
|
||||
if math.nan in frozenset([math.nan]):
|
||||
print("This should be marked unsafe")
|
||||
|
||||
@@ -2,24 +2,17 @@ use std::collections::HashSet;
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::warn;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
|
||||
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
|
||||
use ruff_source_file::{OneIndexed, SourceFile};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
use crate::VERSION;
|
||||
use crate::fs::normalize_path;
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
use crate::registry::{Linter, RuleNamespace};
|
||||
|
||||
/// An emitter for producing SARIF 2.1.0-compliant JSON output.
|
||||
///
|
||||
/// Static Analysis Results Interchange Format (SARIF) is a standard format
|
||||
/// for static analysis results. For full specfification, see:
|
||||
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
|
||||
pub struct SarifEmitter;
|
||||
|
||||
impl Emitter for SarifEmitter {
|
||||
@@ -36,7 +29,7 @@ impl Emitter for SarifEmitter {
|
||||
|
||||
let unique_rules: HashSet<_> = results
|
||||
.iter()
|
||||
.filter_map(|result| result.rule_id.as_secondary_code())
|
||||
.filter_map(|result| result.code.as_secondary_code())
|
||||
.collect();
|
||||
let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
|
||||
rules.sort_by(|a, b| a.code.cmp(b.code));
|
||||
@@ -141,15 +134,6 @@ impl RuleCode<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RuleCode<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
|
||||
fn from(code: &'a Diagnostic) -> Self {
|
||||
match code.secondary_code() {
|
||||
@@ -159,83 +143,12 @@ impl<'a> From<&'a Diagnostic> for RuleCode<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a single result in a SARIF 2.1.0 report.
|
||||
///
|
||||
/// See the SARIF 2.1.0 specification for details:
|
||||
/// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug)]
|
||||
struct SarifResult<'a> {
|
||||
rule_id: RuleCode<'a>,
|
||||
code: RuleCode<'a>,
|
||||
level: String,
|
||||
message: SarifMessage,
|
||||
locations: Vec<SarifLocation>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
fixes: Vec<SarifFix>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SarifMessage {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SarifPhysicalLocation {
|
||||
artifact_location: SarifArtifactLocation,
|
||||
region: SarifRegion,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SarifLocation {
|
||||
physical_location: SarifPhysicalLocation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SarifFix {
|
||||
description: RuleDescription,
|
||||
artifact_changes: Vec<SarifArtifactChange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RuleDescription {
|
||||
text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SarifArtifactChange {
|
||||
artifact_location: SarifArtifactLocation,
|
||||
replacements: Vec<SarifReplacement>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SarifArtifactLocation {
|
||||
message: String,
|
||||
uri: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SarifReplacement {
|
||||
deleted_region: SarifRegion,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
inserted_content: Option<InsertedContent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InsertedContent {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Copy)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SarifRegion {
|
||||
start_line: OneIndexed,
|
||||
start_column: OneIndexed,
|
||||
end_line: OneIndexed,
|
||||
@@ -243,108 +156,71 @@ struct SarifRegion {
|
||||
}
|
||||
|
||||
impl<'a> SarifResult<'a> {
|
||||
fn range_to_sarif_region(source_file: &SourceFile, range: TextRange) -> SarifRegion {
|
||||
let source_code = source_file.to_source_code();
|
||||
let start_location = source_code.line_column(range.start());
|
||||
let end_location = source_code.line_column(range.end());
|
||||
|
||||
SarifRegion {
|
||||
start_line: start_location.line,
|
||||
start_column: start_location.column,
|
||||
end_line: end_location.line,
|
||||
end_column: end_location.column,
|
||||
}
|
||||
}
|
||||
|
||||
fn fix(diagnostic: &'a Diagnostic, uri: &str) -> Option<SarifFix> {
|
||||
let fix = diagnostic.fix()?;
|
||||
|
||||
let Some(source_file) = diagnostic.ruff_source_file() else {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
|
||||
diagnostic.id()
|
||||
);
|
||||
|
||||
warn!(
|
||||
"Omitting the fix for diagnostic with id `{}` because the source file is missing. This is a bug in Ruff, please report an issue.",
|
||||
diagnostic.id()
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
let fix_description = diagnostic
|
||||
.first_help_text()
|
||||
.map(std::string::ToString::to_string);
|
||||
|
||||
let replacements: Vec<SarifReplacement> = fix
|
||||
.edits()
|
||||
.iter()
|
||||
.map(|edit| {
|
||||
let range = edit.range();
|
||||
let deleted_region = Self::range_to_sarif_region(source_file, range);
|
||||
SarifReplacement {
|
||||
deleted_region,
|
||||
inserted_content: edit.content().map(|content| InsertedContent {
|
||||
text: content.to_string(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let artifact_changes = vec![SarifArtifactChange {
|
||||
artifact_location: SarifArtifactLocation {
|
||||
uri: uri.to_string(),
|
||||
},
|
||||
replacements,
|
||||
}];
|
||||
|
||||
Some(SarifFix {
|
||||
description: RuleDescription {
|
||||
text: fix_description,
|
||||
},
|
||||
artifact_changes,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn uri(diagnostic: &Diagnostic) -> Result<String> {
|
||||
let path = normalize_path(&*diagnostic.expect_ruff_filename());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
return url::Url::from_file_path(&path)
|
||||
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))
|
||||
.map(|u| u.to_string());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Ok(format!("file://{}", path.display()));
|
||||
}
|
||||
|
||||
fn from_message(diagnostic: &'a Diagnostic) -> Result<Self> {
|
||||
let start_location = diagnostic.ruff_start_location().unwrap_or_default();
|
||||
let end_location = diagnostic.ruff_end_location().unwrap_or_default();
|
||||
let region = SarifRegion {
|
||||
start_line: start_location.line,
|
||||
start_column: start_location.column,
|
||||
end_line: end_location.line,
|
||||
end_column: end_location.column,
|
||||
};
|
||||
|
||||
let uri = Self::uri(diagnostic)?;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn from_message(message: &'a Diagnostic) -> Result<Self> {
|
||||
let start_location = message.ruff_start_location().unwrap_or_default();
|
||||
let end_location = message.ruff_end_location().unwrap_or_default();
|
||||
let path = normalize_path(&*message.expect_ruff_filename());
|
||||
Ok(Self {
|
||||
rule_id: RuleCode::from(diagnostic),
|
||||
code: RuleCode::from(message),
|
||||
level: "error".to_string(),
|
||||
message: SarifMessage {
|
||||
text: diagnostic.body().to_string(),
|
||||
},
|
||||
fixes: Self::fix(diagnostic, &uri).into_iter().collect(),
|
||||
locations: vec![SarifLocation {
|
||||
physical_location: SarifPhysicalLocation {
|
||||
artifact_location: SarifArtifactLocation { uri },
|
||||
region,
|
||||
},
|
||||
}],
|
||||
message: message.body().to_string(),
|
||||
uri: url::Url::from_file_path(&path)
|
||||
.map_err(|()| anyhow::anyhow!("Failed to convert path to URL: {}", path.display()))?
|
||||
.to_string(),
|
||||
start_line: start_location.line,
|
||||
start_column: start_location.column,
|
||||
end_line: end_location.line,
|
||||
end_column: end_location.column,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn from_message(message: &'a Diagnostic) -> Result<Self> {
|
||||
let start_location = message.ruff_start_location().unwrap_or_default();
|
||||
let end_location = message.ruff_end_location().unwrap_or_default();
|
||||
let path = normalize_path(&*message.expect_ruff_filename());
|
||||
Ok(Self {
|
||||
code: RuleCode::from(message),
|
||||
level: "error".to_string(),
|
||||
message: message.body().to_string(),
|
||||
uri: path.display().to_string(),
|
||||
start_line: start_location.line,
|
||||
start_column: start_location.column,
|
||||
end_line: end_location.line,
|
||||
end_column: end_location.column,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for SarifResult<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
json!({
|
||||
"level": self.level,
|
||||
"message": {
|
||||
"text": self.message,
|
||||
},
|
||||
"locations": [{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": self.uri,
|
||||
},
|
||||
"region": {
|
||||
"startLine": self.start_line,
|
||||
"startColumn": self.start_column,
|
||||
"endLine": self.end_line,
|
||||
"endColumn": self.end_column,
|
||||
}
|
||||
}
|
||||
}],
|
||||
"ruleId": self.code.as_str(),
|
||||
})
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -380,7 +256,6 @@ mod tests {
|
||||
insta::assert_json_snapshot!(value, {
|
||||
".runs[0].tool.driver.version" => "[VERSION]",
|
||||
".runs[0].results[].locations[].physicalLocation.artifactLocation.uri" => "[URI]",
|
||||
".runs[0].results[].fixes[].artifactChanges[].artifactLocation.uri" => "[URI]",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,30 +8,6 @@ expression: value
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"fixes": [
|
||||
{
|
||||
"artifactChanges": [
|
||||
{
|
||||
"artifactLocation": {
|
||||
"uri": "[URI]"
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"deletedRegion": {
|
||||
"endColumn": 1,
|
||||
"endLine": 2,
|
||||
"startColumn": 1,
|
||||
"startLine": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": {
|
||||
"text": "Remove unused import: `os`"
|
||||
}
|
||||
}
|
||||
],
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
@@ -54,30 +30,6 @@ expression: value
|
||||
"ruleId": "F401"
|
||||
},
|
||||
{
|
||||
"fixes": [
|
||||
{
|
||||
"artifactChanges": [
|
||||
{
|
||||
"artifactLocation": {
|
||||
"uri": "[URI]"
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"deletedRegion": {
|
||||
"endColumn": 10,
|
||||
"endLine": 6,
|
||||
"startColumn": 5,
|
||||
"startLine": 6
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"description": {
|
||||
"text": "Remove assignment to unused variable `x`"
|
||||
}
|
||||
}
|
||||
],
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
|
||||
@@ -487,6 +487,7 @@ impl<'a> Iterator for PathParamIterator<'a> {
|
||||
let param_name_end = param_content.find(':').unwrap_or(param_content.len());
|
||||
let param_name = ¶m_content[..param_name_end];
|
||||
|
||||
#[expect(clippy::range_plus_one)]
|
||||
return Some((param_name, start..end + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::StringFlags;
|
||||
use ruff_python_ast::{
|
||||
@@ -5,8 +7,6 @@ use ruff_python_ast::{
|
||||
StringLiteralValue, UnaryOp, str::TripleQuotes,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_maxsplit_without_separator_fix_enabled;
|
||||
@@ -47,40 +47,14 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
|
||||
/// ## References
|
||||
/// - [Python documentation: `str.split`](https://docs.python.org/3/library/stdtypes.html#str.split)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct SplitStaticString {
|
||||
method: Method,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum Method {
|
||||
Split,
|
||||
RSplit,
|
||||
}
|
||||
|
||||
impl Method {
|
||||
fn is_split(self) -> bool {
|
||||
matches!(self, Method::Split)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Method {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Method::Split => f.write_str("split"),
|
||||
Method::RSplit => f.write_str("rsplit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) struct SplitStaticString;
|
||||
|
||||
impl Violation for SplitStaticString {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"Consider using a list literal instead of `str.{}`",
|
||||
self.method
|
||||
)
|
||||
"Consider using a list literal instead of `str.split`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
@@ -103,21 +77,26 @@ pub(crate) fn split_static_string(
|
||||
};
|
||||
|
||||
// `split` vs `rsplit`.
|
||||
let method = if attr == "split" {
|
||||
Method::Split
|
||||
let direction = if attr == "split" {
|
||||
Direction::Left
|
||||
} else {
|
||||
Method::RSplit
|
||||
Direction::Right
|
||||
};
|
||||
|
||||
let sep_arg = arguments.find_argument_value("sep", 0);
|
||||
let split_replacement = if let Some(sep) = sep_arg {
|
||||
match sep {
|
||||
Expr::NoneLiteral(_) => {
|
||||
split_default(str_value, maxsplit_value, method, checker.settings())
|
||||
split_default(str_value, maxsplit_value, direction, checker.settings())
|
||||
}
|
||||
Expr::StringLiteral(sep_value) => {
|
||||
let sep_value_str = sep_value.value.to_str();
|
||||
Some(split_sep(str_value, sep_value_str, maxsplit_value, method))
|
||||
Some(split_sep(
|
||||
str_value,
|
||||
sep_value_str,
|
||||
maxsplit_value,
|
||||
direction,
|
||||
))
|
||||
}
|
||||
// Ignore names until type inference is available.
|
||||
_ => {
|
||||
@@ -125,10 +104,10 @@ pub(crate) fn split_static_string(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
split_default(str_value, maxsplit_value, method, checker.settings())
|
||||
split_default(str_value, maxsplit_value, direction, checker.settings())
|
||||
};
|
||||
|
||||
let mut diagnostic = checker.report_diagnostic(SplitStaticString { method }, call.range());
|
||||
let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range());
|
||||
if let Some(ref replacement_expr) = split_replacement {
|
||||
diagnostic.set_fix(Fix::applicable_edit(
|
||||
Edit::range_replacement(checker.generator().expr(replacement_expr), call.range()),
|
||||
@@ -198,7 +177,7 @@ fn construct_replacement(elts: &[&str], flags: StringLiteralFlags) -> Expr {
|
||||
fn split_default(
|
||||
str_value: &StringLiteralValue,
|
||||
max_split: i32,
|
||||
method: Method,
|
||||
direction: Direction,
|
||||
settings: &LinterSettings,
|
||||
) -> Option<Expr> {
|
||||
// From the Python documentation:
|
||||
@@ -217,7 +196,7 @@ fn split_default(
|
||||
let Ok(max_split) = usize::try_from(max_split) else {
|
||||
return None;
|
||||
};
|
||||
let list_items: Vec<&str> = if method.is_split() {
|
||||
let list_items: Vec<&str> = if direction == Direction::Left {
|
||||
string_val
|
||||
.trim_start_matches(py_unicode_is_whitespace)
|
||||
.splitn(max_split + 1, py_unicode_is_whitespace)
|
||||
@@ -245,7 +224,7 @@ fn split_default(
|
||||
// - " x ".rsplit(maxsplit=0) -> [' x']
|
||||
// - "".split(maxsplit=0) -> []
|
||||
// - " ".split(maxsplit=0) -> []
|
||||
let processed_str = if method.is_split() {
|
||||
let processed_str = if direction == Direction::Left {
|
||||
string_val.trim_start_matches(py_unicode_is_whitespace)
|
||||
} else {
|
||||
string_val.trim_end_matches(py_unicode_is_whitespace)
|
||||
@@ -277,22 +256,22 @@ fn split_sep(
|
||||
str_value: &StringLiteralValue,
|
||||
sep_value: &str,
|
||||
max_split: i32,
|
||||
method: Method,
|
||||
direction: Direction,
|
||||
) -> Expr {
|
||||
let value = str_value.to_str();
|
||||
let list_items: Vec<&str> = if let Ok(split_n) = usize::try_from(max_split) {
|
||||
match method {
|
||||
Method::Split => value.splitn(split_n + 1, sep_value).collect(),
|
||||
Method::RSplit => {
|
||||
match direction {
|
||||
Direction::Left => value.splitn(split_n + 1, sep_value).collect(),
|
||||
Direction::Right => {
|
||||
let mut items: Vec<&str> = value.rsplitn(split_n + 1, sep_value).collect();
|
||||
items.reverse();
|
||||
items
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match method {
|
||||
Method::Split => value.split(sep_value).collect(),
|
||||
Method::RSplit => {
|
||||
match direction {
|
||||
Direction::Left => value.split(sep_value).collect(),
|
||||
Direction::Right => {
|
||||
let mut items: Vec<&str> = value.rsplit(sep_value).collect();
|
||||
items.reverse();
|
||||
items
|
||||
@@ -337,6 +316,12 @@ fn get_maxsplit_value(arg: Option<&Expr>) -> Option<i32> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Direction {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Like [`char::is_whitespace`] but with Python's notion of whitespace.
|
||||
///
|
||||
/// <https://github.com/astral-sh/ruff/issues/19845>
|
||||
|
||||
@@ -846,7 +846,7 @@ help: Replace with list literal
|
||||
105 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
106 | print("a,b".rsplit(","))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:111:7
|
||||
|
|
||||
110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
@@ -864,7 +864,7 @@ help: Replace with list literal
|
||||
113 |
|
||||
114 | # https://github.com/astral-sh/ruff/issues/18069
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:112:7
|
||||
|
|
||||
110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
@@ -1043,7 +1043,7 @@ help: Replace with list literal
|
||||
125 | print("".rsplit(sep=None, maxsplit=0))
|
||||
126 | print(" ".rsplit(maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:124:7
|
||||
|
|
||||
122 | print(" x ".split(maxsplit=0))
|
||||
@@ -1063,7 +1063,7 @@ help: Replace with list literal
|
||||
126 | print(" ".rsplit(maxsplit=0))
|
||||
127 | print(" ".rsplit(sep=None, maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:125:7
|
||||
|
|
||||
123 | print(" x ".split(sep=None, maxsplit=0))
|
||||
@@ -1083,7 +1083,7 @@ help: Replace with list literal
|
||||
127 | print(" ".rsplit(sep=None, maxsplit=0))
|
||||
128 | print(" x ".rsplit(maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:126:7
|
||||
|
|
||||
124 | print("".rsplit(maxsplit=0))
|
||||
@@ -1103,7 +1103,7 @@ help: Replace with list literal
|
||||
128 | print(" x ".rsplit(maxsplit=0))
|
||||
129 | print(" x ".rsplit(maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:127:7
|
||||
|
|
||||
125 | print("".rsplit(sep=None, maxsplit=0))
|
||||
@@ -1123,7 +1123,7 @@ help: Replace with list literal
|
||||
129 | print(" x ".rsplit(maxsplit=0))
|
||||
130 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:128:7
|
||||
|
|
||||
126 | print(" ".rsplit(maxsplit=0))
|
||||
@@ -1143,7 +1143,7 @@ help: Replace with list literal
|
||||
130 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
131 | print(" x ".rsplit(maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:129:7
|
||||
|
|
||||
127 | print(" ".rsplit(sep=None, maxsplit=0))
|
||||
@@ -1163,7 +1163,7 @@ help: Replace with list literal
|
||||
131 | print(" x ".rsplit(maxsplit=0))
|
||||
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:130:7
|
||||
|
|
||||
128 | print(" x ".rsplit(maxsplit=0))
|
||||
@@ -1183,7 +1183,7 @@ help: Replace with list literal
|
||||
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
133 |
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:131:7
|
||||
|
|
||||
129 | print(" x ".rsplit(maxsplit=0))
|
||||
@@ -1202,7 +1202,7 @@ help: Replace with list literal
|
||||
133 |
|
||||
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:132:7
|
||||
|
|
||||
130 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
@@ -1345,7 +1345,7 @@ help: Replace with list literal
|
||||
169 |
|
||||
170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:168:7
|
||||
|
|
||||
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
|
||||
@@ -1375,7 +1375,7 @@ SIM905 Consider using a list literal instead of `str.split`
|
||||
|
|
||||
help: Replace with list literal
|
||||
|
||||
SIM905 Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:172:1
|
||||
|
|
||||
170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
|
||||
@@ -894,7 +894,7 @@ help: Replace with list literal
|
||||
105 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
106 | print("a,b".rsplit(","))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:111:7
|
||||
|
|
||||
110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
@@ -912,7 +912,7 @@ help: Replace with list literal
|
||||
113 |
|
||||
114 | # https://github.com/astral-sh/ruff/issues/18069
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:112:7
|
||||
|
|
||||
110 | # https://github.com/astral-sh/ruff/issues/18042
|
||||
@@ -1091,7 +1091,7 @@ help: Replace with list literal
|
||||
125 | print("".rsplit(sep=None, maxsplit=0))
|
||||
126 | print(" ".rsplit(maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:124:7
|
||||
|
|
||||
122 | print(" x ".split(maxsplit=0))
|
||||
@@ -1111,7 +1111,7 @@ help: Replace with list literal
|
||||
126 | print(" ".rsplit(maxsplit=0))
|
||||
127 | print(" ".rsplit(sep=None, maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:125:7
|
||||
|
|
||||
123 | print(" x ".split(sep=None, maxsplit=0))
|
||||
@@ -1131,7 +1131,7 @@ help: Replace with list literal
|
||||
127 | print(" ".rsplit(sep=None, maxsplit=0))
|
||||
128 | print(" x ".rsplit(maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:126:7
|
||||
|
|
||||
124 | print("".rsplit(maxsplit=0))
|
||||
@@ -1151,7 +1151,7 @@ help: Replace with list literal
|
||||
128 | print(" x ".rsplit(maxsplit=0))
|
||||
129 | print(" x ".rsplit(maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:127:7
|
||||
|
|
||||
125 | print("".rsplit(sep=None, maxsplit=0))
|
||||
@@ -1171,7 +1171,7 @@ help: Replace with list literal
|
||||
129 | print(" x ".rsplit(maxsplit=0))
|
||||
130 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:128:7
|
||||
|
|
||||
126 | print(" ".rsplit(maxsplit=0))
|
||||
@@ -1191,7 +1191,7 @@ help: Replace with list literal
|
||||
130 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
131 | print(" x ".rsplit(maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:129:7
|
||||
|
|
||||
127 | print(" ".rsplit(sep=None, maxsplit=0))
|
||||
@@ -1211,7 +1211,7 @@ help: Replace with list literal
|
||||
131 | print(" x ".rsplit(maxsplit=0))
|
||||
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:130:7
|
||||
|
|
||||
128 | print(" x ".rsplit(maxsplit=0))
|
||||
@@ -1231,7 +1231,7 @@ help: Replace with list literal
|
||||
132 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
133 |
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:131:7
|
||||
|
|
||||
129 | print(" x ".rsplit(maxsplit=0))
|
||||
@@ -1250,7 +1250,7 @@ help: Replace with list literal
|
||||
133 |
|
||||
134 | # https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:132:7
|
||||
|
|
||||
130 | print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
@@ -1393,7 +1393,7 @@ help: Replace with list literal
|
||||
169 |
|
||||
170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:168:7
|
||||
|
|
||||
166 | print("S\x1cP\x1dL\x1eI\x1fT".split())
|
||||
@@ -1429,7 +1429,7 @@ help: Replace with list literal
|
||||
171 + ["a", "b", "c d "] # ["a", "b", "c d "]
|
||||
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
|
||||
|
||||
SIM905 [*] Consider using a list literal instead of `str.rsplit`
|
||||
SIM905 [*] Consider using a list literal instead of `str.split`
|
||||
--> SIM905.py:172:1
|
||||
|
|
||||
170 | # leading/trailing whitespace should not count towards maxsplit
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, ExprCall};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||
use ruff_python_semantic::{SemanticModel, analyze::typing};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::{Applicability, Edit, Fix, Violation};
|
||||
|
||||
pub(crate) fn is_keyword_only_argument_non_default(arguments: &Arguments, name: &str) -> bool {
|
||||
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
|
||||
arguments
|
||||
.find_keyword(name)
|
||||
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
|
||||
@@ -24,7 +24,10 @@ pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
|
||||
/// Check if the given segments represent a pathlib Path subclass or `PackagePath` with preview mode support.
|
||||
/// In stable mode, only checks for `Path` and `PurePath`. In preview mode, also checks for
|
||||
/// `PosixPath`, `PurePosixPath`, `WindowsPath`, `PureWindowsPath`, and `PackagePath`.
|
||||
pub(crate) fn is_pure_path_subclass_with_preview(checker: &Checker, segments: &[&str]) -> bool {
|
||||
pub(crate) fn is_pure_path_subclass_with_preview(
|
||||
checker: &crate::checkers::ast::Checker,
|
||||
segments: &[&str],
|
||||
) -> bool {
|
||||
let is_core_pathlib = matches!(segments, ["pathlib", "Path" | "PurePath"]);
|
||||
|
||||
if is_core_pathlib {
|
||||
@@ -190,7 +193,7 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
|
||||
}
|
||||
|
||||
pub(crate) fn has_unknown_keywords_or_starred_expr(
|
||||
arguments: &Arguments,
|
||||
arguments: &ast::Arguments,
|
||||
allowed: &[&str],
|
||||
) -> bool {
|
||||
if arguments.args.iter().any(Expr::is_starred_expr) {
|
||||
@@ -204,7 +207,11 @@ pub(crate) fn has_unknown_keywords_or_starred_expr(
|
||||
}
|
||||
|
||||
/// Returns `true` if argument `name` is set to a non-default `None` value.
|
||||
pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, position: usize) -> bool {
|
||||
pub(crate) fn is_argument_non_default(
|
||||
arguments: &ast::Arguments,
|
||||
name: &str,
|
||||
position: usize,
|
||||
) -> bool {
|
||||
arguments
|
||||
.find_argument_value(name, position)
|
||||
.is_some_and(|expr| !expr.is_none_literal_expr())
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{ArgOrKeyword, ExprCall};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::preview::is_fix_os_chmod_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
has_unknown_keywords_or_starred_expr, is_file_descriptor, is_keyword_only_argument_non_default,
|
||||
is_pathlib_path_call,
|
||||
check_os_pathlib_two_arg_calls, is_file_descriptor, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.chmod`.
|
||||
@@ -78,80 +73,22 @@ pub(crate) fn os_chmod(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
// 0 1 2 3
|
||||
// os.chmod(path, mode, *, dir_fd=None, follow_symlinks=True)
|
||||
// ```
|
||||
let path_arg = call.arguments.find_argument_value("path", 0);
|
||||
|
||||
if path_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
if call
|
||||
.arguments
|
||||
.find_argument_value("path", 0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
|| is_keyword_only_argument_non_default(&call.arguments, "dir_fd")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let range = call.range();
|
||||
let mut diagnostic = checker.report_diagnostic(OsChmod, call.func.range());
|
||||
|
||||
if !is_fix_os_chmod_enabled(checker.settings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if call.arguments.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
if has_unknown_keywords_or_starred_expr(
|
||||
&call.arguments,
|
||||
&["path", "mode", "dir_fd", "follow_symlinks"],
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let (Some(path_arg), Some(_)) = (path_arg, call.arguments.find_argument_value("mode", 1))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import("pathlib", "Path"),
|
||||
call.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
|
||||
let locator = checker.locator();
|
||||
let path_code = locator.slice(path_arg.range());
|
||||
|
||||
let args = |arg: ArgOrKeyword| match arg {
|
||||
ArgOrKeyword::Arg(expr) if expr.range() != path_arg.range() => {
|
||||
Some(locator.slice(expr.range()))
|
||||
}
|
||||
ArgOrKeyword::Keyword(kw)
|
||||
if matches!(kw.arg.as_deref(), Some("mode" | "follow_symlinks")) =>
|
||||
{
|
||||
Some(locator.slice(kw.range()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let chmod_args = itertools::join(
|
||||
call.arguments.arguments_source_order().filter_map(args),
|
||||
", ",
|
||||
);
|
||||
|
||||
let replacement = if is_pathlib_path_call(checker, path_arg) {
|
||||
format!("{path_code}.chmod({chmod_args})")
|
||||
} else {
|
||||
format!("{binding}({path_code}).chmod({chmod_args})")
|
||||
};
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
Ok(Fix::applicable_edits(
|
||||
Edit::range_replacement(replacement, range),
|
||||
[import_edit],
|
||||
applicability,
|
||||
))
|
||||
});
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"chmod",
|
||||
"path",
|
||||
"mode",
|
||||
is_fix_os_chmod_enabled(checker.settings()),
|
||||
OsChmod,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_path_samefile_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
};
|
||||
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_two_arg_calls;
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
@@ -67,16 +65,13 @@ pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&
|
||||
return;
|
||||
}
|
||||
|
||||
let fix_enabled = is_fix_os_path_samefile_enabled(checker.settings())
|
||||
&& !has_unknown_keywords_or_starred_expr(&call.arguments, &["f1", "f2"]);
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"samefile",
|
||||
"f1",
|
||||
"f2",
|
||||
fix_enabled,
|
||||
is_fix_os_path_samefile_enabled(checker.settings()),
|
||||
OsPathSamefile,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_rename_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
is_keyword_only_argument_non_default,
|
||||
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
@@ -80,11 +79,13 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fix_enabled = is_fix_os_rename_enabled(checker.settings())
|
||||
&& !has_unknown_keywords_or_starred_expr(
|
||||
&call.arguments,
|
||||
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
|
||||
);
|
||||
|
||||
check_os_pathlib_two_arg_calls(checker, call, "rename", "src", "dst", fix_enabled, OsRename);
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"rename",
|
||||
"src",
|
||||
"dst",
|
||||
is_fix_os_rename_enabled(checker.settings()),
|
||||
OsRename,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_replace_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
is_keyword_only_argument_non_default,
|
||||
check_os_pathlib_two_arg_calls, is_keyword_only_argument_non_default,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
@@ -83,19 +82,13 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
return;
|
||||
}
|
||||
|
||||
let fix_enabled = is_fix_os_replace_enabled(checker.settings())
|
||||
&& !has_unknown_keywords_or_starred_expr(
|
||||
&call.arguments,
|
||||
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
|
||||
);
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"replace",
|
||||
"src",
|
||||
"dst",
|
||||
fix_enabled,
|
||||
is_fix_os_replace_enabled(checker.settings()),
|
||||
OsReplace,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,14 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
return;
|
||||
};
|
||||
|
||||
let target_is_directory_arg = call.arguments.find_argument_value("target_is_directory", 2);
|
||||
|
||||
if let Some(expr) = &target_is_directory_arg {
|
||||
if expr.as_boolean_literal_expr().is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import("pathlib", "Path"),
|
||||
@@ -121,9 +129,7 @@ pub(crate) fn os_symlink(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
let src_code = locator.slice(src.range());
|
||||
let dst_code = locator.slice(dst.range());
|
||||
|
||||
let target_is_directory = call
|
||||
.arguments
|
||||
.find_argument_value("target_is_directory", 2)
|
||||
let target_is_directory = target_is_directory_arg
|
||||
.and_then(|expr| {
|
||||
let code = locator.slice(expr.range());
|
||||
expr.as_boolean_literal_expr()
|
||||
|
||||
@@ -500,72 +500,5 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
||||
126 |
|
||||
127 | os.makedirs("name", unknown_kwarg=True)
|
||||
| ^^^^^^^^^^^
|
||||
128 |
|
||||
129 | # https://github.com/astral-sh/ruff/issues/20134
|
||||
|
|
||||
help: Replace with `Path(...).mkdir(parents=True)`
|
||||
|
||||
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
--> full_name.py:130:1
|
||||
|
|
||||
129 | # https://github.com/astral-sh/ruff/issues/20134
|
||||
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
|
||||
| ^^^^^^^^
|
||||
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
||||
|
|
||||
help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
--> full_name.py:131:1
|
||||
|
|
||||
129 | # https://github.com/astral-sh/ruff/issues/20134
|
||||
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
|
||||
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
||||
| ^^^^^^^^
|
||||
132 |
|
||||
133 | # Only diagnostic
|
||||
|
|
||||
help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
--> full_name.py:134:1
|
||||
|
|
||||
133 | # Only diagnostic
|
||||
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^
|
||||
135 |
|
||||
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
|
|
||||
help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
--> full_name.py:136:1
|
||||
|
|
||||
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
|
||||
135 |
|
||||
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^^
|
||||
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
|
|
||||
help: Replace with `Path(...).rename(...)`
|
||||
|
||||
PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
--> full_name.py:137:1
|
||||
|
|
||||
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^^^
|
||||
138 |
|
||||
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
|
|
||||
help: Replace with `Path(...).replace(...)`
|
||||
|
||||
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
||||
--> full_name.py:139:1
|
||||
|
|
||||
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
138 |
|
||||
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Replace with `Path(...).samefile()`
|
||||
|
||||
@@ -931,7 +931,6 @@ help: Replace with `Path(...).mkdir(parents=True)`
|
||||
126 + pathlib.Path("name").mkdir(mode=0o777, exist_ok=False, parents=True)
|
||||
127 |
|
||||
128 | os.makedirs("name", unknown_kwarg=True)
|
||||
129 |
|
||||
|
||||
PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
||||
--> full_name.py:127:1
|
||||
@@ -940,102 +939,5 @@ PTH103 `os.makedirs()` should be replaced by `Path.mkdir(parents=True)`
|
||||
126 |
|
||||
127 | os.makedirs("name", unknown_kwarg=True)
|
||||
| ^^^^^^^^^^^
|
||||
128 |
|
||||
129 | # https://github.com/astral-sh/ruff/issues/20134
|
||||
|
|
||||
help: Replace with `Path(...).mkdir(parents=True)`
|
||||
|
||||
PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()`
|
||||
--> full_name.py:130:1
|
||||
|
|
||||
129 | # https://github.com/astral-sh/ruff/issues/20134
|
||||
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
|
||||
| ^^^^^^^^
|
||||
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
||||
|
|
||||
help: Replace with `Path(...).chmod(...)`
|
||||
1 | import os
|
||||
2 | import os.path
|
||||
3 + import pathlib
|
||||
4 |
|
||||
5 | p = "/foo"
|
||||
6 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
128 | os.makedirs("name", unknown_kwarg=True)
|
||||
129 |
|
||||
130 | # https://github.com/astral-sh/ruff/issues/20134
|
||||
- os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
|
||||
131 + pathlib.Path("pth1_link").chmod(mode=0o600, follow_symlinks= False)
|
||||
132 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
||||
133 |
|
||||
134 | # Only diagnostic
|
||||
|
||||
PTH101 [*] `os.chmod()` should be replaced by `Path.chmod()`
|
||||
--> full_name.py:131:1
|
||||
|
|
||||
129 | # https://github.com/astral-sh/ruff/issues/20134
|
||||
130 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
|
||||
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
||||
| ^^^^^^^^
|
||||
132 |
|
||||
133 | # Only diagnostic
|
||||
|
|
||||
help: Replace with `Path(...).chmod(...)`
|
||||
1 | import os
|
||||
2 | import os.path
|
||||
3 + import pathlib
|
||||
4 |
|
||||
5 | p = "/foo"
|
||||
6 | q = "bar"
|
||||
--------------------------------------------------------------------------------
|
||||
129 |
|
||||
130 | # https://github.com/astral-sh/ruff/issues/20134
|
||||
131 | os.chmod("pth1_link", mode=0o600, follow_symlinks= False )
|
||||
- os.chmod("pth1_link", mode=0o600, follow_symlinks=True)
|
||||
132 + pathlib.Path("pth1_link").chmod(mode=0o600, follow_symlinks=True)
|
||||
133 |
|
||||
134 | # Only diagnostic
|
||||
135 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
|
||||
|
||||
PTH101 `os.chmod()` should be replaced by `Path.chmod()`
|
||||
--> full_name.py:134:1
|
||||
|
|
||||
133 | # Only diagnostic
|
||||
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^
|
||||
135 |
|
||||
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
|
|
||||
help: Replace with `Path(...).chmod(...)`
|
||||
|
||||
PTH104 `os.rename()` should be replaced by `Path.rename()`
|
||||
--> full_name.py:136:1
|
||||
|
|
||||
134 | os.chmod("pth1_file", 0o700, None, True, 1, *[1], **{"x": 1}, foo=1)
|
||||
135 |
|
||||
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^^
|
||||
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
|
|
||||
help: Replace with `Path(...).rename(...)`
|
||||
|
||||
PTH105 `os.replace()` should be replaced by `Path.replace()`
|
||||
--> full_name.py:137:1
|
||||
|
|
||||
136 | os.rename("pth1_file", "pth1_file1", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^^^
|
||||
138 |
|
||||
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
|
|
||||
help: Replace with `Path(...).replace(...)`
|
||||
|
||||
PTH121 `os.path.samefile()` should be replaced by `Path.samefile()`
|
||||
--> full_name.py:139:1
|
||||
|
|
||||
137 | os.replace("pth1_file1", "pth1_file", None, None, 1, *[1], **{"x": 1}, foo=1)
|
||||
138 |
|
||||
139 | os.path.samefile("pth1_file", "pth1_link", 1, *[1], **{"x": 1}, foo=1)
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Replace with `Path(...).samefile()`
|
||||
|
||||
@@ -2,7 +2,7 @@ use ast::FStringFlags;
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr, StringFlags};
|
||||
use ruff_python_ast::{self as ast, Arguments, Expr};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -72,42 +72,24 @@ fn is_static_length(elts: &[Expr]) -> bool {
|
||||
fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option<Expr> {
|
||||
// If all elements are string constants, join them into a single string.
|
||||
if joinees.iter().all(Expr::is_string_literal_expr) {
|
||||
let mut flags: Option<ast::StringLiteralFlags> = None;
|
||||
let content = joinees
|
||||
.iter()
|
||||
.filter_map(|expr| {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
|
||||
if flags.is_none() {
|
||||
// Take the flags from the first Expr
|
||||
flags = Some(value.first_literal_flags());
|
||||
}
|
||||
Some(value.to_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.join(joiner);
|
||||
|
||||
let mut flags = flags?;
|
||||
|
||||
// If the result is a raw string and contains a newline, use triple quotes.
|
||||
if flags.prefix().is_raw() && content.contains(['\n', '\r']) {
|
||||
flags = flags.with_triple_quotes(ruff_python_ast::str::TripleQuotes::Yes);
|
||||
|
||||
// Prefer a delimiter that doesn't occur in the content; if both occur, bail.
|
||||
if content.contains(flags.quote_str()) {
|
||||
flags = flags.with_quote_style(flags.quote_style().opposite());
|
||||
if content.contains(flags.quote_str()) {
|
||||
// Both "'''" and "\"\"\"" are present in content; avoid emitting
|
||||
// an invalid raw triple-quoted literal (or escaping). Bail on the fix.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut flags = None;
|
||||
let node = ast::StringLiteral {
|
||||
value: content.into_boxed_str(),
|
||||
flags,
|
||||
value: joinees
|
||||
.iter()
|
||||
.filter_map(|expr| {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
|
||||
if flags.is_none() {
|
||||
// take the flags from the first Expr
|
||||
flags = Some(value.first_literal_flags());
|
||||
}
|
||||
Some(value.to_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.join(joiner)
|
||||
.into_boxed_str(),
|
||||
flags: flags?,
|
||||
range: TextRange::default(),
|
||||
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
|
||||
};
|
||||
|
||||
@@ -125,39 +125,18 @@ help: Replace with `f"{secrets.token_urlsafe()}a{secrets.token_hex()}"`
|
||||
13 | nok2 = a.join(["1", "2", "3"]) # Not OK (not a static joiner)
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FLY002 [*] Consider f-string instead of string join
|
||||
--> FLY002.py:20:8
|
||||
|
|
||||
18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
|
||||
19 | # https://github.com/astral-sh/ruff/issues/19887
|
||||
20 | nok8 = '\n'.join([r'line1','line2'])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
21 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
|
||||
|
|
||||
help: Replace with f-string
|
||||
17 | nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
|
||||
18 | nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
|
||||
19 | # https://github.com/astral-sh/ruff/issues/19887
|
||||
- nok8 = '\n'.join([r'line1','line2'])
|
||||
20 + nok8 = r'''line1
|
||||
21 + line2'''
|
||||
22 | nok9 = '\n'.join([r"raw string", '<""">', "<'''>"]) # Not OK (both triple-quote delimiters appear; should bail)
|
||||
23 |
|
||||
24 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FLY002 [*] Consider `f"{url}{filename}"` instead of string join
|
||||
--> FLY002.py:25:11
|
||||
--> FLY002.py:23:11
|
||||
|
|
||||
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
24 | def create_file_public_url(url, filename):
|
||||
25 | return''.join([url, filename])
|
||||
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
22 | def create_file_public_url(url, filename):
|
||||
23 | return''.join([url, filename])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Replace with `f"{url}{filename}"`
|
||||
22 |
|
||||
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
24 | def create_file_public_url(url, filename):
|
||||
20 |
|
||||
21 | # Regression test for: https://github.com/astral-sh/ruff/issues/7197
|
||||
22 | def create_file_public_url(url, filename):
|
||||
- return''.join([url, filename])
|
||||
25 + return f"{url}{filename}"
|
||||
23 + return f"{url}{filename}"
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -269,7 +269,7 @@ pub(crate) fn indentation(
|
||||
range: TextRange,
|
||||
context: &LintContext,
|
||||
) {
|
||||
if !indent_level.is_multiple_of(indent_size) {
|
||||
if indent_level % indent_size != 0 {
|
||||
if logical_line.is_comment_only() {
|
||||
context.report_diagnostic_if_enabled(
|
||||
IndentationWithInvalidMultipleComment {
|
||||
|
||||
@@ -139,11 +139,7 @@ pub(crate) fn super_call_with_parameters(checker: &Checker, call: &ast::ExprCall
|
||||
return;
|
||||
};
|
||||
|
||||
if !((first_arg_id == "__class__"
|
||||
|| (first_arg_id == parent_name.as_str()
|
||||
// If the first argument matches the class name, check if it's a local variable
|
||||
// that shadows the class name. If so, don't apply UP008.
|
||||
&& !checker.semantic().current_scope().has(first_arg_id)))
|
||||
if !((first_arg_id == "__class__" || first_arg_id == parent_name.as_str())
|
||||
&& second_arg_id == parent_arg.name().as_str())
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::pad;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for membership tests against single-item containers.
|
||||
@@ -27,16 +27,13 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// The fix is always marked as unsafe.
|
||||
///
|
||||
/// When the right-hand side is a string, this fix can change the behavior of your program.
|
||||
/// This is because `c in "a"` is true both when `c` is `"a"` and when `c` is the empty string.
|
||||
/// When the right-hand side is a string, the fix is marked as unsafe.
|
||||
/// This is because `c in "a"` is true both when `c` is `"a"` and when `c` is the empty string,
|
||||
/// so the fix can change the behavior of your program in these cases.
|
||||
///
|
||||
/// Additionally, converting `in`/`not in` against a single-item container to `==`/`!=` can
|
||||
/// change runtime behavior: `in` may consider identity (e.g., `NaN`) and always
|
||||
/// yields a `bool`.
|
||||
///
|
||||
/// Comments within the replacement range will also be removed.
|
||||
/// Additionally, if there are comments within the fix's range,
|
||||
/// it will also be marked as unsafe.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons)
|
||||
@@ -103,8 +100,14 @@ pub(crate) fn single_item_membership_test(
|
||||
expr.range(),
|
||||
);
|
||||
|
||||
// All supported cases can change runtime behavior; mark as unsafe.
|
||||
let fix = Fix::unsafe_edit(edit);
|
||||
let applicability =
|
||||
if right.is_string_literal_expr() || checker.comment_ranges().intersects(expr.range()) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
let fix = Fix::applicable_edit(edit, applicability);
|
||||
|
||||
checker
|
||||
.report_diagnostic(SingleItemMembershipTest { membership_test }, expr.range())
|
||||
|
||||
@@ -18,7 +18,6 @@ help: Convert to equality test
|
||||
4 | print("Single-element tuple")
|
||||
5 |
|
||||
6 | if 1 in [1]:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:6:4
|
||||
@@ -38,7 +37,6 @@ help: Convert to equality test
|
||||
7 | print("Single-element list")
|
||||
8 |
|
||||
9 | if 1 in {1}:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:9:4
|
||||
@@ -58,7 +56,6 @@ help: Convert to equality test
|
||||
10 | print("Single-element set")
|
||||
11 |
|
||||
12 | if "a" in "a":
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:12:4
|
||||
@@ -98,7 +95,6 @@ help: Convert to inequality test
|
||||
16 | print("Check `not in` membership test")
|
||||
17 |
|
||||
18 | if not 1 in (1,):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:18:8
|
||||
@@ -118,7 +114,6 @@ help: Convert to equality test
|
||||
19 | print("Check the negated membership test")
|
||||
20 |
|
||||
21 | # Non-errors.
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:52:5
|
||||
@@ -349,122 +344,4 @@ help: Convert to inequality test
|
||||
- ] and \
|
||||
113 + if foo != bar and \
|
||||
114 | 0 < 1: ...
|
||||
115 |
|
||||
116 | # https://github.com/astral-sh/ruff/issues/20255
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:125:4
|
||||
|
|
||||
124 | # NaN behavior differences
|
||||
125 | if math.nan in [math.nan]:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
126 | print("This is True")
|
||||
|
|
||||
help: Convert to equality test
|
||||
122 | import math
|
||||
123 |
|
||||
124 | # NaN behavior differences
|
||||
- if math.nan in [math.nan]:
|
||||
125 + if math.nan == math.nan:
|
||||
126 | print("This is True")
|
||||
127 |
|
||||
128 | if math.nan in (math.nan,):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:128:4
|
||||
|
|
||||
126 | print("This is True")
|
||||
127 |
|
||||
128 | if math.nan in (math.nan,):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
129 | print("This is True")
|
||||
|
|
||||
help: Convert to equality test
|
||||
125 | if math.nan in [math.nan]:
|
||||
126 | print("This is True")
|
||||
127 |
|
||||
- if math.nan in (math.nan,):
|
||||
128 + if math.nan == math.nan:
|
||||
129 | print("This is True")
|
||||
130 |
|
||||
131 | if math.nan in {math.nan}:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:131:4
|
||||
|
|
||||
129 | print("This is True")
|
||||
130 |
|
||||
131 | if math.nan in {math.nan}:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
132 | print("This is True")
|
||||
|
|
||||
help: Convert to equality test
|
||||
128 | if math.nan in (math.nan,):
|
||||
129 | print("This is True")
|
||||
130 |
|
||||
- if math.nan in {math.nan}:
|
||||
131 + if math.nan == math.nan:
|
||||
132 | print("This is True")
|
||||
133 |
|
||||
134 | # Potential type differences with custom __eq__ methods
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:140:4
|
||||
|
|
||||
139 | obj = CustomEq()
|
||||
140 | if obj in [CustomEq()]:
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
141 | pass
|
||||
|
|
||||
help: Convert to equality test
|
||||
137 | return "custom"
|
||||
138 |
|
||||
139 | obj = CustomEq()
|
||||
- if obj in [CustomEq()]:
|
||||
140 + if obj == CustomEq():
|
||||
141 | pass
|
||||
142 |
|
||||
143 | if obj in (CustomEq(),):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:143:4
|
||||
|
|
||||
141 | pass
|
||||
142 |
|
||||
143 | if obj in (CustomEq(),):
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
144 | pass
|
||||
|
|
||||
help: Convert to equality test
|
||||
140 | if obj in [CustomEq()]:
|
||||
141 | pass
|
||||
142 |
|
||||
- if obj in (CustomEq(),):
|
||||
143 + if obj == CustomEq():
|
||||
144 | pass
|
||||
145 |
|
||||
146 | if obj in {CustomEq()}:
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_0.py:146:4
|
||||
|
|
||||
144 | pass
|
||||
145 |
|
||||
146 | if obj in {CustomEq()}:
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
147 | pass
|
||||
|
|
||||
help: Convert to equality test
|
||||
143 | if obj in (CustomEq(),):
|
||||
144 | pass
|
||||
145 |
|
||||
- if obj in {CustomEq()}:
|
||||
146 + if obj == CustomEq():
|
||||
147 | pass
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -18,7 +18,6 @@ help: Convert to equality test
|
||||
4 | print("Single-element set")
|
||||
5 |
|
||||
6 | if 1 in set((1,)):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_1.py:6:4
|
||||
@@ -38,7 +37,6 @@ help: Convert to equality test
|
||||
7 | print("Single-element set")
|
||||
8 |
|
||||
9 | if 1 in set({1}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_1.py:9:4
|
||||
@@ -58,7 +56,6 @@ help: Convert to equality test
|
||||
10 | print("Single-element set")
|
||||
11 |
|
||||
12 | if 1 in frozenset([1]):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_1.py:12:4
|
||||
@@ -78,7 +75,6 @@ help: Convert to equality test
|
||||
13 | print("Single-element set")
|
||||
14 |
|
||||
15 | if 1 in frozenset((1,)):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_1.py:15:4
|
||||
@@ -98,7 +94,6 @@ help: Convert to equality test
|
||||
16 | print("Single-element set")
|
||||
17 |
|
||||
18 | if 1 in frozenset({1}):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_1.py:18:4
|
||||
@@ -118,7 +113,6 @@ help: Convert to equality test
|
||||
19 | print("Single-element set")
|
||||
20 |
|
||||
21 | if 1 in set(set([1])):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_1.py:21:4
|
||||
@@ -137,42 +131,4 @@ help: Convert to equality test
|
||||
21 + if 1 == 1:
|
||||
22 | print('Recursive solution')
|
||||
23 |
|
||||
24 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_1.py:59:4
|
||||
|
|
||||
58 | # set() and frozenset() with NaN
|
||||
59 | if math.nan in set([math.nan]):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
60 | print("This should be marked unsafe")
|
||||
|
|
||||
help: Convert to equality test
|
||||
56 | import math
|
||||
57 |
|
||||
58 | # set() and frozenset() with NaN
|
||||
- if math.nan in set([math.nan]):
|
||||
59 + if math.nan == math.nan:
|
||||
60 | print("This should be marked unsafe")
|
||||
61 |
|
||||
62 | if math.nan in frozenset([math.nan]):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
FURB171 [*] Membership test against single-item container
|
||||
--> FURB171_1.py:62:4
|
||||
|
|
||||
60 | print("This should be marked unsafe")
|
||||
61 |
|
||||
62 | if math.nan in frozenset([math.nan]):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
63 | print("This should be marked unsafe")
|
||||
|
|
||||
help: Convert to equality test
|
||||
59 | if math.nan in set([math.nan]):
|
||||
60 | print("This should be marked unsafe")
|
||||
61 |
|
||||
- if math.nan in frozenset([math.nan]):
|
||||
62 + if math.nan == math.nan:
|
||||
63 | print("This should be marked unsafe")
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
24 |
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp,
|
||||
Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator,
|
||||
@@ -68,8 +67,6 @@ pub struct Generator<'a> {
|
||||
indent: &'a Indentation,
|
||||
/// The line ending to use.
|
||||
line_ending: LineEnding,
|
||||
/// Preferred quote style to use. For more info see [`Generator::with_preferred_quote`].
|
||||
preferred_quote: Option<Quote>,
|
||||
buffer: String,
|
||||
indent_depth: usize,
|
||||
num_newlines: usize,
|
||||
@@ -81,7 +78,6 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
|
||||
Self {
|
||||
indent: stylist.indentation(),
|
||||
line_ending: stylist.line_ending(),
|
||||
preferred_quote: None,
|
||||
buffer: String::new(),
|
||||
indent_depth: 0,
|
||||
num_newlines: 0,
|
||||
@@ -96,7 +92,6 @@ impl<'a> Generator<'a> {
|
||||
// Style preferences.
|
||||
indent,
|
||||
line_ending,
|
||||
preferred_quote: None,
|
||||
// Internal state.
|
||||
buffer: String::new(),
|
||||
indent_depth: 0,
|
||||
@@ -105,16 +100,6 @@ impl<'a> Generator<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a preferred quote style for generated source code.
|
||||
///
|
||||
/// - If [`None`], the generator will attempt to preserve the existing quote style whenever possible.
|
||||
/// - If [`Some`], the generator will prefer the specified quote style, ignoring the one found in the source.
|
||||
#[must_use]
|
||||
pub fn with_preferred_quote(mut self, quote: Option<Quote>) -> Self {
|
||||
self.preferred_quote = quote;
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate source code from a [`Stmt`].
|
||||
pub fn stmt(mut self, stmt: &Stmt) -> String {
|
||||
self.unparse_stmt(stmt);
|
||||
@@ -173,8 +158,7 @@ impl<'a> Generator<'a> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
|
||||
let escape = AsciiEscape::with_preferred_quote(s, quote_style);
|
||||
let escape = AsciiEscape::with_preferred_quote(s, flags.quote_style());
|
||||
if let Some(len) = escape.layout().len {
|
||||
self.buffer.reserve(len);
|
||||
}
|
||||
@@ -192,9 +176,7 @@ impl<'a> Generator<'a> {
|
||||
return;
|
||||
}
|
||||
self.p(flags.prefix().as_str());
|
||||
|
||||
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
|
||||
let escape = UnicodeEscape::with_preferred_quote(s, quote_style);
|
||||
let escape = UnicodeEscape::with_preferred_quote(s, flags.quote_style());
|
||||
if let Some(len) = escape.layout().len {
|
||||
self.buffer.reserve(len);
|
||||
}
|
||||
@@ -1524,9 +1506,7 @@ impl<'a> Generator<'a> {
|
||||
self.buffer += &s;
|
||||
return;
|
||||
}
|
||||
|
||||
let quote_style = self.preferred_quote.unwrap_or_else(|| flags.quote_style());
|
||||
let escape = UnicodeEscape::with_preferred_quote(&s, quote_style);
|
||||
let escape = UnicodeEscape::with_preferred_quote(&s, flags.quote_style());
|
||||
if let Some(len) = escape.layout().len {
|
||||
self.buffer.reserve(len);
|
||||
}
|
||||
@@ -1551,9 +1531,6 @@ impl<'a> Generator<'a> {
|
||||
flags: AnyStringFlags,
|
||||
) {
|
||||
self.p(flags.prefix().as_str());
|
||||
|
||||
let flags =
|
||||
flags.with_quote_style(self.preferred_quote.unwrap_or_else(|| flags.quote_style()));
|
||||
self.p(flags.quote_str());
|
||||
self.unparse_interpolated_string_body(values, flags);
|
||||
self.p(flags.quote_str());
|
||||
@@ -1586,7 +1563,6 @@ impl<'a> Generator<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::{Mod, ModModule};
|
||||
use ruff_python_parser::{self, Mode, ParseOptions, parse_module};
|
||||
use ruff_source_file::LineEnding;
|
||||
@@ -1604,17 +1580,15 @@ mod tests {
|
||||
generator.generate()
|
||||
}
|
||||
|
||||
/// Like [`round_trip`] but configure the [`Generator`] with the requested
|
||||
/// `indentation`, `line_ending` and `preferred_quote` settings.
|
||||
/// Like [`round_trip`] but configure the [`Generator`] with the requested `indentation` and
|
||||
/// `line_ending` settings.
|
||||
fn round_trip_with(
|
||||
indentation: &Indentation,
|
||||
line_ending: LineEnding,
|
||||
preferred_quote: Option<Quote>,
|
||||
contents: &str,
|
||||
) -> String {
|
||||
let module = parse_module(contents).unwrap();
|
||||
let mut generator =
|
||||
Generator::new(indentation, line_ending).with_preferred_quote(preferred_quote);
|
||||
let mut generator = Generator::new(indentation, line_ending);
|
||||
generator.unparse_suite(module.suite());
|
||||
generator.generate()
|
||||
}
|
||||
@@ -2000,7 +1974,6 @@ if True:
|
||||
round_trip_with(
|
||||
&Indentation::new(" ".to_string()),
|
||||
LineEnding::default(),
|
||||
None,
|
||||
r"
|
||||
if True:
|
||||
pass
|
||||
@@ -2018,7 +1991,6 @@ if True:
|
||||
round_trip_with(
|
||||
&Indentation::new(" ".to_string()),
|
||||
LineEnding::default(),
|
||||
None,
|
||||
r"
|
||||
if True:
|
||||
pass
|
||||
@@ -2036,7 +2008,6 @@ if True:
|
||||
round_trip_with(
|
||||
&Indentation::new("\t".to_string()),
|
||||
LineEnding::default(),
|
||||
None,
|
||||
r"
|
||||
if True:
|
||||
pass
|
||||
@@ -2058,7 +2029,6 @@ if True:
|
||||
round_trip_with(
|
||||
&Indentation::default(),
|
||||
LineEnding::Lf,
|
||||
None,
|
||||
"if True:\n print(42)",
|
||||
),
|
||||
"if True:\n print(42)",
|
||||
@@ -2068,7 +2038,6 @@ if True:
|
||||
round_trip_with(
|
||||
&Indentation::default(),
|
||||
LineEnding::CrLf,
|
||||
None,
|
||||
"if True:\n print(42)",
|
||||
),
|
||||
"if True:\r\n print(42)",
|
||||
@@ -2078,32 +2047,9 @@ if True:
|
||||
round_trip_with(
|
||||
&Indentation::default(),
|
||||
LineEnding::Cr,
|
||||
None,
|
||||
"if True:\n print(42)",
|
||||
),
|
||||
"if True:\r print(42)",
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case::test_case(r#""'hello'""#, r#""'hello'""#, Quote::Single ; "basic str ignored")]
|
||||
#[test_case::test_case(r#"b"'hello'""#, r#"b"'hello'""#, Quote::Single ; "basic bytes ignored")]
|
||||
#[test_case::test_case("'hello'", r#""hello""#, Quote::Double ; "basic str double")]
|
||||
#[test_case::test_case(r#""hello""#, "'hello'", Quote::Single ; "basic str single")]
|
||||
#[test_case::test_case("b'hello'", r#"b"hello""#, Quote::Double ; "basic bytes double")]
|
||||
#[test_case::test_case(r#"b"hello""#, "b'hello'", Quote::Single ; "basic bytes single")]
|
||||
#[test_case::test_case(r#""hello""#, r#""hello""#, Quote::Double ; "remain str double")]
|
||||
#[test_case::test_case("'hello'", "'hello'", Quote::Single ; "remain str single")]
|
||||
#[test_case::test_case("x: list['str']", r#"x: list["str"]"#, Quote::Double ; "type ann double")]
|
||||
#[test_case::test_case(r#"x: list["str"]"#, "x: list['str']", Quote::Single ; "type ann single")]
|
||||
#[test_case::test_case("f'hello'", r#"f"hello""#, Quote::Double ; "basic fstring double")]
|
||||
#[test_case::test_case(r#"f"hello""#, "f'hello'", Quote::Single ; "basic fstring single")]
|
||||
fn preferred_quote(inp: &str, out: &str, quote: Quote) {
|
||||
let got = round_trip_with(
|
||||
&Indentation::default(),
|
||||
LineEnding::default(),
|
||||
Some(quote),
|
||||
inp,
|
||||
);
|
||||
assert_eq!(got, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1613,7 +1613,7 @@ pub(super) fn needs_chaperone_space(
|
||||
if is_no_chaperone_for_escaped_quote_in_triple_quoted_docstring_enabled(context) {
|
||||
if flags.is_triple_quoted() {
|
||||
if let Some(before_quote) = trim_end.strip_suffix(flags.quote_style().as_char()) {
|
||||
if count_consecutive_chars_from_end(before_quote, '\\').is_multiple_of(2) {
|
||||
if count_consecutive_chars_from_end(before_quote, '\\') % 2 == 0 {
|
||||
// Even backslash count preceding quote;
|
||||
// ```py
|
||||
// """a " """
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.13.1"
|
||||
version = "0.13.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -46,7 +46,6 @@ class MDTestRunner:
|
||||
CRATE_NAME,
|
||||
"--no-run",
|
||||
"--color=always",
|
||||
"--test=mdtest",
|
||||
"--message-format",
|
||||
message_format,
|
||||
],
|
||||
|
||||
@@ -752,36 +752,6 @@ b_container = ClassContainer[B](B)
|
||||
a_instance: A = use_a_class_container(b_container) # This should work
|
||||
```
|
||||
|
||||
## TypeIs
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
`TypeIs[T]` is invariant in `T`. See the [typing spec][typeis-spec] for a justification.
|
||||
|
||||
```py
|
||||
from typing import TypeIs
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
class C[T]:
|
||||
def check(x: object) -> TypeIs[T]:
|
||||
# this is a bad check, but we only care about it type-checking
|
||||
return False
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
```
|
||||
|
||||
## Inheriting from generic classes with inferred variance
|
||||
|
||||
When inheriting from a generic class with our type variable substituted in, we count its occurrences
|
||||
@@ -867,4 +837,3 @@ static_assert(is_subtype_of(DerivedContravariant[A], DerivedContravariant[B]))
|
||||
|
||||
[linear-time-variance-talk]: https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s
|
||||
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
|
||||
[typeis-spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis
|
||||
|
||||
@@ -74,25 +74,52 @@ from typing import Any as Any, Literal as Literal
|
||||
|
||||
Here, none of the symbols are being re-exported in the stub file.
|
||||
|
||||
In this case the symbols shouldn't be available as imports or attributes.
|
||||
|
||||
```py
|
||||
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
|
||||
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
|
||||
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
|
||||
from b import foo, Any, Literal
|
||||
from a import b
|
||||
|
||||
# error: [unresolved-attribute] "no attribute `Any`"
|
||||
reveal_type(b.Any) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "no attribute `Literal`"
|
||||
reveal_type(b.Literal) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "no attribute `foo`"
|
||||
reveal_type(b.foo) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "no attribute `bar`"
|
||||
reveal_type(b.bar) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-import] "Module `a.b` has no member `foo`"
|
||||
# error: [unresolved-import] "Module `a.b` has no member `bar`"
|
||||
# error: [unresolved-import] "Module `a.b` has no member `Any`"
|
||||
# error: [unresolved-import] "Module `a.b` has no member `Literal`"
|
||||
from a.b import foo, bar, Any, Literal
|
||||
|
||||
reveal_type(Any) # revealed: Unknown
|
||||
reveal_type(Literal) # revealed: Unknown
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
reveal_type(bar) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b.pyi`:
|
||||
`a/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
import foo
|
||||
```
|
||||
|
||||
`a/b.pyi`:
|
||||
|
||||
```pyi
|
||||
import a.foo
|
||||
from . import bar
|
||||
from typing import Any, Literal
|
||||
```
|
||||
|
||||
`foo.pyi`:
|
||||
`a/foo.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
`a/bar.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
@@ -261,39 +288,93 @@ reveal_type(Foo) # revealed: Unknown
|
||||
|
||||
## Re-exports in `__init__.pyi`
|
||||
|
||||
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
|
||||
but the inference would be `Unknown`.
|
||||
Within `__init__.pyi` relative imports (`from . import xyz` or `from .pub import xyz`) are also
|
||||
treated as a re-exports.
|
||||
|
||||
We check the both the members of the module and the imports of the module as you _should_ be able to
|
||||
do `from a import priv` but the attribute `a.priv` _should not_ exist.
|
||||
|
||||
The most subtle detail here is whether `from .semipriv import Pub` should make the `a.semipriv`
|
||||
attribute exist or not. We do not currently do this, although perhaps we should.
|
||||
|
||||
```py
|
||||
# error: 15 "Module `a` has no member `Foo`"
|
||||
# error: 20 "Module `a` has no member `c`"
|
||||
from a import Foo, c, foo
|
||||
import a
|
||||
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(foo) # revealed: <module 'a.foo'>
|
||||
reveal_type(a.Pub) # revealed: <class 'Pub'>
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.Priv) # revealed: Unknown
|
||||
reveal_type(a.pub) # revealed: <module 'a.pub'>
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.priv) # revealed: Unknown
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.semipriv) # revealed: Unknown
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.sub) # revealed: Unknown
|
||||
reveal_type(a.subpub) # revealed: <module 'a.sub.subpub'>
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a.subpriv) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-import] "Priv"
|
||||
from a import Pub, Priv
|
||||
|
||||
# error: [unresolved-import] "subpriv"
|
||||
from a import pub, priv, semipriv, sub, subpub, subpriv
|
||||
|
||||
reveal_type(Pub) # revealed: <class 'Pub'>
|
||||
reveal_type(Priv) # revealed: Unknown
|
||||
reveal_type(pub) # revealed: <module 'a.pub'>
|
||||
reveal_type(priv) # revealed: <module 'a.priv'>
|
||||
reveal_type(semipriv) # revealed: <module 'a.semipriv'>
|
||||
reveal_type(sub) # revealed: <module 'a.sub'>
|
||||
reveal_type(subpub) # revealed: <module 'a.sub.subpub'>
|
||||
reveal_type(subpriv) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
from .b import c
|
||||
from .foo import Foo
|
||||
# re-exported because they're relative
|
||||
from .sub import subpub
|
||||
from .semipriv import Pub
|
||||
from . import pub
|
||||
|
||||
# not re-exported because they're absolute
|
||||
from a.sub import subpriv
|
||||
from a.semipriv import Priv
|
||||
from a import priv
|
||||
```
|
||||
|
||||
`a/foo.pyi`:
|
||||
`a/pub.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
`a/b/__init__.pyi`:
|
||||
`a/priv.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`a/semipriv.pyi`:
|
||||
|
||||
```pyi
|
||||
class Pub: ...
|
||||
|
||||
class Priv: ...
|
||||
```
|
||||
|
||||
`a/sub/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
`a/b/c.pyi`:
|
||||
`a/sub/subpub.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
```
|
||||
|
||||
`a/sub/subpriv.pyi`:
|
||||
|
||||
```pyi
|
||||
|
||||
|
||||
@@ -331,7 +331,11 @@ pub(crate) fn imported_symbol<'db>(
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| {
|
||||
if file.is_stub(db) {
|
||||
RequiresExplicitReExport::Yes
|
||||
if file.is_init(db) {
|
||||
RequiresExplicitReExport::YesButInitIdiomAllowed
|
||||
} else {
|
||||
RequiresExplicitReExport::Yes
|
||||
}
|
||||
} else {
|
||||
RequiresExplicitReExport::No
|
||||
}
|
||||
@@ -932,7 +936,8 @@ fn place_from_bindings_impl<'db>(
|
||||
let mut bindings_with_constraints = bindings_with_constraints.peekable();
|
||||
|
||||
let is_non_exported = |binding: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !is_reexported(db, binding)
|
||||
requires_explicit_reexport.is_yes()
|
||||
&& !requires_explicit_reexport.is_satisfied(is_reexported(db, binding))
|
||||
};
|
||||
|
||||
let unbound_reachability_constraint = match bindings_with_constraints.peek() {
|
||||
@@ -1209,7 +1214,8 @@ fn place_from_declarations_impl<'db>(
|
||||
let mut exactly_one_declaration = false;
|
||||
|
||||
let is_non_exported = |declaration: Definition<'db>| {
|
||||
requires_explicit_reexport.is_yes() && !is_reexported(db, declaration)
|
||||
requires_explicit_reexport.is_yes()
|
||||
&& !requires_explicit_reexport.is_satisfied(is_reexported(db, declaration))
|
||||
};
|
||||
|
||||
let undeclared_reachability = match declarations.peek() {
|
||||
@@ -1320,21 +1326,26 @@ fn place_from_declarations_impl<'db>(
|
||||
// This will first check if the definition is using the "redundant alias" pattern like `import foo
|
||||
// as foo` or `from foo import bar as bar`. If it's not, it will check whether the symbol is being
|
||||
// exported via `__all__`.
|
||||
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool {
|
||||
fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> ReExportKind {
|
||||
// This information is computed by the semantic index builder.
|
||||
if definition.is_reexported(db) {
|
||||
return true;
|
||||
let reexported = definition.is_reexported(db);
|
||||
if reexported != ReExportKind::No {
|
||||
return reexported;
|
||||
}
|
||||
// At this point, the definition should either be an `import` or `from ... import` statement.
|
||||
// This is because the default value of `is_reexported` is `true` for any other kind of
|
||||
// definition.
|
||||
let Some(all_names) = dunder_all_names(db, definition.file(db)) else {
|
||||
return false;
|
||||
return ReExportKind::No;
|
||||
};
|
||||
let table = place_table(db, definition.scope(db));
|
||||
let symbol_id = definition.place(db).expect_symbol();
|
||||
let symbol_name = table.symbol(symbol_id).name();
|
||||
all_names.contains(symbol_name)
|
||||
if all_names.contains(symbol_name) {
|
||||
ReExportKind::Yes
|
||||
} else {
|
||||
ReExportKind::No
|
||||
}
|
||||
}
|
||||
|
||||
mod implicit_globals {
|
||||
@@ -1500,13 +1511,35 @@ mod implicit_globals {
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum RequiresExplicitReExport {
|
||||
Yes,
|
||||
No,
|
||||
/// This is an `__init__.pyi` and `from . import b` is considered a re-export
|
||||
YesButInitIdiomAllowed,
|
||||
Yes,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ReExportKind {
|
||||
No,
|
||||
/// `b` in `from . import b`
|
||||
InitIdiom,
|
||||
Yes,
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for ReExportKind {}
|
||||
|
||||
impl RequiresExplicitReExport {
|
||||
/// Whether re-exports are necessary at all (this is really "is not No")
|
||||
const fn is_yes(self) -> bool {
|
||||
matches!(self, RequiresExplicitReExport::Yes)
|
||||
!matches!(self, RequiresExplicitReExport::No)
|
||||
}
|
||||
|
||||
/// Whether the style of re-export is sufficient for the context
|
||||
fn is_satisfied(self, reexport: ReExportKind) -> bool {
|
||||
match self {
|
||||
RequiresExplicitReExport::No => true,
|
||||
RequiresExplicitReExport::YesButInitIdiomAllowed => reexport != ReExportKind::No,
|
||||
RequiresExplicitReExport::Yes => reexport == ReExportKind::Yes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::place::ReExportKind;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::definition::{
|
||||
@@ -1436,6 +1437,12 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
(Name::new(alias.name.id.split('.').next().unwrap()), false)
|
||||
};
|
||||
|
||||
let is_reexported = if is_reexported {
|
||||
ReExportKind::Yes
|
||||
} else {
|
||||
ReExportKind::No
|
||||
};
|
||||
|
||||
let symbol = self.add_symbol(symbol_name);
|
||||
self.add_definition(
|
||||
symbol.into(),
|
||||
@@ -1562,6 +1569,15 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
(&alias.name.id, false)
|
||||
};
|
||||
|
||||
let is_reexported = if is_reexported {
|
||||
ReExportKind::Yes
|
||||
} else if node.level == 1 {
|
||||
// `from . import a`
|
||||
ReExportKind::InitIdiom
|
||||
} else {
|
||||
ReExportKind::No
|
||||
};
|
||||
|
||||
// Look for imports `from __future__ import annotations`, ignore `as ...`
|
||||
// We intentionally don't enforce the rules about location of `__future__`
|
||||
// imports here, we assume the user's intent was to apply the `__future__`
|
||||
|
||||
@@ -8,6 +8,7 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
use crate::Db;
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::place::ReExportKind;
|
||||
use crate::semantic_index::place::ScopedPlaceId;
|
||||
use crate::semantic_index::scope::{FileScopeId, ScopeId};
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
@@ -41,7 +42,7 @@ pub struct Definition<'db> {
|
||||
pub kind: DefinitionKind<'db>,
|
||||
|
||||
/// This is a dedicated field to avoid accessing `kind` to compute this value.
|
||||
pub(crate) is_reexported: bool,
|
||||
pub(crate) is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
@@ -337,7 +338,7 @@ impl<'ast> From<StarImportDefinitionNodeRef<'ast>> for DefinitionNodeRef<'ast, '
|
||||
pub(crate) struct ImportDefinitionNodeRef<'ast> {
|
||||
pub(crate) node: &'ast ast::StmtImport,
|
||||
pub(crate) alias_index: usize,
|
||||
pub(crate) is_reexported: bool,
|
||||
pub(crate) is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -350,7 +351,7 @@ pub(crate) struct StarImportDefinitionNodeRef<'ast> {
|
||||
pub(crate) struct ImportFromDefinitionNodeRef<'ast> {
|
||||
pub(crate) node: &'ast ast::StmtImportFrom,
|
||||
pub(crate) alias_index: usize,
|
||||
pub(crate) is_reexported: bool,
|
||||
pub(crate) is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -678,11 +679,11 @@ pub enum DefinitionKind<'db> {
|
||||
}
|
||||
|
||||
impl DefinitionKind<'_> {
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
pub(crate) fn is_reexported(&self) -> ReExportKind {
|
||||
match self {
|
||||
DefinitionKind::Import(import) => import.is_reexported(),
|
||||
DefinitionKind::ImportFrom(import) => import.is_reexported(),
|
||||
_ => true,
|
||||
_ => ReExportKind::Yes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,7 +957,7 @@ impl<'db> ComprehensionDefinitionKind<'db> {
|
||||
pub struct ImportDefinitionKind {
|
||||
node: AstNodeRef<ast::StmtImport>,
|
||||
alias_index: usize,
|
||||
is_reexported: bool,
|
||||
is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
impl ImportDefinitionKind {
|
||||
@@ -968,7 +969,7 @@ impl ImportDefinitionKind {
|
||||
&self.node.node(module).names[self.alias_index]
|
||||
}
|
||||
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
pub(crate) fn is_reexported(&self) -> ReExportKind {
|
||||
self.is_reexported
|
||||
}
|
||||
}
|
||||
@@ -977,7 +978,7 @@ impl ImportDefinitionKind {
|
||||
pub struct ImportFromDefinitionKind {
|
||||
node: AstNodeRef<ast::StmtImportFrom>,
|
||||
alias_index: usize,
|
||||
is_reexported: bool,
|
||||
is_reexported: ReExportKind,
|
||||
}
|
||||
|
||||
impl ImportFromDefinitionKind {
|
||||
@@ -989,7 +990,7 @@ impl ImportFromDefinitionKind {
|
||||
&self.node.node(module).names[self.alias_index]
|
||||
}
|
||||
|
||||
pub(crate) fn is_reexported(&self) -> bool {
|
||||
pub(crate) fn is_reexported(&self) -> ReExportKind {
|
||||
self.is_reexported
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6620,7 +6620,6 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
|
||||
.map(|ty| ty.variance_of(db, typevar))
|
||||
.collect(),
|
||||
Type::SubclassOf(subclass_of_type) => subclass_of_type.variance_of(db, typevar),
|
||||
Type::TypeIs(type_is_type) => type_is_type.variance_of(db, typevar),
|
||||
Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::WrapperDescriptor(_)
|
||||
@@ -6641,6 +6640,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::NonInferableTypeVar(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_)
|
||||
| Type::TypeAlias(_) => TypeVarVariance::Bivariant,
|
||||
};
|
||||
@@ -10956,16 +10956,6 @@ impl<'db> TypeIsType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> VarianceInferable<'db> for TypeIsType<'db> {
|
||||
// See the [typing spec] on why `TypeIs` is invariant in its type.
|
||||
// [typing spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis
|
||||
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
|
||||
self.return_type(db)
|
||||
.with_polarity(TypeVarVariance::Invariant)
|
||||
.variance_of(db, typevar)
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk the MRO of this class and return the last class just before the specified known base.
|
||||
/// This can be used to determine upper bounds for `Self` type variables on methods that are
|
||||
/// being added to the given class.
|
||||
|
||||
@@ -87,8 +87,8 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
|
||||
sort_text: Some(format!("{i:-max_index_len$}")),
|
||||
detail: type_display.clone(),
|
||||
label_details: Some(CompletionItemLabelDetails {
|
||||
detail: comp.module_name.map(|name| format!(" (import {name})")),
|
||||
description: type_display,
|
||||
detail: type_display,
|
||||
description: comp.module_name.map(ToString::to_string),
|
||||
}),
|
||||
insert_text: comp.insert.map(String::from),
|
||||
additional_text_edits: import_edit.map(|edit| vec![edit]),
|
||||
|
||||
@@ -250,7 +250,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> {
|
||||
fn report_checked_file(&self, db: &dyn Db, file: File, diagnostics: &[Diagnostic]) {
|
||||
let checked = self.checked_files.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
|
||||
if checked.is_multiple_of(100) || checked == self.total_files {
|
||||
if checked % 100 == 0 || checked == self.total_files {
|
||||
// Report progress every 100 files or when all files are checked
|
||||
self.report_progress();
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ You can add the following configuration to `.gitlab-ci.yml` to run a `ruff forma
|
||||
stage: build
|
||||
interruptible: true
|
||||
image:
|
||||
name: ghcr.io/astral-sh/ruff:0.13.1-alpine
|
||||
name: ghcr.io/astral-sh/ruff:0.13.0-alpine
|
||||
before_script:
|
||||
- cd $CI_PROJECT_DIR
|
||||
- ruff --version
|
||||
@@ -106,7 +106,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.13.1
|
||||
rev: v0.13.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
@@ -119,7 +119,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.13.1
|
||||
rev: v0.13.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
@@ -133,7 +133,7 @@ To avoid running on Jupyter Notebooks, remove `jupyter` from the list of allowed
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.13.1
|
||||
rev: v0.13.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -369,7 +369,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.13.1
|
||||
rev: v0.13.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ruff"
|
||||
version = "0.13.1"
|
||||
version = "0.13.0"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
|
||||
readme = "README.md"
|
||||
@@ -111,11 +111,30 @@ force-exclude = '''
|
||||
major_labels = [] # Ruff never uses the major version number
|
||||
minor_labels = ["breaking"] # Bump the minor version on breaking changes
|
||||
|
||||
ignore_labels = ["internal", "ci", "testing", "ty"]
|
||||
changelog_ignore_labels = ["internal", "ci", "testing", "ty"]
|
||||
|
||||
changelog_sections.breaking = "Breaking changes"
|
||||
changelog_sections.preview = "Preview features"
|
||||
changelog_sections.bug = "Bug fixes"
|
||||
changelog_sections.rule = "Rule changes"
|
||||
changelog_sections.diagnostics = "Rule changes"
|
||||
changelog_sections.docstring = "Rule changes"
|
||||
changelog_sections.fixes = "Rule changes"
|
||||
changelog_sections.isort = "Rule changes"
|
||||
changelog_sections.performance = "Performance"
|
||||
changelog_sections.formatter = "Formatter"
|
||||
changelog_sections.server = "Server"
|
||||
changelog_sections.cli = "CLI"
|
||||
changelog_sections.configuration = "Configuration"
|
||||
changelog_sections.documentation = "Documentation"
|
||||
changelog_sections.__unknown__ = "Other changes"
|
||||
|
||||
# We exclude contributors from the CHANGELOG file
|
||||
# Generate separately with `rooster contributors` for the GitHub release page
|
||||
changelog_contributors = false
|
||||
|
||||
version_files = [
|
||||
"README.md",
|
||||
"pyproject.toml",
|
||||
"docs/integrations.md",
|
||||
"docs/tutorial.md",
|
||||
"crates/ruff/Cargo.toml",
|
||||
@@ -123,22 +142,3 @@ version_files = [
|
||||
"crates/ruff_wasm/Cargo.toml",
|
||||
"scripts/benchmarks/pyproject.toml",
|
||||
]
|
||||
|
||||
[tool.rooster.section-labels]
|
||||
"Breaking changes" = ["breaking"]
|
||||
"Preview features" = ["preview"]
|
||||
"Bug fixes" = ["bug"]
|
||||
"Rule changes" = [
|
||||
"diagnostics",
|
||||
"docstrings",
|
||||
"rule",
|
||||
"fixes",
|
||||
"isort",
|
||||
]
|
||||
"Performance" = ["performance"]
|
||||
"Formatter" = ["formatter"]
|
||||
"Server" = ["server"]
|
||||
"CLI" = ["cli"]
|
||||
"Configuration" = ["configuration"]
|
||||
"Documentation" = ["documentation"]
|
||||
"Other changes" = ["__unknown__"]
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.90"
|
||||
channel = "1.89"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "scripts"
|
||||
version = "0.13.1"
|
||||
version = "0.13.0"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
@@ -11,8 +11,14 @@ project_root="$(dirname "$script_root")"
|
||||
|
||||
echo "Updating metadata with rooster..."
|
||||
cd "$project_root"
|
||||
uvx --python 3.12 --isolated -- \
|
||||
rooster@0.0.10a1 release "$@"
|
||||
uv tool run --from 'rooster-blue>=0.0.7' --python 3.12 --isolated -- \
|
||||
rooster release "$@"
|
||||
|
||||
echo "Updating lockfile..."
|
||||
cargo update -p ruff
|
||||
|
||||
echo "Generating contributors list..."
|
||||
echo ""
|
||||
echo ""
|
||||
uv tool run --from 'rooster-blue>=0.0.7' --python 3.12 --isolated -- \
|
||||
rooster contributors --quiet
|
||||
|
||||
Reference in New Issue
Block a user