Compare commits

..

32 Commits

Author SHA1 Message Date
dylwil3
8316b909d9 preview->stable 2025-06-09 08:04:14 -05:00
Dylan
829acf498d [flake8-boolean-trap] Stabilize lint bool suprtypes in boolean-type-hint-positional-argument (FBT001) (#18520)
Feel free to complain about the rephrasing in the docs!
2025-06-08 20:22:48 -04:00
Dylan
e07f352f99 [flake8-bandit] Stabilize more trusted inputs in subprocess-without-shell-equals-true (S603) (#18521) 2025-06-08 20:22:48 -04:00
Dylan
8d0b6882b7 [flake8-pyi] Stabilize autofix for future-annotations-in-stub (PYI044) (#18518) 2025-06-08 20:22:48 -04:00
Dylan
65a2daea02 [semantic errors] Stabilize semantic errors (#18523) 2025-06-08 20:22:48 -04:00
Dylan
8baaa2f7f3 [syntax errors] Stabilize version-specific unsupported syntax errors (#18522) 2025-06-08 20:22:48 -04:00
Dylan
8b1ce32f04 [ruff] Stabilize checking for file-level directives in unused-noqa (RUF100) (#18497)
Note that the preview behavior was not documented (shame on us!) so the
documentation was not modified.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-08 20:22:48 -04:00
Dylan
eb5abda8ac [flake8-simplify] Stabilize further simplification to binary expressions in autofix for if-else-block-instead-of-if-exp (SIM108) (#18506) 2025-06-08 20:22:48 -04:00
Brent Westbrook
9c4ecf77b6 [refurb] Stabilize fromisoformat-replace-z (FURB162) (#18510)
This PR stabilizes the FURB162 rule by moving it from preview to stable
status for the 0.12.0 release.

## Summary
- **Rule**: FURB162 (`fromisoformat-replace-z`)
- **Purpose**: Detects unnecessary timezone replacement operations when
calling `datetime.fromisoformat()`
- **Change**: Move from `RuleGroup::Preview` to `RuleGroup::Stable` in
`codes.rs`

## Verification Links
- **Tests**:
[refurb/mod.rs](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/refurb/mod.rs#L54)
- Confirms FURB162 has only standard tests, no preview-specific test
cases
- **Documentation**:
https://docs.astral.sh/ruff/rules/fromisoformat-replace-z/ - Current
documentation shows preview status that will be automatically updated
2025-06-08 20:22:48 -04:00
Brent Westbrook
0809d88ca0 [ruff] Stabilize class-with-mixed-type-vars (RUF053) (#18512)
This PR stabilizes the RUF053 rule by moving it from preview to stable
status for the 0.12.0 release.

## Summary
- **Rule**: RUF053 (`class-with-mixed-type-vars`)
- **Purpose**: Detects classes that have both PEP 695 type parameter
lists while also inheriting from `typing.Generic`
- **Change**: Move from `RuleGroup::Preview` to `RuleGroup::Stable` in
`codes.rs` and migrate preview tests to stable tests

## Verification Links
- **Tests**:
[ruff/mod.rs](https://github.com/astral-sh/ruff/blob/main/crates/ruff_linter/src/rules/ruff/mod.rs#L98)
- Shows RUF053 moved from preview_rules to main rules test function
- **Documentation**:
https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars/ - Current
documentation shows preview status that will be automatically updated
2025-06-08 20:22:48 -04:00
Dylan
5c59167686 [ruff] Stabilize checking in presence of slices for collection-literal-concatenation (RUF005) (#18500) 2025-06-08 20:22:48 -04:00
Dylan
e2ea301c74 [refurb] Stabilize fix safety for readlines-in-for (FURB129) (#18496)
Note that the preview behavior was not documented (shame on us!) so the
documentation was not modified.

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-06-08 20:22:44 -04:00
Brent Westbrook
62364ea47e Ruff 0.12
Summary
--

Release branch for Ruff 0.12.0

TODOs
--

- [ ] Drop empty first commit
- [ ] Merge with rebase-merge (**don't squash merge!!!!**)
2025-06-08 20:14:44 -04:00
Charlie Marsh
331821244b Refactor fix in readlines-in-for (#18573)
## Summary

Post-merge feedback from https://github.com/astral-sh/ruff/pull/18542.
2025-06-08 20:10:13 -04:00
Ben Bar-Or
1dc8f8f903 [ty] Add hints to invalid-type-form for common mistakes (#18543)
Co-authored-by: Ben Bar-Or <ben.baror@ridewithvia.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-06-09 00:40:05 +01:00
Charlie Marsh
301b9f4135 Add trailing space around readlines (#18542)
Closes https://github.com/astral-sh/ruff/issues/17683.
2025-06-08 12:00:30 -04:00
Micha Reiser
86e5a311f0 [ty] Introduce and use System::env_var for better test isolation (#18538) 2025-06-07 19:56:58 +02:00
Micha Reiser
0c20010bb9 [ty] Split CLI tests into multiple files (#18537) 2025-06-07 16:43:28 +00:00
Alex Waygood
72552f31e4 [ty] Fix panic when pulling types for UnaryOp expressions inside Literal slices (#18536) 2025-06-07 15:26:10 +00:00
Alex Waygood
95497ffaab [ty] Fix panic when trying to pull types for attribute expressions inside Literal type expressions (#18535) 2025-06-07 15:59:12 +01:00
Micha Reiser
b3b900dc1e Treat ty: comments as pragma comments (#18532)
## Summary

Add support for ty's `ty:` pragma comments to ruff's formatter and E501

Fixes https://github.com/astral-sh/ruff/issues/18529

## Test Plan

Added test
2025-06-07 16:02:43 +02:00
Alex Waygood
503427855d [ty] Enable more corpus tests (#18531) 2025-06-07 14:18:25 +01:00
Alex Waygood
6e785867c3 [ty] Unify Type::is_subtype_of() and Type::is_assignable_to() (#18430) 2025-06-06 17:28:55 +00:00
Alex Waygood
1274521f9f [ty] Track the origin of the environment.python setting for better error messages (#18483) 2025-06-06 13:36:41 +01:00
shimies
8d24760643 Fix doc for Neovim setting examples (#18491)
## Summary
This PR fixes an error in the example Neovim configuration on [this
documentation
page](https://docs.astral.sh/ruff/editors/settings/#configuration).
The `configuration` block should be nested under `settings`, consistent
with other properties and as outlined
[here](https://docs.astral.sh/ruff/editors/setup/#neovim).

I encountered this issue when copying the example to configure ruff
integration in my neovim - the config didn’t work until I corrected the
nesting.

## Test Plan
- [x] Confirmed that the corrected configuration works in a real Neovim
+ Ruff setup
- [x] Verified that the updated configuration renders correctly in
MkDocs
<img width="382" alt="image"
src="https://github.com/user-attachments/assets/0722fb35-8ffa-4b10-90ba-c6e8417e40bf"
/>
2025-06-06 15:19:16 +05:30
Carl Meyer
db8db536f8 [ty] clarify requirements for scope_id argument to in_type_expression (#18488) 2025-06-05 22:46:26 -07:00
Carl Meyer
cb8246bc5f [ty] remove unnecessary Either (#18489)
Just a quick review-comment follow-up.
2025-06-05 18:39:22 -07:00
Dylan
5faf72a4d9 Bump 0.11.13 (#18484) 2025-06-05 15:18:38 -05:00
Micha Reiser
28dbc5c51e [ty] Fix completion order in playground (#18480) 2025-06-05 18:55:54 +02:00
Brent Westbrook
ce216c79cc Remove Message::to_rule (#18447)
## Summary

As the title says, this PR removes the `Message::to_rule` method by
replacing related uses of `Rule` with `NoqaCode` (or the rule's name in
the case of the cache). Where it seemed a `Rule` was really needed, we
convert back to the `Rule` by parsing either the rule name (with
`str::parse`) or the `NoqaCode` (with `Rule::from_code`).

I thought this was kind of like cheating and that it might not resolve
this part of Micha's
[comment](https://github.com/astral-sh/ruff/pull/18391#issuecomment-2933764275):

> because we can't add Rule to Diagnostic or **have it anywhere in our
shared rendering logic**

but after looking again, the only remaining `Rule` conversion in
rendering code is for the SARIF output format. The other two non-test
`Rule` conversions are for caching and writing a fix summary, which I
don't think fall into the shared rendering logic. That leaves the SARIF
format as the only real problem, but maybe we can delay that for now.

The motivation here is that we won't be able to store a `Rule` on the
new `Diagnostic` type, but we should be able to store a `NoqaCode`,
likely as a string.

## Test Plan

Existing tests

##
[Benchmarks](https://codspeed.io/astral-sh/ruff/branches/brent%2Fremove-to-rule)

Almost no perf regression, only -1% on
`linter/default-rules[large/dataset.py]`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-06-05 12:48:29 -04:00
Victorien
33468cc8cc [pyupgrade] Apply UP035 only on py313+ for get_type_hints() (#18476) 2025-06-05 17:16:29 +01:00
Ibraheem Ahmed
8531f4b3ca [ty] Add infrastructure for AST garbage collection (#18445)
## Summary

https://github.com/astral-sh/ty/issues/214 will require a couple
invasive changes that I would like to get merged even before garbage
collection is fully implemented (to avoid rebasing):
- `ParsedModule` can no longer be dereferenced directly. Instead you
need to load a `ParsedModuleRef` to access the AST, which requires a
reference to the salsa database (as it may require re-parsing the AST if
it was collected).
- `AstNodeRef` can only be dereferenced with the `node` method, which
takes a reference to the `ParsedModuleRef`. This allows us to encode the
fact that ASTs do not live as long as the database and may be collected
as soon a given instance of a `ParsedModuleRef` is dropped. There are a
number of places where we currently merge the `'db` and `'ast`
lifetimes, so this requires giving some types/functions two separate
lifetime parameters.
2025-06-05 11:43:18 -04:00
153 changed files with 4756 additions and 5112 deletions

View File

@@ -1,5 +1,31 @@
# Changelog
## 0.11.13
### Preview features
- \[`airflow`\] Add unsafe fix for module moved cases (`AIR301`,`AIR311`,`AIR312`,`AIR302`) ([#18367](https://github.com/astral-sh/ruff/pull/18367),[#18366](https://github.com/astral-sh/ruff/pull/18366),[#18363](https://github.com/astral-sh/ruff/pull/18363),[#18093](https://github.com/astral-sh/ruff/pull/18093))
- \[`refurb`\] Add coverage of `set` and `frozenset` calls (`FURB171`) ([#18035](https://github.com/astral-sh/ruff/pull/18035))
- \[`refurb`\] Mark `FURB180` fix unsafe when class has bases ([#18149](https://github.com/astral-sh/ruff/pull/18149))
### Bug fixes
- \[`perflint`\] Fix missing parentheses for lambda and ternary conditions (`PERF401`, `PERF403`) ([#18412](https://github.com/astral-sh/ruff/pull/18412))
- \[`pyupgrade`\] Apply `UP035` only on py313+ for `get_type_hints()` ([#18476](https://github.com/astral-sh/ruff/pull/18476))
- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP004`,`UP050`) ([#18393](https://github.com/astral-sh/ruff/pull/18393), [#18390](https://github.com/astral-sh/ruff/pull/18390))
### Rule changes
- \[`fastapi`\] Avoid false positive for class dependencies (`FAST003`) ([#18271](https://github.com/astral-sh/ruff/pull/18271))
### Documentation
- Update editor setup docs for Neovim and Vim ([#18324](https://github.com/astral-sh/ruff/pull/18324))
### Other changes
- Support Python 3.14 template strings (t-strings) in formatter and parser ([#17851](https://github.com/astral-sh/ruff/pull/17851))
## 0.11.12
### Preview features

8
Cargo.lock generated
View File

@@ -2501,7 +2501,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.12"
version = "0.11.13"
dependencies = [
"anyhow",
"argfile",
@@ -2738,7 +2738,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.12"
version = "0.11.13"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3074,7 +3074,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.12"
version = "0.11.13"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3965,6 +3965,7 @@ dependencies = [
"anyhow",
"bitflags 2.9.1",
"camino",
"colored 3.0.0",
"compact_str",
"countme",
"dir-test",
@@ -3977,6 +3978,7 @@ dependencies = [
"ordermap",
"quickcheck",
"quickcheck_macros",
"ruff_annotate_snippets",
"ruff_db",
"ruff_index",
"ruff_macros",

View File

@@ -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.11.12/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.12/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.11.13/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.13/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.11.12
rev: v0.11.13
hooks:
# Run the linter.
- id: ruff

View File

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

View File

@@ -439,7 +439,10 @@ impl LintCacheData {
let messages = messages
.iter()
.filter_map(|msg| msg.to_rule().map(|rule| (rule, msg)))
// Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
// this also serves to filter them out, but we shouldn't be caching files with syntax
// errors anyway.
.filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
.map(|(rule, msg)| {
// Make sure that all message use the same source file.
assert_eq!(

View File

@@ -30,7 +30,7 @@ impl<'a> Explanation<'a> {
let (linter, _) = Linter::parse_code(&code).unwrap();
let fix = rule.fixable().to_string();
Self {
name: rule.as_ref(),
name: rule.name().as_str(),
code,
linter: linter.name(),
summary: rule.message_formats()[0],
@@ -44,7 +44,7 @@ impl<'a> Explanation<'a> {
fn format_rule_text(rule: Rule) -> String {
let mut output = String::new();
let _ = write!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code());
let _ = write!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());
output.push('\n');
output.push('\n');

View File

@@ -165,9 +165,9 @@ impl AddAssign for FixMap {
continue;
}
let fixed_in_file = self.0.entry(filename).or_default();
for (rule, count) in fixed {
for (rule, name, count) in fixed.iter() {
if count > 0 {
*fixed_in_file.entry(rule).or_default() += count;
*fixed_in_file.entry(rule).or_default(name) += count;
}
}
}
@@ -305,7 +305,7 @@ pub(crate) fn lint_path(
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default();
let fixed = FixTable::default();
(result, transformed, fixed)
}
} else {
@@ -319,7 +319,7 @@ pub(crate) fn lint_path(
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default();
let fixed = FixTable::default();
(result, transformed, fixed)
};
@@ -473,7 +473,7 @@ pub(crate) fn lint_stdin(
}
let transformed = source_kind;
let fixed = FxHashMap::default();
let fixed = FixTable::default();
(result, transformed, fixed)
}
} else {
@@ -487,7 +487,7 @@ pub(crate) fn lint_stdin(
ParseSource::None,
);
let transformed = source_kind;
let fixed = FxHashMap::default();
let fixed = FixTable::default();
(result, transformed, fixed)
};

View File

@@ -7,6 +7,7 @@ use bitflags::bitflags;
use colored::Colorize;
use itertools::{Itertools, iterate};
use ruff_linter::codes::NoqaCode;
use ruff_linter::linter::FixTable;
use serde::Serialize;
use ruff_linter::fs::relativize_path;
@@ -80,7 +81,7 @@ impl Printer {
let fixed = diagnostics
.fixed
.values()
.flat_map(std::collections::HashMap::values)
.flat_map(FixTable::counts)
.sum::<usize>();
if self.flags.intersects(Flags::SHOW_VIOLATIONS) {
@@ -302,7 +303,7 @@ impl Printer {
let statistics: Vec<ExpandedStatistics> = diagnostics
.messages
.iter()
.map(|message| (message.to_noqa_code(), message))
.map(|message| (message.noqa_code(), message))
.sorted_by_key(|(code, message)| (*code, message.fixable()))
.fold(
vec![],
@@ -472,13 +473,13 @@ fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableStatistics
fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap) -> Result<()> {
let total = fixed
.values()
.map(|table| table.values().sum::<usize>())
.map(|table| table.counts().sum::<usize>())
.sum::<usize>();
assert!(total > 0);
let num_digits = num_digits(
*fixed
fixed
.values()
.filter_map(|table| table.values().max())
.filter_map(|table| table.counts().max())
.max()
.unwrap(),
);
@@ -498,12 +499,11 @@ fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap) -> Result<()> {
relativize_path(filename).bold(),
":".cyan()
)?;
for (rule, count) in table.iter().sorted_by_key(|(.., count)| Reverse(*count)) {
for (code, name, count) in table.iter().sorted_by_key(|(.., count)| Reverse(*count)) {
writeln!(
writer,
" {count:>num_digits$} × {} ({})",
rule.noqa_code().to_string().red().bold(),
rule.as_ref(),
" {count:>num_digits$} × {code} ({name})",
code = code.to_string().red().bold(),
)?;
}
}

View File

@@ -566,7 +566,7 @@ fn venv() -> Result<()> {
----- stderr -----
ruff failed
Cause: Invalid search path settings
Cause: Failed to discover the site-packages directory: Invalid `--python` argument: `none` does not point to a Python executable or a directory on disk
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `none`: does not point to a Python executable or a directory on disk
");
});

View File

@@ -5436,14 +5436,15 @@ match 2:
print("it's one")
"#
),
@r"
success: true
exit_code: 0
@r###"
success: false
exit_code: 1
----- stdout -----
All checks passed!
test.py:2:1: SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
Found 1 error.
----- stderr -----
"
"###
);
// syntax error on 3.9 with preview

View File

@@ -1,5 +1,4 @@
use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
use ruff_python_ast::ModModule;
@@ -18,7 +17,7 @@ use crate::source::source_text;
/// The query is only cached when the [`source_text()`] hasn't changed. This is because
/// comparing two ASTs is a non-trivial operation and every offset change is directly
/// reflected in the changed AST offsets.
/// The other reason is that Ruff's AST doesn't implement `Eq` which Sala requires
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
/// for determining if a query result is unchanged.
#[salsa::tracked(returns(ref), no_eq)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
@@ -36,7 +35,10 @@ pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
ParsedModule::new(parsed)
}
/// Cheap cloneable wrapper around the parsed module.
/// A wrapper around a parsed module.
///
/// This type manages instances of the module AST. A particular instance of the AST
/// is represented with the [`ParsedModuleRef`] type.
#[derive(Clone)]
pub struct ParsedModule {
inner: Arc<Parsed<ModModule>>,
@@ -49,17 +51,11 @@ impl ParsedModule {
}
}
/// Consumes `self` and returns the Arc storing the parsed module.
pub fn into_arc(self) -> Arc<Parsed<ModModule>> {
self.inner
}
}
impl Deref for ParsedModule {
type Target = Parsed<ModModule>;
fn deref(&self) -> &Self::Target {
&self.inner
/// Loads a reference to the parsed module.
pub fn load(&self, _db: &dyn Db) -> ParsedModuleRef {
ParsedModuleRef {
module_ref: self.inner.clone(),
}
}
}
@@ -77,6 +73,30 @@ impl PartialEq for ParsedModule {
impl Eq for ParsedModule {}
/// Cheap cloneable wrapper around an instance of a module AST.
#[derive(Clone)]
pub struct ParsedModuleRef {
module_ref: Arc<Parsed<ModModule>>,
}
impl ParsedModuleRef {
pub fn as_arc(&self) -> &Arc<Parsed<ModModule>> {
&self.module_ref
}
pub fn into_arc(self) -> Arc<Parsed<ModModule>> {
self.module_ref
}
}
impl std::ops::Deref for ParsedModuleRef {
type Target = Parsed<ModModule>;
fn deref(&self) -> &Self::Target {
&self.module_ref
}
}
#[cfg(test)]
mod tests {
use crate::Db;
@@ -98,7 +118,7 @@ mod tests {
let file = system_path_to_file(&db, path).unwrap();
let parsed = parsed_module(&db, file);
let parsed = parsed_module(&db, file).load(&db);
assert!(parsed.has_valid_syntax());
@@ -114,7 +134,7 @@ mod tests {
let file = system_path_to_file(&db, path).unwrap();
let parsed = parsed_module(&db, file);
let parsed = parsed_module(&db, file).load(&db);
assert!(parsed.has_valid_syntax());
@@ -130,7 +150,7 @@ mod tests {
let virtual_file = db.files().virtual_file(&db, path);
let parsed = parsed_module(&db, virtual_file.file());
let parsed = parsed_module(&db, virtual_file.file()).load(&db);
assert!(parsed.has_valid_syntax());
@@ -146,7 +166,7 @@ mod tests {
let virtual_file = db.files().virtual_file(&db, path);
let parsed = parsed_module(&db, virtual_file.file());
let parsed = parsed_module(&db, virtual_file.file()).load(&db);
assert!(parsed.has_valid_syntax());
@@ -177,7 +197,7 @@ else:
let file = vendored_path_to_file(&db, VendoredPath::new("path.pyi")).unwrap();
let parsed = parsed_module(&db, file);
let parsed = parsed_module(&db, file).load(&db);
assert!(parsed.has_valid_syntax());
}

View File

@@ -171,6 +171,21 @@ pub trait System: Debug {
PatternError,
>;
/// Fetches the environment variable `key` from the current process.
///
/// # Errors
///
/// Returns [`std::env::VarError::NotPresent`] if:
/// - The variable is not set.
/// - The variable's name contains an equal sign or NUL (`'='` or `'\0'`).
///
/// Returns [`std::env::VarError::NotUnicode`] if the variable's value is not valid
/// Unicode.
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
let _ = name;
Err(std::env::VarError::NotPresent)
}
fn as_any(&self) -> &dyn std::any::Any;
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;

View File

@@ -214,6 +214,10 @@ impl System for OsSystem {
})
})))
}
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
std::env::var(name)
}
}
impl OsSystem {

View File

@@ -29,7 +29,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
if let Some(explanation) = rule.explanation() {
let mut output = String::new();
let _ = writeln!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code());
let _ = writeln!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());
let (linter, _) = Linter::parse_code(&rule.noqa_code().to_string()).unwrap();
if linter.url().is_some() {
@@ -101,7 +101,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
let filename = PathBuf::from(ROOT_DIR)
.join("docs")
.join("rules")
.join(rule.as_ref())
.join(&*rule.name())
.with_extension("md");
if args.dry_run {

View File

@@ -55,7 +55,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
FixAvailability::None => format!("<span {SYMBOL_STYLE}></span>"),
};
let rule_name = rule.as_ref();
let rule_name = rule.name();
// If the message ends in a bracketed expression (like: "Use {replacement}"), escape the
// brackets. Otherwise, it'll be interpreted as an HTML attribute via the `attr_list`

View File

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

View File

@@ -110,6 +110,8 @@ from typing_extensions import CapsuleType
# UP035 on py313+ only
from typing_extensions import deprecated
# UP035 on py313+ only
from typing_extensions import get_type_hints
# https://github.com/astral-sh/ruff/issues/15780
from typing_extensions import is_typeddict

View File

@@ -102,3 +102,6 @@ with open("furb129.py") as f:
pass
for line in(f).readlines():
pass
# Test case for issue #17683 (missing space before keyword)
print([line for line in f.readlines()if True])

View File

@@ -65,7 +65,7 @@ use crate::docstrings::extraction::ExtractionTarget;
use crate::importer::{ImportRequest, Importer, ResolutionError};
use crate::noqa::NoqaMapping;
use crate::package::PackageRoot;
use crate::preview::{is_semantic_errors_enabled, is_undefined_export_in_dunder_init_enabled};
use crate::preview::is_undefined_export_in_dunder_init_enabled;
use crate::registry::{AsRule, Rule};
use crate::rules::pyflakes::rules::{
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
@@ -663,9 +663,7 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::AsyncComprehensionInSyncComprehension(_)
| SemanticSyntaxErrorKind::DuplicateParameter(_)
| SemanticSyntaxErrorKind::NonlocalDeclarationAtModuleLevel => {
if is_semantic_errors_enabled(self.settings) {
self.semantic_errors.borrow_mut().push(error);
}
self.semantic_errors.borrow_mut().push(error);
}
}
}

View File

@@ -12,7 +12,6 @@ use crate::fix::edits::delete_comment;
use crate::noqa::{
Code, Directive, FileExemption, FileNoqaDirectives, NoqaDirectives, NoqaMapping,
};
use crate::preview::is_check_file_level_directives_enabled;
use crate::registry::{AsRule, Rule, RuleSet};
use crate::rule_redirects::get_redirect_target;
use crate::rules::pygrep_hooks;
@@ -112,25 +111,16 @@ pub(crate) fn check_noqa(
&& !exemption.includes(Rule::UnusedNOQA)
&& !per_file_ignores.contains(Rule::UnusedNOQA)
{
let directives: Vec<_> = if is_check_file_level_directives_enabled(settings) {
noqa_directives
.lines()
.iter()
.map(|line| (&line.directive, &line.matches, false))
.chain(
file_noqa_directives
.lines()
.iter()
.map(|line| (&line.parsed_file_exemption, &line.matches, true)),
)
.collect()
} else {
noqa_directives
.lines()
.iter()
.map(|line| (&line.directive, &line.matches, false))
.collect()
};
let directives = noqa_directives
.lines()
.iter()
.map(|line| (&line.directive, &line.matches, false))
.chain(
file_noqa_directives
.lines()
.iter()
.map(|line| (&line.parsed_file_exemption, &line.matches, true)),
);
for (directive, matches, is_file_level) in directives {
match directive {
Directive::All(directive) => {

View File

@@ -4,13 +4,13 @@
/// `--select`. For pylint this is e.g. C0414 and E0118 but also C and E01.
use std::fmt::Formatter;
use strum_macros::{AsRefStr, EnumIter};
use strum_macros::EnumIter;
use crate::registry::Linter;
use crate::rule_selector::is_single_rule_selector;
use crate::rules;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NoqaCode(&'static str, &'static str);
impl NoqaCode {
@@ -1019,13 +1019,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "049") => (RuleGroup::Preview, rules::ruff::rules::DataclassEnum),
(Ruff, "051") => (RuleGroup::Stable, rules::ruff::rules::IfKeyInDictDel),
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
(Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
(Ruff, "053") => (RuleGroup::Stable, rules::ruff::rules::ClassWithMixedTypeVars),
(Ruff, "054") => (RuleGroup::Preview, rules::ruff::rules::IndentedFormFeed),
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),
(Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip),
(Ruff, "059") => (RuleGroup::Stable, rules::ruff::rules::UnusedUnpackedVariable),
(Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable),
(Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
@@ -1121,7 +1121,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "132") => (RuleGroup::Preview, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "136") => (RuleGroup::Stable, rules::refurb::rules::IfExprMinMax),
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
(Refurb, "142") => (RuleGroup::Preview, rules::refurb::rules::ForLoopSetMutations),
(Refurb, "142") => (RuleGroup::Stable, rules::refurb::rules::ForLoopSetMutations),
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
@@ -1129,7 +1129,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "156") => (RuleGroup::Preview, rules::refurb::rules::HardcodedStringCharset),
(Refurb, "157") => (RuleGroup::Preview, rules::refurb::rules::VerboseDecimalConstructor),
(Refurb, "161") => (RuleGroup::Stable, rules::refurb::rules::BitCount),
(Refurb, "162") => (RuleGroup::Preview, rules::refurb::rules::FromisoformatReplaceZ),
(Refurb, "162") => (RuleGroup::Stable, rules::refurb::rules::FromisoformatReplaceZ),
(Refurb, "163") => (RuleGroup::Stable, rules::refurb::rules::RedundantLogBase),
(Refurb, "164") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryFromFloat),
(Refurb, "166") => (RuleGroup::Preview, rules::refurb::rules::IntOnSlicedStr),

View File

@@ -1,7 +1,7 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use rustc_hash::FxHashSet;
use ruff_diagnostics::{IsolationLevel, SourceMap};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@@ -59,13 +59,13 @@ fn apply_fixes<'a>(
let mut last_pos: Option<TextSize> = None;
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
let mut isolated: FxHashSet<u32> = FxHashSet::default();
let mut fixed = FxHashMap::default();
let mut fixed = FixTable::default();
let mut source_map = SourceMap::default();
for (rule, fix) in diagnostics
.filter_map(|msg| msg.to_rule().map(|rule| (rule, msg)))
.filter_map(|(rule, diagnostic)| diagnostic.fix().map(|fix| (rule, fix)))
.sorted_by(|(rule1, fix1), (rule2, fix2)| cmp_fix(*rule1, *rule2, fix1, fix2))
for (code, name, fix) in diagnostics
.filter_map(|msg| msg.noqa_code().map(|code| (code, msg.name(), msg)))
.filter_map(|(code, name, diagnostic)| diagnostic.fix().map(|fix| (code, name, fix)))
.sorted_by(|(_, name1, fix1), (_, name2, fix2)| cmp_fix(name1, name2, fix1, fix2))
{
let mut edits = fix
.edits()
@@ -110,7 +110,7 @@ fn apply_fixes<'a>(
}
applied.extend(applied_edits.drain(..));
*fixed.entry(rule).or_default() += 1;
*fixed.entry(code).or_default(name) += 1;
}
// Add the remaining content.
@@ -125,34 +125,44 @@ fn apply_fixes<'a>(
}
/// Compare two fixes.
fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering {
fn cmp_fix(name1: &str, name2: &str, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering {
// Always apply `RedefinedWhileUnused` before `UnusedImport`, as the latter can end up fixing
// the former. But we can't apply this just for `RedefinedWhileUnused` and `UnusedImport` because it violates
// `< is transitive: a < b and b < c implies a < c. The same must hold for both == and >.`
// See https://github.com/astral-sh/ruff/issues/12469#issuecomment-2244392085
match (rule1, rule2) {
(Rule::RedefinedWhileUnused, Rule::RedefinedWhileUnused) => std::cmp::Ordering::Equal,
(Rule::RedefinedWhileUnused, _) => std::cmp::Ordering::Less,
(_, Rule::RedefinedWhileUnused) => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
let redefined_while_unused = Rule::RedefinedWhileUnused.name().as_str();
if (name1, name2) == (redefined_while_unused, redefined_while_unused) {
std::cmp::Ordering::Equal
} else if name1 == redefined_while_unused {
std::cmp::Ordering::Less
} else if name2 == redefined_while_unused {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
// Apply fixes in order of their start position.
.then_with(|| fix1.min_start().cmp(&fix2.min_start()))
// Break ties in the event of overlapping rules, for some specific combinations.
.then_with(|| match (&rule1, &rule2) {
.then_with(|| {
let rules = (name1, name2);
// Apply `MissingTrailingPeriod` fixes before `NewLineAfterLastParagraph` fixes.
(Rule::MissingTrailingPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less,
(Rule::NewLineAfterLastParagraph, Rule::MissingTrailingPeriod) => {
let missing_trailing_period = Rule::MissingTrailingPeriod.name().as_str();
let newline_after_last_paragraph = Rule::NewLineAfterLastParagraph.name().as_str();
let if_else_instead_of_dict_get = Rule::IfElseBlockInsteadOfDictGet.name().as_str();
let if_else_instead_of_if_exp = Rule::IfElseBlockInsteadOfIfExp.name().as_str();
if rules == (missing_trailing_period, newline_after_last_paragraph) {
std::cmp::Ordering::Less
} else if rules == (newline_after_last_paragraph, missing_trailing_period) {
std::cmp::Ordering::Greater
}
// Apply `IfElseBlockInsteadOfDictGet` fixes before `IfElseBlockInsteadOfIfExp` fixes.
(Rule::IfElseBlockInsteadOfDictGet, Rule::IfElseBlockInsteadOfIfExp) => {
else if rules == (if_else_instead_of_dict_get, if_else_instead_of_if_exp) {
std::cmp::Ordering::Less
}
(Rule::IfElseBlockInsteadOfIfExp, Rule::IfElseBlockInsteadOfDictGet) => {
} else if rules == (if_else_instead_of_if_exp, if_else_instead_of_dict_get) {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
_ => std::cmp::Ordering::Equal,
})
}
@@ -197,7 +207,7 @@ mod tests {
source_map,
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(code, "");
assert_eq!(fixes.values().sum::<usize>(), 0);
assert_eq!(fixes.counts().sum::<usize>(), 0);
assert!(source_map.markers().is_empty());
}
@@ -234,7 +244,7 @@ print("hello world")
"#
.trim()
);
assert_eq!(fixes.values().sum::<usize>(), 1);
assert_eq!(fixes.counts().sum::<usize>(), 1);
assert_eq!(
source_map.markers(),
&[
@@ -275,7 +285,7 @@ class A(Bar):
"
.trim(),
);
assert_eq!(fixes.values().sum::<usize>(), 1);
assert_eq!(fixes.counts().sum::<usize>(), 1);
assert_eq!(
source_map.markers(),
&[
@@ -312,7 +322,7 @@ class A:
"
.trim()
);
assert_eq!(fixes.values().sum::<usize>(), 1);
assert_eq!(fixes.counts().sum::<usize>(), 1);
assert_eq!(
source_map.markers(),
&[
@@ -353,7 +363,7 @@ class A(object):
"
.trim()
);
assert_eq!(fixes.values().sum::<usize>(), 2);
assert_eq!(fixes.counts().sum::<usize>(), 2);
assert_eq!(
source_map.markers(),
&[
@@ -395,7 +405,7 @@ class A:
"
.trim(),
);
assert_eq!(fixes.values().sum::<usize>(), 1);
assert_eq!(fixes.counts().sum::<usize>(), 1);
assert_eq!(
source_map.markers(),
&[

View File

@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::collections::hash_map::Entry;
use std::path::Path;
use anyhow::{Result, anyhow};
@@ -22,13 +23,14 @@ use crate::checkers::imports::check_imports;
use crate::checkers::noqa::check_noqa;
use crate::checkers::physical_lines::check_physical_lines;
use crate::checkers::tokens::check_tokens;
use crate::codes::NoqaCode;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::fix::{FixResult, fix_file};
use crate::message::Message;
use crate::noqa::add_noqa;
use crate::package::PackageRoot;
use crate::preview::{is_py314_support_enabled, is_unsupported_syntax_enabled};
use crate::preview::is_py314_support_enabled;
use crate::registry::{AsRule, Rule, RuleSet};
#[cfg(any(feature = "test-rules", test))]
use crate::rules::ruff::rules::test_rules::{self, TEST_RULES, TestRule};
@@ -84,7 +86,53 @@ impl LinterResult {
}
}
pub type FixTable = FxHashMap<Rule, usize>;
#[derive(Debug, Default, PartialEq)]
struct FixCount {
rule_name: &'static str,
count: usize,
}
/// A mapping from a noqa code to the corresponding lint name and a count of applied fixes.
#[derive(Debug, Default, PartialEq)]
pub struct FixTable(FxHashMap<NoqaCode, FixCount>);
impl FixTable {
pub fn counts(&self) -> impl Iterator<Item = usize> {
self.0.values().map(|fc| fc.count)
}
pub fn entry(&mut self, code: NoqaCode) -> FixTableEntry {
FixTableEntry(self.0.entry(code))
}
pub fn iter(&self) -> impl Iterator<Item = (NoqaCode, &'static str, usize)> {
self.0
.iter()
.map(|(code, FixCount { rule_name, count })| (*code, *rule_name, *count))
}
pub fn keys(&self) -> impl Iterator<Item = NoqaCode> {
self.0.keys().copied()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
pub struct FixTableEntry<'a>(Entry<'a, NoqaCode, FixCount>);
impl<'a> FixTableEntry<'a> {
pub fn or_default(self, rule_name: &'static str) -> &'a mut usize {
&mut (self
.0
.or_insert(FixCount {
rule_name,
count: 0,
})
.count)
}
}
pub struct FixerResult<'a> {
/// The result returned by the linter, after applying any fixes.
@@ -399,11 +447,7 @@ pub fn check_path(
}
}
let syntax_errors = if is_unsupported_syntax_enabled(settings) {
parsed.unsupported_syntax_errors()
} else {
&[]
};
let syntax_errors = parsed.unsupported_syntax_errors();
diagnostics_to_messages(
diagnostics,
@@ -581,7 +625,7 @@ pub fn lint_fix<'a>(
let mut transformed = Cow::Borrowed(source_kind);
// Track the number of fixed errors across iterations.
let mut fixed = FxHashMap::default();
let mut fixed = FixTable::default();
// As an escape hatch, bail after 100 iterations.
let mut iterations = 0;
@@ -650,12 +694,7 @@ pub fn lint_fix<'a>(
// syntax error. Return the original code.
if has_valid_syntax && has_no_syntax_errors {
if let Some(error) = parsed.errors().first() {
report_fix_syntax_error(
path,
transformed.source_code(),
error,
fixed.keys().copied(),
);
report_fix_syntax_error(path, transformed.source_code(), error, fixed.keys());
return Err(anyhow!("Fix introduced a syntax error"));
}
}
@@ -670,8 +709,8 @@ pub fn lint_fix<'a>(
{
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.
for (rule, count) in applied {
*fixed.entry(rule).or_default() += count;
for (rule, name, count) in applied.iter() {
*fixed.entry(rule).or_default(name) += count;
}
transformed = Cow::Owned(transformed.updated(fixed_contents, &source_map));
@@ -698,10 +737,10 @@ pub fn lint_fix<'a>(
}
}
fn collect_rule_codes(rules: impl IntoIterator<Item = Rule>) -> String {
fn collect_rule_codes(rules: impl IntoIterator<Item = NoqaCode>) -> String {
rules
.into_iter()
.map(|rule| rule.noqa_code().to_string())
.map(|rule| rule.to_string())
.sorted_unstable()
.dedup()
.join(", ")
@@ -709,7 +748,7 @@ fn collect_rule_codes(rules: impl IntoIterator<Item = Rule>) -> String {
#[expect(clippy::print_stderr)]
fn report_failed_to_converge_error(path: &Path, transformed: &str, messages: &[Message]) {
let codes = collect_rule_codes(messages.iter().filter_map(Message::to_rule));
let codes = collect_rule_codes(messages.iter().filter_map(Message::noqa_code));
if cfg!(debug_assertions) {
eprintln!(
"{}{} Failed to converge after {} iterations in `{}` with rule codes {}:---\n{}\n---",
@@ -745,7 +784,7 @@ fn report_fix_syntax_error(
path: &Path,
transformed: &str,
error: &ParseError,
rules: impl IntoIterator<Item = Rule>,
rules: impl IntoIterator<Item = NoqaCode>,
) {
let codes = collect_rule_codes(rules);
if cfg!(debug_assertions) {

View File

@@ -33,7 +33,7 @@ impl Emitter for AzureEmitter {
line = location.line,
col = location.column,
code = message
.to_noqa_code()
.noqa_code()
.map_or_else(String::new, |code| format!("code={code};")),
body = message.body(),
)?;

View File

@@ -33,7 +33,7 @@ impl Emitter for GithubEmitter {
writer,
"::error title=Ruff{code},file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = message
.to_noqa_code()
.noqa_code()
.map_or_else(String::new, |code| format!(" ({code})")),
file = message.filename(),
row = source_location.line,
@@ -50,7 +50,7 @@ impl Emitter for GithubEmitter {
column = location.column,
)?;
if let Some(code) = message.to_noqa_code() {
if let Some(code) = message.noqa_code() {
write!(writer, " {code}")?;
}

View File

@@ -90,7 +90,7 @@ impl Serialize for SerializedMessages<'_> {
}
fingerprints.insert(message_fingerprint);
let (description, check_name) = if let Some(code) = message.to_noqa_code() {
let (description, check_name) = if let Some(code) = message.noqa_code() {
(message.body().to_string(), code.to_string())
} else {
let description = message.body();

View File

@@ -81,8 +81,8 @@ pub(crate) fn message_to_json_value(message: &Message, context: &EmitterContext)
}
json!({
"code": message.to_noqa_code().map(|code| code.to_string()),
"url": message.to_rule().and_then(|rule| rule.url()),
"code": message.noqa_code().map(|code| code.to_string()),
"url": message.to_url(),
"message": message.body(),
"fix": fix,
"cell": notebook_cell_index,

View File

@@ -59,7 +59,7 @@ impl Emitter for JunitEmitter {
body = message.body()
));
let mut case = TestCase::new(
if let Some(code) = message.to_noqa_code() {
if let Some(code) = message.noqa_code() {
format!("org.ruff.{code}")
} else {
"org.ruff".to_string()

View File

@@ -224,30 +224,22 @@ impl Message {
self.fix().is_some()
}
/// Returns the [`Rule`] corresponding to the diagnostic message.
pub fn to_rule(&self) -> Option<Rule> {
if self.is_syntax_error() {
None
} else {
Some(self.name().parse().expect("Expected a valid rule name"))
}
}
/// Returns the [`NoqaCode`] corresponding to the diagnostic message.
pub fn to_noqa_code(&self) -> Option<NoqaCode> {
pub fn noqa_code(&self) -> Option<NoqaCode> {
self.noqa_code
}
/// Returns the URL for the rule documentation, if it exists.
pub fn to_url(&self) -> Option<String> {
// TODO(brent) Rule::url calls Rule::explanation, which calls ViolationMetadata::explain,
// which when derived (seems always to be the case?) is always `Some`, so I think it's
// pretty safe to inline the Rule::url implementation here, using `self.name()`:
//
// format!("{}/rules/{}", env!("CARGO_PKG_HOMEPAGE"), self.name())
//
// at least in the case of diagnostics, I guess syntax errors will return None
self.to_rule().and_then(|rule| rule.url())
if self.is_syntax_error() {
None
} else {
Some(format!(
"{}/rules/{}",
env!("CARGO_PKG_HOMEPAGE"),
self.name()
))
}
}
/// Returns the filename for the message.

View File

@@ -26,7 +26,7 @@ impl Emitter for PylintEmitter {
message.compute_start_location().line
};
let body = if let Some(code) = message.to_noqa_code() {
let body = if let Some(code) = message.noqa_code() {
format!("[{code}] {body}", body = message.body())
} else {
message.body().to_string()

View File

@@ -71,7 +71,7 @@ fn message_to_rdjson_value(message: &Message) -> Value {
"range": rdjson_range(start_location, end_location),
},
"code": {
"value": message.to_noqa_code().map(|code| code.to_string()),
"value": message.noqa_code().map(|code| code.to_string()),
"url": message.to_url(),
},
"suggestions": rdjson_suggestions(fix.edits(), &source_code),
@@ -84,7 +84,7 @@ fn message_to_rdjson_value(message: &Message) -> Value {
"range": rdjson_range(start_location, end_location),
},
"code": {
"value": message.to_noqa_code().map(|code| code.to_string()),
"value": message.noqa_code().map(|code| code.to_string()),
"url": message.to_url(),
},
})

View File

@@ -8,7 +8,7 @@ use serde_json::json;
use ruff_source_file::OneIndexed;
use crate::VERSION;
use crate::codes::Rule;
use crate::codes::NoqaCode;
use crate::fs::normalize_path;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::{Linter, RuleNamespace};
@@ -27,7 +27,7 @@ impl Emitter for SarifEmitter {
.map(SarifResult::from_message)
.collect::<Result<Vec<_>>>()?;
let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.rule).collect();
let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.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));
@@ -61,13 +61,19 @@ struct SarifRule<'a> {
url: Option<String>,
}
impl From<Rule> for SarifRule<'_> {
fn from(rule: Rule) -> Self {
let code = rule.noqa_code().to_string();
let (linter, _) = Linter::parse_code(&code).unwrap();
impl From<NoqaCode> for SarifRule<'_> {
fn from(code: NoqaCode) -> Self {
let code_str = code.to_string();
// This is a manual re-implementation of Rule::from_code, but we also want the Linter. This
// avoids calling Linter::parse_code twice.
let (linter, suffix) = Linter::parse_code(&code_str).unwrap();
let rule = linter
.all_rules()
.find(|rule| rule.noqa_code().suffix() == suffix)
.expect("Expected a valid noqa code corresponding to a rule");
Self {
name: rule.into(),
code,
code: code_str,
linter: linter.name(),
summary: rule.message_formats()[0],
explanation: rule.explanation(),
@@ -106,7 +112,7 @@ impl Serialize for SarifRule<'_> {
#[derive(Debug)]
struct SarifResult {
rule: Option<Rule>,
code: Option<NoqaCode>,
level: String,
message: String,
uri: String,
@@ -123,7 +129,7 @@ impl SarifResult {
let end_location = message.compute_end_location();
let path = normalize_path(&*message.filename());
Ok(Self {
rule: message.to_rule(),
code: message.noqa_code(),
level: "error".to_string(),
message: message.body().to_string(),
uri: url::Url::from_file_path(&path)
@@ -143,7 +149,7 @@ impl SarifResult {
let end_location = message.compute_end_location();
let path = normalize_path(&*message.filename());
Ok(Self {
rule: message.to_rule(),
code: message.noqa_code(),
level: "error".to_string(),
message: message.body().to_string(),
uri: path.display().to_string(),
@@ -178,7 +184,7 @@ impl Serialize for SarifResult {
}
}
}],
"ruleId": self.rule.map(|rule| rule.noqa_code().to_string()),
"ruleId": self.code.map(|code| code.to_string()),
})
.serialize(serializer)
}

View File

@@ -151,7 +151,7 @@ impl Display for RuleCodeAndBody<'_> {
if let Some(fix) = self.message.fix() {
// Do not display an indicator for inapplicable fixes
if fix.applies(self.unsafe_fixes.required_applicability()) {
if let Some(code) = self.message.to_noqa_code() {
if let Some(code) = self.message.noqa_code() {
write!(f, "{} ", code.to_string().red().bold())?;
}
return write!(
@@ -164,7 +164,7 @@ impl Display for RuleCodeAndBody<'_> {
}
}
if let Some(code) = self.message.to_noqa_code() {
if let Some(code) = self.message.noqa_code() {
write!(
f,
"{code} {body}",
@@ -254,7 +254,7 @@ impl Display for MessageCodeFrame<'_> {
let label = self
.message
.to_noqa_code()
.noqa_code()
.map_or_else(String::new, |code| code.to_string());
let line_start = self.notebook_index.map_or_else(

View File

@@ -12,13 +12,14 @@ use log::warn;
use ruff_python_trivia::{CommentRanges, Cursor, indentation_at_offset};
use ruff_source_file::{LineEnding, LineRanges};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use rustc_hash::FxHashSet;
use crate::Edit;
use crate::Locator;
use crate::codes::NoqaCode;
use crate::fs::relativize_path;
use crate::message::Message;
use crate::registry::{Rule, RuleSet};
use crate::registry::Rule;
use crate::rule_redirects::get_redirect_target;
/// Generates an array of edits that matches the length of `messages`.
@@ -780,7 +781,7 @@ fn build_noqa_edits_by_diagnostic(
if let Some(noqa_edit) = generate_noqa_edit(
comment.directive,
comment.line,
RuleSet::from_rule(comment.rule),
FxHashSet::from_iter([comment.code]),
locator,
line_ending,
) {
@@ -816,7 +817,7 @@ fn build_noqa_edits_by_line<'a>(
offset,
matches
.into_iter()
.map(|NoqaComment { rule, .. }| rule)
.map(|NoqaComment { code, .. }| code)
.collect(),
locator,
line_ending,
@@ -829,7 +830,7 @@ fn build_noqa_edits_by_line<'a>(
struct NoqaComment<'a> {
line: TextSize,
rule: Rule,
code: NoqaCode,
directive: Option<&'a Directive<'a>>,
}
@@ -845,13 +846,11 @@ fn find_noqa_comments<'a>(
// Mark any non-ignored diagnostics.
for message in messages {
let Some(rule) = message.to_rule() else {
let Some(code) = message.noqa_code() else {
comments_by_line.push(None);
continue;
};
let code = rule.noqa_code();
match &exemption {
FileExemption::All(_) => {
// If the file is exempted, don't add any noqa directives.
@@ -900,7 +899,7 @@ fn find_noqa_comments<'a>(
if !codes.includes(code) {
comments_by_line.push(Some(NoqaComment {
line: directive_line.start(),
rule,
code,
directive: Some(directive),
}));
}
@@ -912,7 +911,7 @@ fn find_noqa_comments<'a>(
// There's no existing noqa directive that suppresses the diagnostic.
comments_by_line.push(Some(NoqaComment {
line: locator.line_start(noqa_offset),
rule,
code,
directive: None,
}));
}
@@ -922,7 +921,7 @@ fn find_noqa_comments<'a>(
struct NoqaEdit<'a> {
edit_range: TextRange,
rules: RuleSet,
noqa_codes: FxHashSet<NoqaCode>,
codes: Option<&'a Codes<'a>>,
line_ending: LineEnding,
}
@@ -941,18 +940,15 @@ impl NoqaEdit<'_> {
Some(codes) => {
push_codes(
writer,
self.rules
self.noqa_codes
.iter()
.map(|rule| rule.noqa_code().to_string())
.map(ToString::to_string)
.chain(codes.iter().map(ToString::to_string))
.sorted_unstable(),
);
}
None => {
push_codes(
writer,
self.rules.iter().map(|rule| rule.noqa_code().to_string()),
);
push_codes(writer, self.noqa_codes.iter().map(ToString::to_string));
}
}
write!(writer, "{}", self.line_ending.as_str()).unwrap();
@@ -968,7 +964,7 @@ impl Ranged for NoqaEdit<'_> {
fn generate_noqa_edit<'a>(
directive: Option<&'a Directive>,
offset: TextSize,
rules: RuleSet,
noqa_codes: FxHashSet<NoqaCode>,
locator: &Locator,
line_ending: LineEnding,
) -> Option<NoqaEdit<'a>> {
@@ -997,7 +993,7 @@ fn generate_noqa_edit<'a>(
Some(NoqaEdit {
edit_range,
rules,
noqa_codes,
codes,
line_ending,
})

View File

@@ -7,17 +7,6 @@
use crate::settings::LinterSettings;
// https://github.com/astral-sh/ruff/issues/17412
// https://github.com/astral-sh/ruff/issues/11934
pub(crate) const fn is_semantic_errors_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/16429
pub(crate) const fn is_unsupported_syntax_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
pub(crate) const fn is_py314_support_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
@@ -29,23 +18,11 @@ pub(crate) const fn is_full_path_match_source_strategy_enabled(settings: &Linter
// Rule-specific behavior
// https://github.com/astral-sh/ruff/pull/17136
pub(crate) const fn is_shell_injection_only_trusted_input_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/15541
pub(crate) const fn is_suspicious_function_reference_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/7501
pub(crate) const fn is_bool_subtype_of_annotation_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/10759
pub(crate) const fn is_comprehension_with_min_max_sum_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
@@ -63,21 +40,11 @@ pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSet
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/12676
pub(crate) const fn is_fix_future_annotations_in_stub_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11074
pub(crate) const fn is_only_add_return_none_at_end_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/12796
pub(crate) const fn is_simplify_ternary_to_binary_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/16719
pub(crate) const fn is_fix_manual_dict_comprehension_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
@@ -104,13 +71,6 @@ pub(crate) const fn is_unicode_to_unicode_confusables_enabled(settings: &LinterS
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/17078
pub(crate) const fn is_support_slices_in_literal_concatenation_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/11370
pub(crate) const fn is_undefined_export_in_dunder_init_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
@@ -121,16 +81,9 @@ pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) ->
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/17061
pub(crate) const fn is_check_file_level_directives_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/17644
pub(crate) const fn is_readlines_in_for_fix_safe_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
pub(crate) const fn multiple_with_statements_fix_safe_enabled(settings: &LinterSettings) -> bool {
// https://github.com/astral-sh/ruff/pull/18208
pub(crate) const fn is_multiple_with_statements_fix_safe_enabled(
settings: &LinterSettings,
) -> bool {
settings.preview.is_enabled()
}

View File

@@ -1,6 +1,7 @@
//! Remnant of the registry of all [`Rule`] implementations, now it's reexporting from codes.rs
//! with some helper symbols
use ruff_db::diagnostic::LintName;
use strum_macros::EnumIter;
pub use codes::Rule;
@@ -348,9 +349,18 @@ impl Rule {
/// Return the URL for the rule documentation, if it exists.
pub fn url(&self) -> Option<String> {
self.explanation()
.is_some()
.then(|| format!("{}/rules/{}", env!("CARGO_PKG_HOMEPAGE"), self.as_ref()))
self.explanation().is_some().then(|| {
format!(
"{}/rules/{name}",
env!("CARGO_PKG_HOMEPAGE"),
name = self.name()
)
})
}
pub fn name(&self) -> LintName {
let name: &'static str = self.into();
LintName::of(name)
}
}
@@ -421,7 +431,7 @@ pub mod clap_completion {
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(Rule::iter().map(|rule| {
let name = rule.noqa_code().to_string();
let help = rule.as_ref().to_string();
let help = rule.name().as_str();
PossibleValue::new(name).help(help)
})))
}
@@ -443,7 +453,7 @@ mod tests {
assert!(
rule.explanation().is_some(),
"Rule {} is missing documentation",
rule.as_ref()
rule.name()
);
}
}
@@ -460,10 +470,10 @@ mod tests {
.collect();
for rule in Rule::iter() {
let rule_name = rule.as_ref();
let rule_name = rule.name();
for pattern in &patterns {
assert!(
!pattern.matches(rule_name),
!pattern.matches(&rule_name),
"{rule_name} does not match naming convention, see CONTRIBUTING.md"
);
}

View File

@@ -302,9 +302,8 @@ impl Display for RuleSet {
} else {
writeln!(f, "[")?;
for rule in self {
let name = rule.as_ref();
let code = rule.noqa_code();
writeln!(f, "\t{name} ({code}),")?;
writeln!(f, "\t{name} ({code}),", name = rule.name())?;
}
write!(f, "]")?;
}

View File

@@ -485,8 +485,7 @@ pub mod clap_completion {
prefix.linter().common_prefix(),
prefix.short_code()
);
let name: &'static str = rule.into();
return Some(PossibleValue::new(code).help(name));
return Some(PossibleValue::new(code).help(rule.name().as_str()));
}
None

View File

@@ -3,7 +3,6 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
@@ -18,7 +17,7 @@ mod tests {
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
#[test_case(Rule::FastApiUnusedPathParameter, Path::new("FAST003.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("fastapi").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),
@@ -32,7 +31,7 @@ mod tests {
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))]
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}_py38", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("fastapi").join(path).as_path(),
&settings::LinterSettings {

View File

@@ -104,7 +104,6 @@ mod tests {
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -7,7 +7,6 @@ use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::Violation;
use crate::preview::is_shell_injection_only_trusted_input_enabled;
use crate::{
checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal,
};
@@ -325,9 +324,7 @@ pub(crate) fn shell_injection(checker: &Checker, call: &ast::ExprCall) {
}
// S603
_ => {
if !is_trusted_input(arg)
|| !is_shell_injection_only_trusted_input_enabled(checker.settings)
{
if !is_trusted_input(arg) {
if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) {
checker.report_diagnostic(
SubprocessWithoutShellEqualsTrue,

View File

@@ -106,74 +106,6 @@ S603.py:21:1: S603 `subprocess` call: check for execution of untrusted input
23 | # Literals are fine, they're trusted.
|
S603.py:24:1: S603 `subprocess` call: check for execution of untrusted input
|
23 | # Literals are fine, they're trusted.
24 | run("true")
| ^^^ S603
25 | Popen(["true"])
26 | Popen("true", shell=False)
|
S603.py:25:1: S603 `subprocess` call: check for execution of untrusted input
|
23 | # Literals are fine, they're trusted.
24 | run("true")
25 | Popen(["true"])
| ^^^^^ S603
26 | Popen("true", shell=False)
27 | call("true", shell=False)
|
S603.py:26:1: S603 `subprocess` call: check for execution of untrusted input
|
24 | run("true")
25 | Popen(["true"])
26 | Popen("true", shell=False)
| ^^^^^ S603
27 | call("true", shell=False)
28 | check_call("true", shell=False)
|
S603.py:27:1: S603 `subprocess` call: check for execution of untrusted input
|
25 | Popen(["true"])
26 | Popen("true", shell=False)
27 | call("true", shell=False)
| ^^^^ S603
28 | check_call("true", shell=False)
29 | check_output("true", shell=False)
|
S603.py:28:1: S603 `subprocess` call: check for execution of untrusted input
|
26 | Popen("true", shell=False)
27 | call("true", shell=False)
28 | check_call("true", shell=False)
| ^^^^^^^^^^ S603
29 | check_output("true", shell=False)
30 | run("true", shell=False)
|
S603.py:29:1: S603 `subprocess` call: check for execution of untrusted input
|
27 | call("true", shell=False)
28 | check_call("true", shell=False)
29 | check_output("true", shell=False)
| ^^^^^^^^^^^^ S603
30 | run("true", shell=False)
|
S603.py:30:1: S603 `subprocess` call: check for execution of untrusted input
|
28 | check_call("true", shell=False)
29 | check_output("true", shell=False)
30 | run("true", shell=False)
| ^^^ S603
31 |
32 | # Not through assignments though.
|
S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
|
32 | # Not through assignments though.
@@ -184,15 +116,6 @@ S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
36 | # Instant named expressions are fine.
|
S603.py:37:1: S603 `subprocess` call: check for execution of untrusted input
|
36 | # Instant named expressions are fine.
37 | run(c := "true")
| ^^^ S603
38 |
39 | # But non-instant are not.
|
S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
39 | # But non-instant are not.
@@ -200,20 +123,3 @@ S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
41 | run(e)
| ^^^ S603
|
S603.py:46:1: S603 `subprocess` call: check for execution of untrusted input
|
44 | # https://github.com/astral-sh/ruff/issues/17798
45 | # Tuple literals are trusted
46 | check_output(("literal", "cmd", "using", "tuple"), text=True)
| ^^^^^^^^^^^^ S603
47 | Popen(("literal", "cmd", "using", "tuple"))
|
S603.py:47:1: S603 `subprocess` call: check for execution of untrusted input
|
45 | # Tuple literals are trusted
46 | check_output(("literal", "cmd", "using", "tuple"), text=True)
47 | Popen(("literal", "cmd", "using", "tuple"))
| ^^^^^ S603
|

View File

@@ -1,125 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S603.py:5:1: S603 `subprocess` call: check for execution of untrusted input
|
3 | # Different Popen wrappers are checked.
4 | a = input()
5 | Popen(a, shell=False)
| ^^^^^ S603
6 | call(a, shell=False)
7 | check_call(a, shell=False)
|
S603.py:6:1: S603 `subprocess` call: check for execution of untrusted input
|
4 | a = input()
5 | Popen(a, shell=False)
6 | call(a, shell=False)
| ^^^^ S603
7 | check_call(a, shell=False)
8 | check_output(a, shell=False)
|
S603.py:7:1: S603 `subprocess` call: check for execution of untrusted input
|
5 | Popen(a, shell=False)
6 | call(a, shell=False)
7 | check_call(a, shell=False)
| ^^^^^^^^^^ S603
8 | check_output(a, shell=False)
9 | run(a, shell=False)
|
S603.py:8:1: S603 `subprocess` call: check for execution of untrusted input
|
6 | call(a, shell=False)
7 | check_call(a, shell=False)
8 | check_output(a, shell=False)
| ^^^^^^^^^^^^ S603
9 | run(a, shell=False)
|
S603.py:9:1: S603 `subprocess` call: check for execution of untrusted input
|
7 | check_call(a, shell=False)
8 | check_output(a, shell=False)
9 | run(a, shell=False)
| ^^^ S603
10 |
11 | # Falsey values are treated as false.
|
S603.py:12:1: S603 `subprocess` call: check for execution of untrusted input
|
11 | # Falsey values are treated as false.
12 | Popen(a, shell=0)
| ^^^^^ S603
13 | Popen(a, shell=[])
14 | Popen(a, shell={})
|
S603.py:13:1: S603 `subprocess` call: check for execution of untrusted input
|
11 | # Falsey values are treated as false.
12 | Popen(a, shell=0)
13 | Popen(a, shell=[])
| ^^^^^ S603
14 | Popen(a, shell={})
15 | Popen(a, shell=None)
|
S603.py:14:1: S603 `subprocess` call: check for execution of untrusted input
|
12 | Popen(a, shell=0)
13 | Popen(a, shell=[])
14 | Popen(a, shell={})
| ^^^^^ S603
15 | Popen(a, shell=None)
|
S603.py:15:1: S603 `subprocess` call: check for execution of untrusted input
|
13 | Popen(a, shell=[])
14 | Popen(a, shell={})
15 | Popen(a, shell=None)
| ^^^^^ S603
16 |
17 | # Unknown values are treated as falsey.
|
S603.py:18:1: S603 `subprocess` call: check for execution of untrusted input
|
17 | # Unknown values are treated as falsey.
18 | Popen(a, shell=True if True else False)
| ^^^^^ S603
19 |
20 | # No value is also caught.
|
S603.py:21:1: S603 `subprocess` call: check for execution of untrusted input
|
20 | # No value is also caught.
21 | Popen(a)
| ^^^^^ S603
22 |
23 | # Literals are fine, they're trusted.
|
S603.py:34:1: S603 `subprocess` call: check for execution of untrusted input
|
32 | # Not through assignments though.
33 | cmd = ["true"]
34 | run(cmd)
| ^^^ S603
35 |
36 | # Instant named expressions are fine.
|
S603.py:41:1: S603 `subprocess` call: check for execution of untrusted input
|
39 | # But non-instant are not.
40 | (e := "echo")
41 | run(e)
| ^^^ S603
|

View File

@@ -12,7 +12,6 @@ mod tests {
use crate::registry::Rule;
use crate::settings::LinterSettings;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_messages, settings};
@@ -29,24 +28,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_boolean_trap").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_allowed_callable() -> Result<()> {
let diagnostics = test_path(

View File

@@ -7,12 +7,12 @@ use ruff_python_semantic::analyze::visibility;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_bool_subtype_of_annotation_enabled;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
/// as determined by the presence of a `bool` type hint.
/// as determined by the presence of a type hint containing `bool` as an
/// evident subtype - e.g. `bool`, `bool | int`, `typing.Optional[bool]`, etc.
///
/// ## Why is this bad?
/// Calling a function with boolean positional arguments is confusing as the
@@ -30,9 +30,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// Dunder methods that define operators are exempt from this rule, as are
/// setters and `@override` definitions.
///
/// In [preview], this rule will also flag annotations that include boolean
/// variants, like `bool | int`.
///
/// ## Example
///
/// ```python
@@ -96,8 +93,6 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
#[derive(ViolationMetadata)]
pub(crate) struct BooleanTypeHintPositionalArgument;
@@ -128,14 +123,8 @@ pub(crate) fn boolean_type_hint_positional_argument(
let Some(annotation) = parameter.annotation() else {
continue;
};
if is_bool_subtype_of_annotation_enabled(checker.settings) {
if !match_annotation_to_complex_bool(annotation, checker.semantic()) {
continue;
}
} else {
if !match_annotation_to_literal_bool(annotation) {
continue;
}
if !match_annotation_to_complex_bool(annotation, checker.semantic()) {
continue;
}
// Allow Boolean type hints in setters.
@@ -161,17 +150,6 @@ pub(crate) fn boolean_type_hint_positional_argument(
}
}
/// Returns `true` if the annotation is a boolean type hint (e.g., `bool`).
fn match_annotation_to_literal_bool(annotation: &Expr) -> bool {
match annotation {
// Ex) `True`
Expr::Name(name) => &name.id == "bool",
// Ex) `"True"`
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "bool",
_ => false,
}
}
/// Returns `true` if the annotation is a boolean type hint (e.g., `bool`), or a type hint that
/// includes boolean as a variant (e.g., `bool | int`).
fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel) -> bool {

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs
snapshot_kind: text
---
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
@@ -89,3 +88,17 @@ FBT.py:90:19: FBT001 Boolean-typed positional argument in function definition
| ^^^^^ FBT001
91 | pass
|
FBT.py:100:10: FBT001 Boolean-typed positional argument in function definition
|
100 | def func(x: Union[list, Optional[int | str | float | bool]]):
| ^ FBT001
101 | pass
|
FBT.py:104:10: FBT001 Boolean-typed positional argument in function definition
|
104 | def func(x: bool | str):
| ^ FBT001
105 | pass
|

View File

@@ -1,105 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_boolean_trap/mod.rs
snapshot_kind: text
---
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
2 | posonly_nohint,
3 | posonly_nonboolhint: int,
4 | posonly_boolhint: bool,
| ^^^^^^^^^^^^^^^^ FBT001
5 | posonly_boolstrhint: "bool",
6 | /,
|
FBT.py:5:5: FBT001 Boolean-typed positional argument in function definition
|
3 | posonly_nonboolhint: int,
4 | posonly_boolhint: bool,
5 | posonly_boolstrhint: "bool",
| ^^^^^^^^^^^^^^^^^^^ FBT001
6 | /,
7 | offset,
|
FBT.py:10:5: FBT001 Boolean-typed positional argument in function definition
|
8 | posorkw_nonvalued_nohint,
9 | posorkw_nonvalued_nonboolhint: int,
10 | posorkw_nonvalued_boolhint: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
11 | posorkw_nonvalued_boolstrhint: "bool",
12 | posorkw_boolvalued_nohint=True,
|
FBT.py:11:5: FBT001 Boolean-typed positional argument in function definition
|
9 | posorkw_nonvalued_nonboolhint: int,
10 | posorkw_nonvalued_boolhint: bool,
11 | posorkw_nonvalued_boolstrhint: "bool",
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
|
FBT.py:14:5: FBT001 Boolean-typed positional argument in function definition
|
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
16 | posorkw_nonboolvalued_nohint=1,
|
FBT.py:15:5: FBT001 Boolean-typed positional argument in function definition
|
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
16 | posorkw_nonboolvalued_nohint=1,
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
FBT.py:18:5: FBT001 Boolean-typed positional argument in function definition
|
16 | posorkw_nonboolvalued_nohint=1,
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
18 | posorkw_nonboolvalued_boolhint: bool = 3,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
20 | *,
|
FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
18 | posorkw_nonboolvalued_boolhint: bool = 3,
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
20 | *,
21 | kwonly_nonvalued_nohint,
|
FBT.py:90:19: FBT001 Boolean-typed positional argument in function definition
|
89 | # FBT001: Boolean positional arg in function definition
90 | def foo(self, value: bool) -> None:
| ^^^^^ FBT001
91 | pass
|
FBT.py:100:10: FBT001 Boolean-typed positional argument in function definition
|
100 | def func(x: Union[list, Optional[int | str | float | bool]]):
| ^ FBT001
101 | pass
|
FBT.py:104:10: FBT001 Boolean-typed positional argument in function definition
|
104 | def func(x: bool | str):
| ^ FBT001
105 | pass
|

View File

@@ -16,7 +16,7 @@ mod tests {
#[test_case(Rule::LineContainsTodo; "T003")]
#[test_case(Rule::LineContainsXxx; "T004")]
fn rules(rule_code: Rule) -> Result<()> {
let snapshot = format!("{}_T00.py", rule_code.as_ref());
let snapshot = format!("{}_T00.py", rule_code.name());
let diagnostics = test_path(
Path::new("flake8_fixme/T00.py"),
&settings::LinterSettings::for_rule(rule_code),

View File

@@ -29,7 +29,7 @@ mod tests {
#[test_case(Rule::FormatInGetTextFuncCall, Path::new("INT002.py"))]
#[test_case(Rule::PrintfInGetTextFuncCall, Path::new("INT003.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_gettext").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),

View File

@@ -11,7 +11,6 @@ mod tests {
use crate::registry::Rule;
use crate::rules::pep8_naming;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_messages, settings};
@@ -172,22 +171,4 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_pyi").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -3,7 +3,7 @@ use ruff_python_ast::StmtImportFrom;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use crate::{Fix, FixAvailability, Violation};
use crate::{checkers::ast::Checker, fix, preview::is_fix_future_annotations_in_stub_enabled};
use crate::{checkers::ast::Checker, fix};
/// ## What it does
/// Checks for the presence of the `from __future__ import annotations` import
@@ -55,20 +55,18 @@ pub(crate) fn from_future_import(checker: &Checker, target: &StmtImportFrom) {
let mut diagnostic = checker.report_diagnostic(FutureAnnotationsInStub, *range);
if is_fix_future_annotations_in_stub_enabled(checker.settings) {
let stmt = checker.semantic().current_statement();
let stmt = checker.semantic().current_statement();
diagnostic.try_set_fix(|| {
let edit = fix::edits::remove_unused_imports(
std::iter::once("annotations"),
stmt,
None,
checker.locator(),
checker.stylist(),
checker.indexer(),
)?;
diagnostic.try_set_fix(|| {
let edit = fix::edits::remove_unused_imports(
std::iter::once("annotations"),
stmt,
None,
checker.locator(),
checker.stylist(),
checker.indexer(),
)?;
Ok(Fix::safe_edit(edit))
});
}
Ok(Fix::safe_edit(edit))
});
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
PYI044.pyi:2:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
1 | # Bad import.
2 | from __future__ import annotations # PYI044.
@@ -10,7 +10,14 @@ PYI044.pyi:2:1: PYI044 `from __future__ import annotations` has no effect in stu
|
= help: Remove `from __future__ import annotations`
PYI044.pyi:3:1: PYI044 `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
Safe fix
1 1 | # Bad import.
2 |-from __future__ import annotations # PYI044.
3 2 | from __future__ import annotations, with_statement # PYI044.
4 3 |
5 4 | # Good imports.
PYI044.pyi:3:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
1 | # Bad import.
2 | from __future__ import annotations # PYI044.
@@ -20,3 +27,12 @@ PYI044.pyi:3:1: PYI044 `from __future__ import annotations` has no effect in stu
5 | # Good imports.
|
= help: Remove `from __future__ import annotations`
Safe fix
1 1 | # Bad import.
2 2 | from __future__ import annotations # PYI044.
3 |-from __future__ import annotations, with_statement # PYI044.
3 |+from __future__ import with_statement # PYI044.
4 4 |
5 5 | # Good imports.
6 6 | from __future__ import with_statement

View File

@@ -1,38 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI044.pyi:2:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
1 | # Bad import.
2 | from __future__ import annotations # PYI044.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044
3 | from __future__ import annotations, with_statement # PYI044.
|
= help: Remove `from __future__ import annotations`
Safe fix
1 1 | # Bad import.
2 |-from __future__ import annotations # PYI044.
3 2 | from __future__ import annotations, with_statement # PYI044.
4 3 |
5 4 | # Good imports.
PYI044.pyi:3:1: PYI044 [*] `from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics
|
1 | # Bad import.
2 | from __future__ import annotations # PYI044.
3 | from __future__ import annotations, with_statement # PYI044.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI044
4 |
5 | # Good imports.
|
= help: Remove `from __future__ import annotations`
Safe fix
1 1 | # Bad import.
2 2 | from __future__ import annotations # PYI044.
3 |-from __future__ import annotations, with_statement # PYI044.
3 |+from __future__ import with_statement # PYI044.
4 4 |
5 5 | # Good imports.
6 6 | from __future__ import with_statement

View File

@@ -3,7 +3,6 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
@@ -15,7 +14,7 @@ mod tests {
#[test_case(Rule::UnnecessaryParenOnRaiseException, Path::new("RSE102.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_raise").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),

View File

@@ -4,7 +4,6 @@ pub mod settings;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use crate::registry::Rule;
@@ -18,7 +17,7 @@ mod tests {
#[test_case(Rule::PrivateMemberAccess, Path::new("SLF001.py"))]
#[test_case(Rule::PrivateMemberAccess, Path::new("SLF001_1.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_self").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),

View File

@@ -58,7 +58,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))]
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -10,7 +10,7 @@ use super::fix_with;
use crate::Fix;
use crate::checkers::ast::Checker;
use crate::fix::edits::fits;
use crate::preview::multiple_with_statements_fix_safe_enabled;
use crate::preview::is_multiple_with_statements_fix_safe_enabled;
use crate::{FixAvailability, Violation};
/// ## What it does
@@ -195,7 +195,7 @@ pub(crate) fn multiple_with_statements(
checker.settings.tab_size,
)
}) {
if multiple_with_statements_fix_safe_enabled(checker.settings) {
if is_multiple_with_statements_fix_safe_enabled(checker.settings) {
Ok(Some(Fix::safe_edit(edit)))
} else {
Ok(Some(Fix::unsafe_edit(edit)))

View File

@@ -7,13 +7,14 @@ use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::edits::fits;
use crate::preview::is_simplify_ternary_to_binary_enabled;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Check for `if`-`else`-blocks that can be replaced with a ternary operator.
/// Moreover, in [preview], check if these ternary expressions can be
/// further simplified to binary expressions.
/// Check for `if`-`else`-blocks that can be replaced with a ternary
/// or binary operator.
///
/// The lint is suppressed if the suggested replacement would exceed
/// the maximum line length configured in [pycodestyle.max-line-length].
///
/// ## Why is this bad?
/// `if`-`else`-blocks that assign a value to a variable in both branches can
@@ -33,7 +34,7 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// bar = x if foo else y
/// ```
///
/// Or, in [preview]:
/// Or:
///
/// ```python
/// if cond:
@@ -57,8 +58,8 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ## References
/// - [Python documentation: Conditional expressions](https://docs.python.org/3/reference/expressions.html#conditional-expressions)
///
/// [preview]: https://docs.astral.sh/ruff/preview/
/// [code coverage]: https://github.com/nedbat/coveragepy/issues/509
/// [pycodestyle.max-line-length]: https://docs.astral.sh/ruff/settings/#lint_pycodestyle_max-line-length
#[derive(ViolationMetadata)]
pub(crate) struct IfElseBlockInsteadOfIfExp {
/// The ternary or binary expression to replace the `if`-`else`-block.
@@ -183,16 +184,12 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast::
//
// The match statement below implements the following
// logic:
// - If `test == body_value` and preview enabled, replace with `target_var = test or else_value`
// - If `test == not body_value` and preview enabled, replace with `target_var = body_value and else_value`
// - If `not test == body_value` and preview enabled, replace with `target_var = body_value and else_value`
// - If `test == body_value`, replace with `target_var = test or else_value`
// - If `test == not body_value`, replace with `target_var = body_value and else_value`
// - If `not test == body_value`, replace with `target_var = body_value and else_value`
// - Otherwise, replace with `target_var = body_value if test else else_value`
let (contents, assignment_kind) = match (
is_simplify_ternary_to_binary_enabled(checker.settings),
test,
body_value,
) {
(true, test_node, body_node)
let (contents, assignment_kind) = match (test, body_value) {
(test_node, body_node)
if ComparableExpr::from(test_node) == ComparableExpr::from(body_node)
&& !contains_effect(test_node, |id| checker.semantic().has_builtin_binding(id)) =>
{
@@ -200,7 +197,7 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &Checker, stmt_if: &ast::
let binary = assignment_binary_or(target_var, body_value, else_value);
(checker.generator().stmt(&binary), AssignmentKind::Binary)
}
(true, test_node, body_node)
(test_node, body_node)
if (test_node.as_unary_op_expr().is_some_and(|op_expr| {
op_expr.op.is_not()
&& ComparableExpr::from(&op_expr.operand) == ComparableExpr::from(body_node)

View File

@@ -118,7 +118,7 @@ SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `
|
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_cond` instead of `if`-`else`-block
SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block
|
139 | # SIM108 - should suggest
140 | # z = cond or other_cond
@@ -130,7 +130,7 @@ SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_co
145 |
146 | # SIM108 - should suggest
|
= help: Replace `if`-`else`-block with `z = cond if cond else other_cond`
= help: Replace `if`-`else`-block with `z = cond or other_cond`
Unsafe fix
138 138 |
@@ -140,12 +140,12 @@ SIM108.py:141:1: SIM108 [*] Use ternary operator `z = cond if cond else other_co
142 |- z = cond
143 |-else:
144 |- z = other_cond
141 |+z = cond if cond else other_cond
141 |+z = cond or other_cond
145 142 |
146 143 | # SIM108 - should suggest
147 144 | # z = cond and other_cond
SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else other_cond` instead of `if`-`else`-block
SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block
|
146 | # SIM108 - should suggest
147 | # z = cond and other_cond
@@ -157,7 +157,7 @@ SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else othe
152 |
153 | # SIM108 - should suggest
|
= help: Replace `if`-`else`-block with `z = cond if not cond else other_cond`
= help: Replace `if`-`else`-block with `z = cond and other_cond`
Unsafe fix
145 145 |
@@ -167,12 +167,12 @@ SIM108.py:148:1: SIM108 [*] Use ternary operator `z = cond if not cond else othe
149 |- z = cond
150 |-else:
151 |- z = other_cond
148 |+z = cond if not cond else other_cond
148 |+z = cond and other_cond
152 149 |
153 150 | # SIM108 - should suggest
154 151 | # z = not cond and other_cond
SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else other_cond` instead of `if`-`else`-block
SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block
|
153 | # SIM108 - should suggest
154 | # z = not cond and other_cond
@@ -184,7 +184,7 @@ SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else othe
159 |
160 | # SIM108 does not suggest
|
= help: Replace `if`-`else`-block with `z = not cond if cond else other_cond`
= help: Replace `if`-`else`-block with `z = not cond and other_cond`
Unsafe fix
152 152 |
@@ -194,7 +194,7 @@ SIM108.py:155:1: SIM108 [*] Use ternary operator `z = not cond if cond else othe
156 |- z = not cond
157 |-else:
158 |- z = other_cond
155 |+z = not cond if cond else other_cond
155 |+z = not cond and other_cond
159 156 |
160 157 | # SIM108 does not suggest
161 158 | # a binary option in these cases,

View File

@@ -1,382 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM108.py:2:1: SIM108 [*] Use ternary operator `b = c if a else d` instead of `if`-`else`-block
|
1 | # SIM108
2 | / if a:
3 | | b = c
4 | | else:
5 | | b = d
| |_________^ SIM108
6 |
7 | # OK
|
= help: Replace `if`-`else`-block with `b = c if a else d`
Unsafe fix
1 1 | # SIM108
2 |-if a:
3 |- b = c
4 |-else:
5 |- b = d
2 |+b = c if a else d
6 3 |
7 4 | # OK
8 5 | b = c if a else d
SIM108.py:30:5: SIM108 [*] Use ternary operator `b = 1 if a else 2` instead of `if`-`else`-block
|
28 | pass
29 | else:
30 | / if a:
31 | | b = 1
32 | | else:
33 | | b = 2
| |_____________^ SIM108
|
= help: Replace `if`-`else`-block with `b = 1 if a else 2`
Unsafe fix
27 27 | if True:
28 28 | pass
29 29 | else:
30 |- if a:
31 |- b = 1
32 |- else:
33 |- b = 2
30 |+ b = 1 if a else 2
34 31 |
35 32 |
36 33 | import sys
SIM108.py:58:1: SIM108 Use ternary operator `abc = x if x > 0 else -x` instead of `if`-`else`-block
|
57 | # SIM108 (without fix due to comments)
58 | / if x > 0:
59 | | # test test
60 | | abc = x
61 | | else:
62 | | # test test test
63 | | abc = -x
| |____________^ SIM108
|
= help: Replace `if`-`else`-block with `abc = x if x > 0 else -x`
SIM108.py:82:1: SIM108 [*] Use ternary operator `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"` instead of `if`-`else`-block
|
81 | # SIM108
82 | / if a:
83 | | b = "cccccccccccccccccccccccccccccccccß"
84 | | else:
85 | | b = "ddddddddddddddddddddddddddddddddd💣"
| |_____________________________________________^ SIM108
|
= help: Replace `if`-`else`-block with `b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"`
Unsafe fix
79 79 |
80 80 |
81 81 | # SIM108
82 |-if a:
83 |- b = "cccccccccccccccccccccccccccccccccß"
84 |-else:
85 |- b = "ddddddddddddddddddddddddddddddddd💣"
82 |+b = "cccccccccccccccccccccccccccccccccß" if a else "ddddddddddddddddddddddddddddddddd💣"
86 83 |
87 84 |
88 85 | # OK (too long)
SIM108.py:105:1: SIM108 Use ternary operator `exitcode = 0 if True else 1` instead of `if`-`else`-block
|
104 | # SIM108 (without fix due to trailing comment)
105 | / if True:
106 | | exitcode = 0
107 | | else:
108 | | exitcode = 1 # Trailing comment
| |________________^ SIM108
|
= help: Replace `if`-`else`-block with `exitcode = 0 if True else 1`
SIM108.py:112:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block
|
111 | # SIM108
112 | / if True: x = 3 # Foo
113 | | else: x = 5
| |___________^ SIM108
|
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
SIM108.py:117:1: SIM108 Use ternary operator `x = 3 if True else 5` instead of `if`-`else`-block
|
116 | # SIM108
117 | / if True: # Foo
118 | | x = 3
119 | | else:
120 | | x = 5
| |_________^ SIM108
|
= help: Replace `if`-`else`-block with `x = 3 if True else 5`
SIM108.py:141:1: SIM108 [*] Use binary operator `z = cond or other_cond` instead of `if`-`else`-block
|
139 | # SIM108 - should suggest
140 | # z = cond or other_cond
141 | / if cond:
142 | | z = cond
143 | | else:
144 | | z = other_cond
| |__________________^ SIM108
145 |
146 | # SIM108 - should suggest
|
= help: Replace `if`-`else`-block with `z = cond or other_cond`
Unsafe fix
138 138 |
139 139 | # SIM108 - should suggest
140 140 | # z = cond or other_cond
141 |-if cond:
142 |- z = cond
143 |-else:
144 |- z = other_cond
141 |+z = cond or other_cond
145 142 |
146 143 | # SIM108 - should suggest
147 144 | # z = cond and other_cond
SIM108.py:148:1: SIM108 [*] Use binary operator `z = cond and other_cond` instead of `if`-`else`-block
|
146 | # SIM108 - should suggest
147 | # z = cond and other_cond
148 | / if not cond:
149 | | z = cond
150 | | else:
151 | | z = other_cond
| |__________________^ SIM108
152 |
153 | # SIM108 - should suggest
|
= help: Replace `if`-`else`-block with `z = cond and other_cond`
Unsafe fix
145 145 |
146 146 | # SIM108 - should suggest
147 147 | # z = cond and other_cond
148 |-if not cond:
149 |- z = cond
150 |-else:
151 |- z = other_cond
148 |+z = cond and other_cond
152 149 |
153 150 | # SIM108 - should suggest
154 151 | # z = not cond and other_cond
SIM108.py:155:1: SIM108 [*] Use binary operator `z = not cond and other_cond` instead of `if`-`else`-block
|
153 | # SIM108 - should suggest
154 | # z = not cond and other_cond
155 | / if cond:
156 | | z = not cond
157 | | else:
158 | | z = other_cond
| |__________________^ SIM108
159 |
160 | # SIM108 does not suggest
|
= help: Replace `if`-`else`-block with `z = not cond and other_cond`
Unsafe fix
152 152 |
153 153 | # SIM108 - should suggest
154 154 | # z = not cond and other_cond
155 |-if cond:
156 |- z = not cond
157 |-else:
158 |- z = other_cond
155 |+z = not cond and other_cond
159 156 |
160 157 | # SIM108 does not suggest
161 158 | # a binary option in these cases,
SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` instead of `if`-`else`-block
|
165 | # (Of course, these specific expressions
166 | # should be simplified for other reasons...)
167 | / if True:
168 | | z = 1
169 | | else:
170 | | z = other
| |_____________^ SIM108
171 |
172 | if False:
|
= help: Replace `if`-`else`-block with `z = 1 if True else other`
Unsafe fix
164 164 | # so, e.g. `True == 1`.
165 165 | # (Of course, these specific expressions
166 166 | # should be simplified for other reasons...)
167 |-if True:
168 |- z = 1
169 |-else:
170 |- z = other
167 |+z = 1 if True else other
171 168 |
172 169 | if False:
173 170 | z = 1
SIM108.py:172:1: SIM108 [*] Use ternary operator `z = 1 if False else other` instead of `if`-`else`-block
|
170 | z = other
171 |
172 | / if False:
173 | | z = 1
174 | | else:
175 | | z = other
| |_____________^ SIM108
176 |
177 | if 1:
|
= help: Replace `if`-`else`-block with `z = 1 if False else other`
Unsafe fix
169 169 | else:
170 170 | z = other
171 171 |
172 |-if False:
173 |- z = 1
174 |-else:
175 |- z = other
172 |+z = 1 if False else other
176 173 |
177 174 | if 1:
178 175 | z = True
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
|
175 | z = other
176 |
177 | / if 1:
178 | | z = True
179 | | else:
180 | | z = other
| |_____________^ SIM108
181 |
182 | # SIM108 does not suggest a binary option in this
|
= help: Replace `if`-`else`-block with `z = True if 1 else other`
Unsafe fix
174 174 | else:
175 175 | z = other
176 176 |
177 |-if 1:
178 |- z = True
179 |-else:
180 |- z = other
177 |+z = True if 1 else other
181 178 |
182 179 | # SIM108 does not suggest a binary option in this
183 180 | # case, since we'd be reducing the number of calls
SIM108.py:185:1: SIM108 [*] Use ternary operator `z = foo() if foo() else other` instead of `if`-`else`-block
|
183 | # case, since we'd be reducing the number of calls
184 | # from Two to one.
185 | / if foo():
186 | | z = foo()
187 | | else:
188 | | z = other
| |_____________^ SIM108
189 |
190 | # SIM108 does not suggest a binary option in this
|
= help: Replace `if`-`else`-block with `z = foo() if foo() else other`
Unsafe fix
182 182 | # SIM108 does not suggest a binary option in this
183 183 | # case, since we'd be reducing the number of calls
184 184 | # from Two to one.
185 |-if foo():
186 |- z = foo()
187 |-else:
188 |- z = other
185 |+z = foo() if foo() else other
189 186 |
190 187 | # SIM108 does not suggest a binary option in this
191 188 | # case, since we'd be reducing the number of calls
SIM108.py:193:1: SIM108 [*] Use ternary operator `z = not foo() if foo() else other` instead of `if`-`else`-block
|
191 | # case, since we'd be reducing the number of calls
192 | # from Two to one.
193 | / if foo():
194 | | z = not foo()
195 | | else:
196 | | z = other
| |_____________^ SIM108
|
= help: Replace `if`-`else`-block with `z = not foo() if foo() else other`
Unsafe fix
190 190 | # SIM108 does not suggest a binary option in this
191 191 | # case, since we'd be reducing the number of calls
192 192 | # from Two to one.
193 |-if foo():
194 |- z = not foo()
195 |-else:
196 |- z = other
193 |+z = not foo() if foo() else other
197 194 |
198 195 |
199 196 | # These two cases double as tests for f-string quote preservation. The first
SIM108.py:202:1: SIM108 [*] Use ternary operator `var = "str" if cond else f"{first}-{second}"` instead of `if`-`else`-block
|
200 | # f-string should preserve its double quotes, and the second should preserve
201 | # single quotes
202 | / if cond:
203 | | var = "str"
204 | | else:
205 | | var = f"{first}-{second}"
| |_____________________________^ SIM108
206 |
207 | if cond:
|
= help: Replace `if`-`else`-block with `var = "str" if cond else f"{first}-{second}"`
Unsafe fix
199 199 | # These two cases double as tests for f-string quote preservation. The first
200 200 | # f-string should preserve its double quotes, and the second should preserve
201 201 | # single quotes
202 |-if cond:
203 |- var = "str"
204 |-else:
205 |- var = f"{first}-{second}"
202 |+var = "str" if cond else f"{first}-{second}"
206 203 |
207 204 | if cond:
208 205 | var = "str"
SIM108.py:207:1: SIM108 [*] Use ternary operator `var = "str" if cond else f'{first}-{second}'` instead of `if`-`else`-block
|
205 | var = f"{first}-{second}"
206 |
207 | / if cond:
208 | | var = "str"
209 | | else:
210 | | var = f'{first}-{second}'
| |_____________________________^ SIM108
|
= help: Replace `if`-`else`-block with `var = "str" if cond else f'{first}-{second}'`
Unsafe fix
204 204 | else:
205 205 | var = f"{first}-{second}"
206 206 |
207 |-if cond:
208 |- var = "str"
209 |-else:
210 |- var = f'{first}-{second}'
207 |+var = "str" if cond else f'{first}-{second}'

View File

@@ -2,7 +2,6 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
@@ -20,7 +19,7 @@ mod tests {
#[test_case(Rule::InvalidTodoCapitalization, Path::new("TD006.py"))]
#[test_case(Rule::MissingSpaceAfterTodoColon, Path::new("TD007.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_todos").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),

View File

@@ -6,7 +6,6 @@ pub mod settings;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
@@ -55,7 +54,7 @@ mod tests {
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_1.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_2.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),
@@ -70,7 +69,7 @@ mod tests {
#[test_case(Rule::QuotedTypeAlias, Path::new("TC008.py"))]
#[test_case(Rule::QuotedTypeAlias, Path::new("TC008_typing_execution_context.py"))]
fn type_alias_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings::for_rules(vec![
@@ -84,11 +83,7 @@ mod tests {
#[test_case(Rule::QuotedTypeAlias, Path::new("TC008_union_syntax_pre_py310.py"))]
fn type_alias_rules_pre_py310(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"pre_py310_{}_{}",
rule_code.as_ref(),
path.to_string_lossy()
);
let snapshot = format!("pre_py310_{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
@@ -107,7 +102,7 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote3.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote3.py"))]
fn quote(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("quote_{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("quote_{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
@@ -126,7 +121,7 @@ mod tests {
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("kw_only.py"))]
fn strict(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("strict_{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("strict_{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
@@ -170,7 +165,7 @@ mod tests {
Path::new("exempt_type_checking_3.py")
)]
fn exempt_type_checking(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
@@ -207,7 +202,7 @@ mod tests {
Path::new("runtime_evaluated_base_classes_5.py")
)]
fn runtime_evaluated_base_classes(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
@@ -238,7 +233,7 @@ mod tests {
Path::new("runtime_evaluated_decorators_3.py")
)]
fn runtime_evaluated_decorators(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
@@ -264,7 +259,7 @@ mod tests {
Path::new("module/undefined.py")
)]
fn base_class_same_file(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
@@ -282,7 +277,7 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("module/app.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("module/routes.py"))]
fn decorator_same_file(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {

View File

@@ -4,7 +4,6 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
@@ -22,7 +21,7 @@ mod tests {
#[test_case(Rule::Numpy2Deprecation, Path::new("NPY201_2.py"))]
#[test_case(Rule::Numpy2Deprecation, Path::new("NPY201_3.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("numpy").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),

View File

@@ -4,7 +4,6 @@ pub mod settings;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
@@ -20,7 +19,7 @@ mod tests {
#[test_case(Rule::DocstringMissingException, Path::new("DOC501.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("pydoclint").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),
@@ -36,7 +35,7 @@ mod tests {
#[test_case(Rule::DocstringMissingException, Path::new("DOC501_google.py"))]
#[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_google.py"))]
fn rules_google_style(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("pydoclint").join(path).as_path(),
&settings::LinterSettings {
@@ -58,7 +57,7 @@ mod tests {
#[test_case(Rule::DocstringMissingException, Path::new("DOC501_numpy.py"))]
#[test_case(Rule::DocstringExtraneousException, Path::new("DOC502_numpy.py"))]
fn rules_numpy_style(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("pydoclint").join(path).as_path(),
&settings::LinterSettings {
@@ -79,7 +78,7 @@ mod tests {
fn rules_google_style_ignore_one_line(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"{}_{}_ignore_one_line",
rule_code.as_ref(),
rule_code.name(),
path.to_string_lossy()
);
let diagnostics = test_path(

View File

@@ -776,8 +776,10 @@ mod tests {
messages.sort_by_key(Ranged::start);
let actual = messages
.iter()
.filter_map(Message::to_rule)
.filter(|msg| !msg.is_syntax_error())
.map(Message::name)
.collect::<Vec<_>>();
let expected: Vec<_> = expected.iter().map(|rule| rule.name().as_str()).collect();
assert_eq!(actual, expected);
}

View File

@@ -271,7 +271,7 @@ const TYPING_TO_RE_39: &[&str] = &["Match", "Pattern"];
const TYPING_RE_TO_RE_39: &[&str] = &["Match", "Pattern"];
// Members of `typing_extensions` that were moved to `typing`.
const TYPING_EXTENSIONS_TO_TYPING_39: &[&str] = &["Annotated", "get_type_hints"];
const TYPING_EXTENSIONS_TO_TYPING_39: &[&str] = &["Annotated"];
// Members of `typing` that were moved _and_ renamed (and thus cannot be
// automatically fixed).
@@ -373,6 +373,9 @@ const TYPING_EXTENSIONS_TO_TYPING_313: &[&str] = &[
"NoDefault",
"ReadOnly",
"TypeIs",
// Introduced in Python 3.5,
// but typing_extensions backports features from py313:
"get_type_hints",
// Introduced in Python 3.6,
// but typing_extensions backports features from py313:
"ContextManager",

View File

@@ -1179,6 +1179,8 @@ UP035.py:111:1: UP035 [*] Import from `warnings` instead: `deprecated`
110 | # UP035 on py313+ only
111 | from typing_extensions import deprecated
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
112 |
113 | # UP035 on py313+ only
|
= help: Import from `warnings`
@@ -1189,5 +1191,25 @@ UP035.py:111:1: UP035 [*] Import from `warnings` instead: `deprecated`
111 |-from typing_extensions import deprecated
111 |+from warnings import deprecated
112 112 |
113 113 |
114 114 | # https://github.com/astral-sh/ruff/issues/15780
113 113 | # UP035 on py313+ only
114 114 | from typing_extensions import get_type_hints
UP035.py:114:1: UP035 [*] Import from `typing` instead: `get_type_hints`
|
113 | # UP035 on py313+ only
114 | from typing_extensions import get_type_hints
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
115 |
116 | # https://github.com/astral-sh/ruff/issues/15780
|
= help: Import from `typing`
Safe fix
111 111 | from typing_extensions import deprecated
112 112 |
113 113 | # UP035 on py313+ only
114 |-from typing_extensions import get_type_hints
114 |+from typing import get_type_hints
115 115 |
116 116 | # https://github.com/astral-sh/ruff/issues/15780
117 117 | from typing_extensions import is_typeddict

View File

@@ -62,24 +62,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::ReadlinesInFor, Path::new("FURB129.py"))]
fn preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("refurb").join(path).as_path(),
&settings::LinterSettings {
preview: settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn write_whole_file_python_39() -> Result<()> {
let diagnostics = test_path(

View File

@@ -6,7 +6,7 @@ use ruff_python_semantic::analyze::typing::is_io_base_expr;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_readlines_in_for_fix_safe_enabled;
use crate::fix::edits::pad_end;
use crate::{AlwaysFixableViolation, Edit, Fix};
/// ## What it does
@@ -85,21 +85,25 @@ fn readlines_in_iter(checker: &Checker, iter_expr: &Expr) {
return;
}
}
let edit = if let Some(parenthesized_range) = parenthesized_range(
let deletion_range = if let Some(parenthesized_range) = parenthesized_range(
expr_attr.value.as_ref().into(),
expr_attr.into(),
checker.comment_ranges(),
checker.source(),
) {
Edit::range_deletion(expr_call.range().add_start(parenthesized_range.len()))
expr_call.range().add_start(parenthesized_range.len())
} else {
Edit::range_deletion(expr_call.range().add_start(expr_attr.value.range().len()))
expr_call.range().add_start(expr_attr.value.range().len())
};
let padded = pad_end(String::new(), deletion_range.end(), checker.locator());
let edit = if padded.is_empty() {
Edit::range_deletion(deletion_range)
} else {
Edit::range_replacement(padded, deletion_range)
};
let mut diagnostic = checker.report_diagnostic(ReadlinesInFor, expr_call.range());
diagnostic.set_fix(if is_readlines_in_for_fix_safe_enabled(checker.settings) {
Fix::safe_edit(edit)
} else {
Fix::unsafe_edit(edit)
});
diagnostic.set_fix(Fix::safe_edit(edit));
}

View File

@@ -12,7 +12,7 @@ FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
4 4 |
5 5 | # Errors
6 6 | with open("FURB129.py") as f:
@@ -33,7 +33,7 @@ FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
6 6 | with open("FURB129.py") as f:
7 7 | for _line in f.readlines():
8 8 | pass
@@ -53,7 +53,7 @@ FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
7 7 | for _line in f.readlines():
8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()]
@@ -74,7 +74,7 @@ FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()]
10 10 | b = {line.upper() for line in f.readlines()}
@@ -93,7 +93,7 @@ FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
12 12 |
13 13 | with Path("FURB129.py").open() as f:
@@ -113,7 +113,7 @@ FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
14 14 | for _line in f.readlines():
15 15 | pass
16 16 |
@@ -133,7 +133,7 @@ FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
17 17 | for _line in open("FURB129.py").readlines():
18 18 | pass
19 19 |
@@ -154,7 +154,7 @@ FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
23 23 |
24 24 | def func():
25 25 | f = Path("FURB129.py").open()
@@ -173,7 +173,7 @@ FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
29 29 |
30 30 |
31 31 | def func(f: io.BytesIO):
@@ -194,7 +194,7 @@ FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
35 35 |
36 36 | def func():
37 37 | with (open("FURB129.py") as f, foo as bar):
@@ -213,7 +213,7 @@ FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
44 44 | import builtins
45 45 |
46 46 | with builtins.open("FURB129.py") as f:
@@ -232,7 +232,7 @@ FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
51 51 | from builtins import open as o
52 52 |
53 53 | with o("FURB129.py") as f:
@@ -252,7 +252,7 @@ FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
90 90 |
91 91 | # https://github.com/astral-sh/ruff/issues/18231
92 92 | with open("furb129.py") as f:
@@ -270,7 +270,7 @@ FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over fil
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
94 94 | pass
95 95 |
96 96 | with open("furb129.py") as f:
@@ -290,7 +290,7 @@ FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over fi
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
98 98 |
99 99 |
100 100 | with open("furb129.py") as f:
@@ -310,10 +310,27 @@ FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over fi
|
= help: Remove `readlines()`
Unsafe fix
Safe fix
100 100 | with open("furb129.py") as f:
101 101 | for line in (((f))).readlines():
102 102 | pass
103 |- for line in(f).readlines():
103 |+ for line in(f):
104 104 | pass
105 105 |
106 106 | # Test case for issue #17683 (missing space before keyword)
FURB129.py:107:29: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
106 | # Test case for issue #17683 (missing space before keyword)
107 | print([line for line in f.readlines()if True])
| ^^^^^^^^^^^^^ FURB129
|
= help: Remove `readlines()`
Safe fix
104 104 | pass
105 105 |
106 106 | # Test case for issue #17683 (missing space before keyword)
107 |- print([line for line in f.readlines()if True])
107 |+ print([line for line in f if True])

View File

@@ -1,319 +0,0 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB129.py:7:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
5 | # Errors
6 | with open("FURB129.py") as f:
7 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
8 | pass
9 | a = [line.lower() for line in f.readlines()]
|
= help: Remove `readlines()`
Safe fix
4 4 |
5 5 | # Errors
6 6 | with open("FURB129.py") as f:
7 |- for _line in f.readlines():
7 |+ for _line in f:
8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()]
10 10 | b = {line.upper() for line in f.readlines()}
FURB129.py:9:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
7 | for _line in f.readlines():
8 | pass
9 | a = [line.lower() for line in f.readlines()]
| ^^^^^^^^^^^^^ FURB129
10 | b = {line.upper() for line in f.readlines()}
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
= help: Remove `readlines()`
Safe fix
6 6 | with open("FURB129.py") as f:
7 7 | for _line in f.readlines():
8 8 | pass
9 |- a = [line.lower() for line in f.readlines()]
9 |+ a = [line.lower() for line in f]
10 10 | b = {line.upper() for line in f.readlines()}
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
12 12 |
FURB129.py:10:35: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
8 | pass
9 | a = [line.lower() for line in f.readlines()]
10 | b = {line.upper() for line in f.readlines()}
| ^^^^^^^^^^^^^ FURB129
11 | c = {line.lower(): line.upper() for line in f.readlines()}
|
= help: Remove `readlines()`
Safe fix
7 7 | for _line in f.readlines():
8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()]
10 |- b = {line.upper() for line in f.readlines()}
10 |+ b = {line.upper() for line in f}
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
12 12 |
13 13 | with Path("FURB129.py").open() as f:
FURB129.py:11:49: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
9 | a = [line.lower() for line in f.readlines()]
10 | b = {line.upper() for line in f.readlines()}
11 | c = {line.lower(): line.upper() for line in f.readlines()}
| ^^^^^^^^^^^^^ FURB129
12 |
13 | with Path("FURB129.py").open() as f:
|
= help: Remove `readlines()`
Safe fix
8 8 | pass
9 9 | a = [line.lower() for line in f.readlines()]
10 10 | b = {line.upper() for line in f.readlines()}
11 |- c = {line.lower(): line.upper() for line in f.readlines()}
11 |+ c = {line.lower(): line.upper() for line in f}
12 12 |
13 13 | with Path("FURB129.py").open() as f:
14 14 | for _line in f.readlines():
FURB129.py:14:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
13 | with Path("FURB129.py").open() as f:
14 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
15 | pass
|
= help: Remove `readlines()`
Safe fix
11 11 | c = {line.lower(): line.upper() for line in f.readlines()}
12 12 |
13 13 | with Path("FURB129.py").open() as f:
14 |- for _line in f.readlines():
14 |+ for _line in f:
15 15 | pass
16 16 |
17 17 | for _line in open("FURB129.py").readlines():
FURB129.py:17:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
15 | pass
16 |
17 | for _line in open("FURB129.py").readlines():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129
18 | pass
|
= help: Remove `readlines()`
Safe fix
14 14 | for _line in f.readlines():
15 15 | pass
16 16 |
17 |-for _line in open("FURB129.py").readlines():
17 |+for _line in open("FURB129.py"):
18 18 | pass
19 19 |
20 20 | for _line in Path("FURB129.py").open().readlines():
FURB129.py:20:14: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
18 | pass
19 |
20 | for _line in Path("FURB129.py").open().readlines():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB129
21 | pass
|
= help: Remove `readlines()`
Safe fix
17 17 | for _line in open("FURB129.py").readlines():
18 18 | pass
19 19 |
20 |-for _line in Path("FURB129.py").open().readlines():
20 |+for _line in Path("FURB129.py").open():
21 21 | pass
22 22 |
23 23 |
FURB129.py:26:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
24 | def func():
25 | f = Path("FURB129.py").open()
26 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
27 | pass
28 | f.close()
|
= help: Remove `readlines()`
Safe fix
23 23 |
24 24 | def func():
25 25 | f = Path("FURB129.py").open()
26 |- for _line in f.readlines():
26 |+ for _line in f:
27 27 | pass
28 28 | f.close()
29 29 |
FURB129.py:32:18: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
31 | def func(f: io.BytesIO):
32 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
33 | pass
|
= help: Remove `readlines()`
Safe fix
29 29 |
30 30 |
31 31 | def func(f: io.BytesIO):
32 |- for _line in f.readlines():
32 |+ for _line in f:
33 33 | pass
34 34 |
35 35 |
FURB129.py:38:22: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
36 | def func():
37 | with (open("FURB129.py") as f, foo as bar):
38 | for _line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
39 | pass
40 | for _line in bar.readlines():
|
= help: Remove `readlines()`
Safe fix
35 35 |
36 36 | def func():
37 37 | with (open("FURB129.py") as f, foo as bar):
38 |- for _line in f.readlines():
38 |+ for _line in f:
39 39 | pass
40 40 | for _line in bar.readlines():
41 41 | pass
FURB129.py:47:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
46 | with builtins.open("FURB129.py") as f:
47 | for line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
48 | pass
|
= help: Remove `readlines()`
Safe fix
44 44 | import builtins
45 45 |
46 46 | with builtins.open("FURB129.py") as f:
47 |- for line in f.readlines():
47 |+ for line in f:
48 48 | pass
49 49 |
50 50 |
FURB129.py:54:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
53 | with o("FURB129.py") as f:
54 | for line in f.readlines():
| ^^^^^^^^^^^^^ FURB129
55 | pass
|
= help: Remove `readlines()`
Safe fix
51 51 | from builtins import open as o
52 52 |
53 53 | with o("FURB129.py") as f:
54 |- for line in f.readlines():
54 |+ for line in f:
55 55 | pass
56 56 |
57 57 |
FURB129.py:93:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
91 | # https://github.com/astral-sh/ruff/issues/18231
92 | with open("furb129.py") as f:
93 | for line in (f).readlines():
| ^^^^^^^^^^^^^^^ FURB129
94 | pass
|
= help: Remove `readlines()`
Safe fix
90 90 |
91 91 | # https://github.com/astral-sh/ruff/issues/18231
92 92 | with open("furb129.py") as f:
93 |- for line in (f).readlines():
93 |+ for line in (f):
94 94 | pass
95 95 |
96 96 | with open("furb129.py") as f:
FURB129.py:97:23: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
96 | with open("furb129.py") as f:
97 | [line for line in (f).readlines()]
| ^^^^^^^^^^^^^^^ FURB129
|
= help: Remove `readlines()`
Safe fix
94 94 | pass
95 95 |
96 96 | with open("furb129.py") as f:
97 |- [line for line in (f).readlines()]
97 |+ [line for line in (f)]
98 98 |
99 99 |
100 100 | with open("furb129.py") as f:
FURB129.py:101:17: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
100 | with open("furb129.py") as f:
101 | for line in (((f))).readlines():
| ^^^^^^^^^^^^^^^^^^^ FURB129
102 | pass
103 | for line in(f).readlines():
|
= help: Remove `readlines()`
Safe fix
98 98 |
99 99 |
100 100 | with open("furb129.py") as f:
101 |- for line in (((f))).readlines():
101 |+ for line in (((f))):
102 102 | pass
103 103 | for line in(f).readlines():
104 104 | pass
FURB129.py:103:16: FURB129 [*] Instead of calling `readlines()`, iterate over file object directly
|
101 | for line in (((f))).readlines():
102 | pass
103 | for line in(f).readlines():
| ^^^^^^^^^^^^^^^ FURB129
104 | pass
|
= help: Remove `readlines()`
Safe fix
100 100 | with open("furb129.py") as f:
101 101 | for line in (((f))).readlines():
102 102 | pass
103 |- for line in(f).readlines():
103 |+ for line in(f):
104 104 | pass

View File

@@ -24,6 +24,7 @@ mod tests {
use crate::{assert_messages, settings};
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005.py"))]
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005_slices.py"))]
#[test_case(Rule::AsyncioDanglingTask, Path::new("RUF006.py"))]
#[test_case(Rule::ZipInsteadOfPairwise, Path::new("RUF007.py"))]
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))]
@@ -94,6 +95,7 @@ mod tests {
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052.py"))]
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
#[test_case(Rule::FalsyDictGetFallback, Path::new("RUF056.py"))]
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_0.py"))]
#[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_1.py"))]
@@ -322,10 +324,7 @@ mod tests {
fn ruff_noqa_filedirective_unused() -> Result<()> {
let diagnostics = test_path(
Path::new("ruff/RUF100_6.py"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA])
},
&settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA]),
)?;
assert_messages!(diagnostics);
Ok(())
@@ -335,15 +334,12 @@ mod tests {
fn ruff_noqa_filedirective_unused_last_of_many() -> Result<()> {
let diagnostics = test_path(
Path::new("ruff/RUF100_7.py"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rules(vec![
Rule::UnusedNOQA,
Rule::FStringMissingPlaceholders,
Rule::LineTooLong,
Rule::UnusedVariable,
])
},
&settings::LinterSettings::for_rules(vec![
Rule::UnusedNOQA,
Rule::FStringMissingPlaceholders,
Rule::LineTooLong,
Rule::UnusedVariable,
]),
)?;
assert_messages!(diagnostics);
Ok(())
@@ -480,10 +476,8 @@ mod tests {
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
#[test_case(Rule::StarmapZip, Path::new("RUF058_0.py"))]
#[test_case(Rule::StarmapZip, Path::new("RUF058_1.py"))]
#[test_case(Rule::ClassWithMixedTypeVars, Path::new("RUF053.py"))]
#[test_case(Rule::IndentedFormFeed, Path::new("RUF054.py"))]
#[test_case(Rule::ImplicitClassVarInDataclass, Path::new("RUF045.py"))]
#[test_case(Rule::CollectionLiteralConcatenation, Path::new("RUF005_slices.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -4,7 +4,6 @@ use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
use crate::preview::is_support_slices_in_literal_concatenation_enabled;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
@@ -96,7 +95,7 @@ enum Type {
}
/// Recursively merge all the tuples and lists in the expression.
fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(Expr, Type)> {
fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> {
let Expr::BinOp(ast::ExprBinOp {
left,
op: Operator::Add,
@@ -108,22 +107,18 @@ fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(
};
let new_left = match left.as_ref() {
Expr::BinOp(ast::ExprBinOp { .. }) => {
match concatenate_expressions(left, should_support_slices) {
Some((new_left, _)) => new_left,
None => *left.clone(),
}
}
Expr::BinOp(ast::ExprBinOp { .. }) => match concatenate_expressions(left) {
Some((new_left, _)) => new_left,
None => *left.clone(),
},
_ => *left.clone(),
};
let new_right = match right.as_ref() {
Expr::BinOp(ast::ExprBinOp { .. }) => {
match concatenate_expressions(right, should_support_slices) {
Some((new_right, _)) => new_right,
None => *right.clone(),
}
}
Expr::BinOp(ast::ExprBinOp { .. }) => match concatenate_expressions(right) {
Some((new_right, _)) => new_right,
None => *right.clone(),
},
_ => *right.clone(),
};
@@ -151,9 +146,7 @@ fn concatenate_expressions(expr: &Expr, should_support_slices: bool) -> Option<(
make_splat_elts(splat_element, other_elements, splat_at_left)
}
// Subscripts are also considered safe-ish to splat if the indexer is a slice.
Expr::Subscript(ast::ExprSubscript { slice, .. })
if should_support_slices && matches!(&**slice, Expr::Slice(_)) =>
{
Expr::Subscript(ast::ExprSubscript { slice, .. }) if matches!(&**slice, Expr::Slice(_)) => {
make_splat_elts(splat_element, other_elements, splat_at_left)
}
// If the splat element is itself a list/tuple, insert them in the other list/tuple.
@@ -198,10 +191,7 @@ pub(crate) fn collection_literal_concatenation(checker: &Checker, expr: &Expr) {
return;
}
let should_support_slices =
is_support_slices_in_literal_concatenation_enabled(checker.settings);
let Some((new_expr, type_)) = concatenate_expressions(expr, should_support_slices) else {
let Some((new_expr, type_)) = concatenate_expressions(expr) else {
return;
};

View File

@@ -7,6 +7,7 @@ use ruff_python_semantic::analyze::function_type::is_stub;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::rules::fastapi::rules::is_fastapi_route;
/// ## What it does

View File

@@ -4,7 +4,6 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
@@ -25,7 +24,7 @@ mod tests {
#[test_case(Rule::ErrorInsteadOfException, Path::new("TRY400.py"))]
#[test_case(Rule::VerboseLogMessage, Path::new("TRY401.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("tryceratops").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),

View File

@@ -20,6 +20,7 @@ use ruff_python_parser::{ParseError, ParseOptions};
use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::SourceFileBuilder;
use crate::codes::Rule;
use crate::fix::{FixResult, fix_file};
use crate::linter::check_path;
use crate::message::{Emitter, EmitterContext, Message, TextEmitter};
@@ -233,8 +234,9 @@ Source with applied fixes:
let messages = messages
.into_iter()
.filter_map(|msg| Some((msg.to_rule()?, msg)))
.map(|(rule, mut diagnostic)| {
.filter_map(|msg| Some((msg.noqa_code()?, msg)))
.map(|(code, mut diagnostic)| {
let rule = Rule::from_code(&code.to_string()).unwrap();
let fixable = diagnostic.fix().is_some_and(|fix| {
matches!(
fix.applicability(),

View File

@@ -174,7 +174,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
output.extend(quote! {
impl #linter {
pub fn rules(&self) -> ::std::vec::IntoIter<Rule> {
pub(crate) fn rules(&self) -> ::std::vec::IntoIter<Rule> {
match self { #prefix_into_iter_match_arms }
}
}
@@ -182,7 +182,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
}
output.extend(quote! {
impl RuleCodePrefix {
pub fn parse(linter: &Linter, code: &str) -> Result<Self, crate::registry::FromCodeError> {
pub(crate) fn parse(linter: &Linter, code: &str) -> Result<Self, crate::registry::FromCodeError> {
use std::str::FromStr;
Ok(match linter {
@@ -190,7 +190,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
})
}
pub fn rules(&self) -> ::std::vec::IntoIter<Rule> {
pub(crate) fn rules(&self) -> ::std::vec::IntoIter<Rule> {
match self {
#(RuleCodePrefix::#linter_idents(prefix) => prefix.clone().rules(),)*
}
@@ -319,7 +319,7 @@ See also https://github.com/astral-sh/ruff/issues/2186.
matches!(self.group(), RuleGroup::Preview)
}
pub fn is_stable(&self) -> bool {
pub(crate) fn is_stable(&self) -> bool {
matches!(self.group(), RuleGroup::Stable)
}
@@ -371,7 +371,7 @@ fn generate_iter_impl(
quote! {
impl Linter {
/// Rules not in the preview.
pub fn rules(self: &Linter) -> ::std::vec::IntoIter<Rule> {
pub(crate) fn rules(self: &Linter) -> ::std::vec::IntoIter<Rule> {
match self {
#linter_rules_match_arms
}
@@ -385,7 +385,7 @@ fn generate_iter_impl(
}
impl RuleCodePrefix {
pub fn iter() -> impl Iterator<Item = RuleCodePrefix> {
pub(crate) fn iter() -> impl Iterator<Item = RuleCodePrefix> {
use strum::IntoEnumIterator;
let mut prefixes = Vec::new();
@@ -436,7 +436,6 @@ fn register_rules<'a>(input: impl Iterator<Item = &'a Rule>) -> TokenStream {
PartialOrd,
Ord,
::ruff_macros::CacheKey,
AsRefStr,
::strum_macros::IntoStaticStr,
::strum_macros::EnumString,
::serde::Serialize,

View File

@@ -2,6 +2,7 @@
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break

View File

@@ -165,7 +165,7 @@ where
pub fn formatted_file(db: &dyn Db, file: File) -> Result<Option<String>, FormatModuleError> {
let options = db.format_options(file);
let parsed = parsed_module(db.upcast(), file);
let parsed = parsed_module(db.upcast(), file).load(db.upcast());
if let Some(first) = parsed.errors().first() {
return Err(FormatModuleError::ParseError(first.clone()));
@@ -174,7 +174,7 @@ pub fn formatted_file(db: &dyn Db, file: File) -> Result<Option<String>, FormatM
let comment_ranges = CommentRanges::from(parsed.tokens());
let source = source_text(db.upcast(), file);
let formatted = format_node(parsed, &comment_ranges, &source, options)?;
let formatted = format_node(&parsed, &comment_ranges, &source, options)?;
let printed = formatted.print()?;
if printed.as_code() == &*source {

View File

@@ -1,7 +1,6 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_comments.py
snapshot_kind: text
---
## Input
```python
@@ -9,6 +8,7 @@ snapshot_kind: text
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
@@ -44,6 +44,7 @@ i = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # NoQA: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # ty: ignore This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break

View File

@@ -26,5 +26,5 @@ pub fn is_pragma_comment(comment: &str) -> bool {
// Case-sensitive match against a variety of pragmas that _do_ require a trailing colon.
trimmed
.split_once(':')
.is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pylint" | "flake8" | "ruff"))
.is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pylint" | "flake8" | "ruff" | "ty"))
}

View File

@@ -242,7 +242,7 @@ fn to_lsp_diagnostic(
let body = diagnostic.body().to_string();
let fix = diagnostic.fix();
let suggestion = diagnostic.suggestion();
let code = diagnostic.to_noqa_code();
let code = diagnostic.noqa_code();
let fix = fix.and_then(|fix| fix.applies(Applicability::Unsafe).then_some(fix));

View File

@@ -85,7 +85,7 @@ pub(crate) fn hover(
fn format_rule_text(rule: Rule) -> String {
let mut output = String::new();
let _ = write!(&mut output, "# {} ({})", rule.as_ref(), rule.noqa_code());
let _ = write!(&mut output, "# {} ({})", rule.name(), rule.noqa_code());
output.push('\n');
output.push('\n');

View File

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

View File

@@ -208,7 +208,7 @@ impl Workspace {
let messages: Vec<ExpandedMessage> = messages
.into_iter()
.map(|msg| ExpandedMessage {
code: msg.to_noqa_code().map(|code| code.to_string()),
code: msg.noqa_code().map(|code| code.to_string()),
message: msg.body().to_string(),
start_location: source_code.line_column(msg.start()).into(),
end_location: source_code.line_column(msg.end()).into(),

View File

@@ -65,7 +65,7 @@ fn syntax_error() {
fn unsupported_syntax_error() {
check!(
"match 2:\n case 1: ...",
r#"{"preview": true, "target-version": "py39"}"#,
r#"{"target-version": "py39"}"#,
[ExpandedMessage {
code: None,
message: "SyntaxError: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)".to_string(),

View File

@@ -1098,7 +1098,7 @@ impl LintConfiguration {
// approach to give each pair it's own `warn_user_once`.
for (preferred, expendable, message) in INCOMPATIBLE_CODES {
if rules.enabled(*preferred) && rules.enabled(*expendable) {
warn_user_once_by_id!(expendable.as_ref(), "{}", message);
warn_user_once_by_id!(expendable.name().as_str(), "{}", message);
rules.disable(*expendable);
}
}

View File

@@ -18,10 +18,9 @@ use clap::{CommandFactory, Parser};
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use rayon::ThreadPoolBuilder;
use ruff_db::Upcast;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
use ruff_db::max_parallelism;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ruff_db::{Upcast, max_parallelism};
use salsa::plumbing::ZalsaDatabase;
use ty_project::metadata::options::ProjectOptionsOverrides;
use ty_project::watch::ProjectWatcher;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,206 @@
use insta_cmd::assert_cmd_snapshot;
use crate::CliTest;
#[test]
fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
// Long flag
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Short flag
assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> {
let case = CliTest::with_files(vec![
(
"ty.toml",
r#"
[terminal]
error-on-warning = true
"#,
),
("test.py", r"print(x) # [unresolved-reference]"),
])?;
// Exit code of 1 due to the setting in `ty.toml`
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Exit code of 0 because the `ty.toml` setting is overwritten by `--config`
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r"
success: true
exit_code: 0
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true").arg("--config").arg("terminal.error-on-warning=false"), @r"
success: true
exit_code: 0
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", r"print(1)")?;
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: TOML parse error at line 1, column 1
|
1 | bad-option=true
| ^^^^^^^^^^
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`
Usage: ty <COMMAND>
For more information, try '--help'.
"###);
Ok(())
}
#[test]
fn config_file_override() -> anyhow::Result<()> {
// Set `error-on-warning` to true in the configuration file
// Explicitly set `--warn unresolved-reference` to ensure the rule warns instead of errors
let case = CliTest::with_files(vec![
("test.py", r"print(x) # [unresolved-reference]"),
(
"ty-override.toml",
r#"
[terminal]
error-on-warning = true
"#,
),
])?;
// Ensure flag works via CLI arg
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config-file").arg("ty-override.toml"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Ensure the flag works via an environment variable
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").env("TY_CONFIG_FILE", "ty-override.toml"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}

View File

@@ -0,0 +1,272 @@
use insta_cmd::assert_cmd_snapshot;
use crate::CliTest;
#[test]
fn only_warnings() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
success: true
exit_code: 0
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn only_info() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
from typing_extensions import reveal_type
reveal_type(1)
"#,
)?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
info[revealed-type]: Revealed type
--> test.py:3:13
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| ^ `Literal[1]`
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
from typing_extensions import reveal_type
reveal_type(1)
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r"
success: true
exit_code: 0
----- stdout -----
info[revealed-type]: Revealed type
--> test.py:3:13
|
2 | from typing_extensions import reveal_type
3 | reveal_type(1)
| ^ `Literal[1]`
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command().arg("--error-on-warning").arg("--warn").arg("unresolved-reference"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> {
let case = CliTest::with_files([
("test.py", r"print(x) # [unresolved-reference]"),
(
"ty.toml",
r#"
[terminal]
error-on-warning = true
"#,
),
])?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: rule `unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn both_warnings_and_errors() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^
3 | print(4[1]) # [non-subscriptable]
|
info: rule `unresolved-reference` was selected on the command line
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^
|
info: rule `non-subscriptable` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r###"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"###,
)?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^
3 | print(4[1]) # [non-subscriptable]
|
info: rule `unresolved-reference` was selected on the command line
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^
|
info: rule `non-subscriptable` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn exit_zero_is_true() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r"
success: true
exit_code: 0
----- stdout -----
warning[unresolved-reference]: Name `x` used when not defined
--> test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^
3 | print(4[1]) # [non-subscriptable]
|
info: rule `unresolved-reference` was selected on the command line
error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^
|
info: rule `non-subscriptable` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}

690
crates/ty/tests/cli/main.rs Normal file
View File

@@ -0,0 +1,690 @@
mod config_option;
mod exit_code;
mod python_environment;
mod rule_selection;
use anyhow::Context as _;
use insta::internals::SettingsBindDropGuard;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use std::{
fmt::Write,
path::{Path, PathBuf},
process::Command,
};
use tempfile::TempDir;
#[test]
fn test_run_in_sub_directory() -> anyhow::Result<()> {
let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> <temp_dir>/test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn test_include_hidden_files_by_default() -> anyhow::Result<()> {
let case = CliTest::with_files([(".test.py", "~")])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> .test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn test_respect_ignore_files() -> anyhow::Result<()> {
// First test that the default option works correctly (the file is skipped)
let case = CliTest::with_files([(".ignore", "test.py"), ("test.py", "~")])?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
WARN No python files found under the given path(s)
");
// Test that we can set to false via CLI
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Test that we can set to false via config file
case.write_file("ty.toml", "src.respect-ignore-files = false")?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Ensure CLI takes precedence
case.write_file("ty.toml", "src.respect-ignore-files = true")?;
assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> test.py:1:2
|
1 | ~
| ^ Expected an expression
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// Paths specified on the CLI are relative to the current working directory and not the project root.
///
/// We test this by adding an extra search path from the CLI to the libs directory when
/// running the CLI from the child directory (using relative paths).
///
/// Project layout:
/// ```
/// - libs
/// |- utils.py
/// - child
/// | - test.py
/// - pyproject.toml
/// ```
///
/// And the command is run in the `child` directory.
#[test]
fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-version = "3.11"
"#,
),
(
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
return a + b
"#,
),
(
"child/test.py",
r#"
from utils import add
stat = add(10, 15)
"#,
),
])?;
// Make sure that the CLI fails when the `libs` directory is not in the search path.
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `utils`
--> test.py:2:6
|
2 | from utils import add
| ^^^^^
3 |
4 | stat = add(10, 15)
|
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// Paths specified in a configuration file are relative to the project root.
///
/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run
/// the CLI from a subdirectory.
///
/// Project layout:
/// ```
/// - libs
/// |- utils.py
/// - child
/// | - test.py
/// - pyproject.toml
/// ```
#[test]
fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-version = "3.11"
extra-paths = ["libs"]
"#,
),
(
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
return a + b
"#,
),
(
"child/test.py",
r#"
from utils import add
stat = add(10, 15)
"#,
),
])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn user_configuration() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"project/ty.toml",
r#"
[rules]
division-by-zero = "warn"
"#,
),
(
"project/main.py",
r#"
y = 4 / 0
for a in range(0, int(y)):
x = a
prin(x)
"#,
),
])?;
let config_directory = case.root().join("home/.config");
let config_env_var = if cfg!(windows) {
"APPDATA"
} else {
"XDG_CONFIG_HOME"
};
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r"
success: false
exit_code: 1
----- stdout -----
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: rule `division-by-zero` was selected in the configuration file
error[unresolved-reference]: Name `prin` used when not defined
--> main.py:7:1
|
5 | x = a
6 |
7 | prin(x)
| ^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"
);
// The user-level configuration sets the severity for `unresolved-reference` to warn.
// Changing the level for `division-by-zero` has no effect, because the project-level configuration
// has higher precedence.
case.write_file(
config_directory.join("ty/ty.toml"),
r#"
[rules]
division-by-zero = "error"
unresolved-reference = "warn"
"#,
)?;
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r"
success: true
exit_code: 0
----- stdout -----
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> main.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: rule `division-by-zero` was selected in the configuration file
warning[unresolved-reference]: Name `prin` used when not defined
--> main.py:7:1
|
5 | x = a
6 |
7 | prin(x)
| ^^^^
|
info: rule `unresolved-reference` was selected in the configuration file
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"
);
Ok(())
}
#[test]
fn check_specific_paths() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"project/main.py",
r#"
y = 4 / 0 # error: division-by-zero
"#,
),
(
"project/tests/test_main.py",
r#"
import does_not_exist # error: unresolved-import
"#,
),
(
"project/other.py",
r#"
from main2 import z # error: unresolved-import
print(z)
"#,
),
])?;
assert_cmd_snapshot!(
case.command(),
@r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `main2`
--> project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^
3 |
4 | print(z)
|
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `does_not_exist`
--> project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^
|
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"###
);
// Now check only the `tests` and `other.py` files.
// We should no longer see any diagnostics related to `main.py`.
assert_cmd_snapshot!(
case.command().arg("project/tests").arg("project/other.py"),
@r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `main2`
--> project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^
3 |
4 | print(z)
|
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `does_not_exist`
--> project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^
|
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"
);
Ok(())
}
#[test]
fn check_non_existing_path() -> anyhow::Result<()> {
let case = CliTest::with_files([])?;
let mut settings = insta::Settings::clone_current();
settings.add_filter(
&regex::escape("The system cannot find the path specified. (os error 3)"),
"No such file or directory (os error 2)",
);
let _s = settings.bind_to_scope();
assert_cmd_snapshot!(
case.command().arg("project/main.py").arg("project/tests"),
@r"
success: false
exit_code: 1
----- stdout -----
error[io]: `<temp_dir>/project/main.py`: No such file or directory (os error 2)
error[io]: `<temp_dir>/project/tests`: No such file or directory (os error 2)
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
WARN No python files found under the given path(s)
"
);
Ok(())
}
#[test]
fn concise_diagnostics() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
print(x) # [unresolved-reference]
print(4[1]) # [non-subscriptable]
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--output-format=concise").arg("--warn").arg("unresolved-reference"), @r"
success: false
exit_code: 1
----- stdout -----
warning[unresolved-reference] test.py:2:7: Name `x` used when not defined
error[non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// This tests the diagnostic format for revealed type.
///
/// This test was introduced because changes were made to
/// how the revealed type diagnostic was constructed and
/// formatted in "verbose" mode. But it required extra
/// logic to ensure the concise version didn't regress on
/// information content. So this test was introduced to
/// capture that.
#[test]
fn concise_revealed_type() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
from typing_extensions import reveal_type
x = "hello"
reveal_type(x)
"#,
)?;
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#"
success: true
exit_code: 0
----- stdout -----
info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]`
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
Ok(())
}
#[test]
fn can_handle_large_binop_expressions() -> anyhow::Result<()> {
let mut content = String::new();
writeln!(
&mut content,
"
from typing_extensions import reveal_type
total = 1{plus_one_repeated}
reveal_type(total)
",
plus_one_repeated = " + 1".repeat(2000 - 1)
)?;
let case = CliTest::with_file("test.py", &ruff_python_trivia::textwrap::dedent(&content))?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
info[revealed-type]: Revealed type
--> test.py:4:13
|
2 | from typing_extensions import reveal_type
3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1...
4 | reveal_type(total)
| ^^^^^ `Literal[2000]`
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
pub(crate) struct CliTest {
_temp_dir: TempDir,
_settings_scope: SettingsBindDropGuard,
project_dir: PathBuf,
}
impl CliTest {
pub(crate) fn new() -> anyhow::Result<Self> {
let temp_dir = TempDir::new()?;
// Canonicalize the tempdir path because macos uses symlinks for tempdirs
// and that doesn't play well with our snapshot filtering.
// Simplify with dunce because otherwise we get UNC paths on Windows.
let project_dir = dunce::simplified(
&temp_dir
.path()
.canonicalize()
.context("Failed to canonicalize project path")?,
)
.to_path_buf();
let mut settings = insta::Settings::clone_current();
settings.add_filter(&tempdir_filter(&project_dir), "<temp_dir>/");
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
let settings_scope = settings.bind_to_scope();
Ok(Self {
project_dir,
_temp_dir: temp_dir,
_settings_scope: settings_scope,
})
}
pub(crate) fn with_files<'a>(
files: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> anyhow::Result<Self> {
let case = Self::new()?;
case.write_files(files)?;
Ok(case)
}
pub(crate) fn with_file(path: impl AsRef<Path>, content: &str) -> anyhow::Result<Self> {
let case = Self::new()?;
case.write_file(path, content)?;
Ok(case)
}
pub(crate) fn write_files<'a>(
&self,
files: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> anyhow::Result<()> {
for (path, content) in files {
self.write_file(path, content)?;
}
Ok(())
}
pub(crate) fn write_file(&self, path: impl AsRef<Path>, content: &str) -> anyhow::Result<()> {
let path = path.as_ref();
let path = self.project_dir.join(path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
}
std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content))
.with_context(|| format!("Failed to write file `{path}`", path = path.display()))?;
Ok(())
}
pub(crate) fn root(&self) -> &Path {
&self.project_dir
}
pub(crate) fn command(&self) -> Command {
let mut command = Command::new(get_cargo_bin("ty"));
command.current_dir(&self.project_dir).arg("check");
// Unset environment variables that can affect test behavior
command.env_remove("VIRTUAL_ENV");
command.env_remove("CONDA_PREFIX");
command
}
}
fn tempdir_filter(path: &Path) -> String {
format!(r"{}\\?/?", regex::escape(path.to_str().unwrap()))
}

View File

@@ -0,0 +1,774 @@
use insta_cmd::assert_cmd_snapshot;
use ruff_python_ast::PythonVersion;
use crate::CliTest;
/// Specifying an option on the CLI should take precedence over the same setting in the
/// project's configuration. Here, this is tested for the Python version.
#[test]
fn config_override_python_version() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-version = "3.11"
"#,
),
(
"test.py",
r#"
import sys
# Access `sys.last_exc` that was only added in Python 3.12
print(sys.last_exc)
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-attribute]: Type `<module 'sys'>` has no attribute `last_exc`
--> test.py:5:7
|
4 | # Access `sys.last_exc` that was only added in Python 3.12
5 | print(sys.last_exc)
| ^^^^^^^^^^^^
|
info: rule `unresolved-attribute` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// Same as above, but for the Python platform.
#[test]
fn config_override_python_platform() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-platform = "linux"
"#,
),
(
"test.py",
r#"
import sys
from typing_extensions import reveal_type
reveal_type(sys.platform)
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: true
exit_code: 0
----- stdout -----
info[revealed-type]: Revealed type
--> test.py:5:13
|
3 | from typing_extensions import reveal_type
4 |
5 | reveal_type(sys.platform)
| ^^^^^^^^^^^^ `Literal["linux"]`
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r"
success: true
exit_code: 0
----- stdout -----
info[revealed-type]: Revealed type
--> test.py:5:13
|
3 | from typing_extensions import reveal_type
4 |
5 | reveal_type(sys.platform)
| ^^^^^^^^^^^^ `LiteralString`
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-version = "3.8"
"#,
),
(
"test.py",
r#"
aiter
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:2:1
|
2 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types
--> pyproject.toml:3:18
|
2 | [tool.ty.environment]
3 | python-version = "3.8"
| ^^^^^ Python 3.8 assumed due to this configuration setting
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:2:1
|
2 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.9 was assumed when resolving types because it was specified on the command line
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python = "venv"
"#,
),
(
"venv/pyvenv.cfg",
r#"
version = 3.8
home = foo/bar/bin
"#,
),
if cfg!(target_os = "windows") {
("foo/bar/bin/python.exe", "")
} else {
("foo/bar/bin/python", "")
},
if cfg!(target_os = "windows") {
("venv/Lib/site-packages/foo.py", "")
} else {
("venv/lib/python3.8/site-packages/foo.py", "")
},
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:1:1
|
1 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types because of your virtual environment
--> venv/pyvenv.cfg:2:11
|
2 | version = 3.8
| ^^^ Python version inferred from virtual environment metadata file
3 | home = foo/bar/bin
|
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python = "venv"
"#,
),
(
"venv/pyvenv.cfg",
r#"home = foo/bar/bin
version = 3.8"#,
),
if cfg!(target_os = "windows") {
("foo/bar/bin/python.exe", "")
} else {
("foo/bar/bin/python", "")
},
if cfg!(target_os = "windows") {
("venv/Lib/site-packages/foo.py", "")
} else {
("venv/lib/python3.8/site-packages/foo.py", "")
},
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:1:1
|
1 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types because of your virtual environment
--> venv/pyvenv.cfg:4:23
|
4 | version = 3.8
| ^^^ Python version inferred from virtual environment metadata file
|
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[project]
requires-python = ">=3.8"
"#,
),
(
"test.py",
r#"
match object():
case int():
pass
case _:
pass
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> test.py:2:1
|
2 | match object():
| ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
3 | case int():
4 | pass
|
info: Python 3.8 was assumed when parsing syntax
--> pyproject.toml:3:19
|
2 | [project]
3 | requires-python = ">=3.8"
| ^^^^^^^ Python 3.8 assumed due to this configuration setting
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]
--> test.py:2:1
|
2 | match object():
| ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
3 | case int():
4 | pass
|
info: Python 3.9 was assumed when parsing syntax because it was specified on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn python_cli_argument_virtual_environment() -> anyhow::Result<()> {
let path_to_executable = if cfg!(windows) {
"my-venv/Scripts/python.exe"
} else {
"my-venv/bin/python"
};
let other_venv_path = "my-venv/foo/some_other_file.txt";
let case = CliTest::with_files([
("test.py", ""),
(
if cfg!(windows) {
"my-venv/Lib/site-packages/foo.py"
} else {
"my-venv/lib/python3.13/site-packages/foo.py"
},
"",
),
(path_to_executable, ""),
(other_venv_path, ""),
])?;
// Passing a path to the installation works
assert_cmd_snapshot!(case.command().arg("--python").arg("my-venv"), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// And so does passing a path to the executable inside the installation
assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// But random other paths inside the installation are rejected
assert_cmd_snapshot!(case.command().arg("--python").arg(other_venv_path), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
ty failed
Cause: Invalid search path settings
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `<temp_dir>/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk
");
// And so are paths that do not exist on disk
assert_cmd_snapshot!(case.command().arg("--python").arg("not-a-directory-or-executable"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
ty failed
Cause: Invalid search path settings
Cause: Failed to discover the site-packages directory: Invalid `--python` argument `<temp_dir>/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk
");
Ok(())
}
#[test]
fn python_cli_argument_system_installation() -> anyhow::Result<()> {
let path_to_executable = if cfg!(windows) {
"Python3.11/python.exe"
} else {
"Python3.11/bin/python"
};
let case = CliTest::with_files([
("test.py", ""),
(
if cfg!(windows) {
"Python3.11/Lib/site-packages/foo.py"
} else {
"Python3.11/lib/python3.11/site-packages/foo.py"
},
"",
),
(path_to_executable, ""),
])?;
// Passing a path to the installation works
assert_cmd_snapshot!(case.command().arg("--python").arg("Python3.11"), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// And so does passing a path to the executable inside the installation
assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn config_file_broken_python_setting() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[project]
name = "test"
version = "0.1.0"
description = "Some description"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
[tool.ty.environment]
python = "not-a-directory-or-executable"
"#,
),
("test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
ty failed
Cause: Invalid search path settings
Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
9 |
10 | [tool.ty.environment]
11 | python = "not-a-directory-or-executable"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk
|
"#);
Ok(())
}
#[test]
fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python = "directory-but-no-site-packages"
"#,
),
("directory-but-no-site-packages/lib/foo.py", ""),
("test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
ty failed
Cause: Invalid search path settings
Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
1 |
2 | [tool.ty.environment]
3 | python = "directory-but-no-site-packages"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable
|
"#);
Ok(())
}
// This error message is never emitted on Windows, because Windows installations have simpler layouts
#[cfg(not(windows))]
#[test]
fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python = "directory-but-no-site-packages"
"#,
),
("directory-but-no-site-packages/foo.py", ""),
("test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
ty failed
Cause: Invalid search path settings
Cause: Failed to discover the site-packages directory: Failed to iterate over the contents of the `lib` directory of the Python installation
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
1 |
2 | [tool.ty.environment]
3 | python = "directory-but-no-site-packages"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
"#);
Ok(())
}
#[test]
fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"ty.toml",
&*format!(
r#"
[environment]
python-version = "{}"
python-platform = "linux"
"#,
PythonVersion::default()
),
),
(
"main.py",
r#"
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-attribute]: Type `<module 'os'>` has no attribute `grantpt`
--> main.py:4:1
|
2 | import os
3 |
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
| ^^^^^^^^^^
|
info: rule `unresolved-attribute` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Use default (which should be latest supported)
let case = CliTest::with_files([
(
"ty.toml",
r#"
[environment]
python-platform = "linux"
"#,
),
(
"main.py",
r#"
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// The `site-packages` directory is used by ty for external import.
/// Ty does the following checks to discover the `site-packages` directory in the order:
/// 1) If `VIRTUAL_ENV` environment variable is set
/// 2) If `CONDA_PREFIX` environment variable is set
/// 3) If a `.venv` directory exists at the project root
///
/// This test is aiming at validating the logic around `CONDA_PREFIX`.
///
/// A conda-like environment file structure is used
/// We test by first not setting the `CONDA_PREFIX` and expect a fail.
/// Then we test by setting `CONDA_PREFIX` to `conda-env` and expect a pass.
///
/// ├── project
/// │ └── test.py
/// └── conda-env
/// └── lib
/// └── python3.13
/// └── site-packages
/// └── package1
/// └── __init__.py
///
/// test.py imports package1
/// And the command is run in the `child` directory.
#[test]
fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
let conda_package1_path = if cfg!(windows) {
"conda-env/Lib/site-packages/package1/__init__.py"
} else {
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
};
let case = CliTest::with_files([
(
"project/test.py",
r#"
import package1
"#,
),
(
conda_package1_path,
r#"
"#,
),
])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:2:8
|
2 | import package1
| ^^^^^^^^
|
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// do command : CONDA_PREFIX=<temp_dir>/conda_env
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")).env("CONDA_PREFIX", case.root().join("conda-env")), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}

View File

@@ -0,0 +1,292 @@
use insta_cmd::assert_cmd_snapshot;
use crate::CliTest;
/// The rule severity can be changed in the configuration file
#[test]
fn configuration_rule_severity() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
y = 4 / 0
for a in range(0, int(y)):
x = a
prin(x) # unresolved-reference
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error).
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:7:1
|
5 | x = a
6 |
7 | prin(x) # unresolved-reference
| ^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"###);
case.write_file(
"pyproject.toml",
r#"
[tool.ty.rules]
division-by-zero = "warn" # promote to warn
unresolved-reference = "ignore"
"#,
)?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: rule `division-by-zero` was selected in the configuration file
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// The rule severity can be changed using `--ignore`, `--warn`, and `--error`
#[test]
fn cli_rule_severity() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
import does_not_exit
y = 4 / 0
for a in range(0, int(y)):
x = a
prin(x) # unresolved-reference
"#,
)?;
// Assert that there's an `unresolved-reference` diagnostic (error)
// and an unresolved-import (error) diagnostic by default.
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `does_not_exit`
--> test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^
3 |
4 | y = 4 / 0
|
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:9:1
|
7 | x = a
8 |
9 | prin(x) # unresolved-reference
| ^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"###);
assert_cmd_snapshot!(
case
.command()
.arg("--ignore")
.arg("unresolved-reference")
.arg("--warn")
.arg("division-by-zero")
.arg("--warn")
.arg("unresolved-import"),
@r"
success: true
exit_code: 0
----- stdout -----
warning[unresolved-import]: Cannot resolve imported module `does_not_exit`
--> test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^
3 |
4 | y = 4 / 0
|
info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment
info: rule `unresolved-import` was selected on the command line
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
info: rule `division-by-zero` was selected on the command line
Found 2 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"
);
Ok(())
}
/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` and
/// values specified last override previous severities.
#[test]
fn cli_rule_severity_precedence() -> anyhow::Result<()> {
let case = CliTest::with_file(
"test.py",
r#"
y = 4 / 0
for a in range(0, int(y)):
x = a
prin(x) # unresolved-reference
"#,
)?;
// Assert that there's a `unresolved-reference` diagnostic (error) by default.
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `prin` used when not defined
--> test.py:7:1
|
5 | x = a
6 |
7 | prin(x) # unresolved-reference
| ^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"###);
assert_cmd_snapshot!(
case
.command()
.arg("--warn")
.arg("unresolved-reference")
.arg("--warn")
.arg("division-by-zero")
.arg("--ignore")
.arg("unresolved-reference"),
@r"
success: true
exit_code: 0
----- stdout -----
warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
--> test.py:2:5
|
2 | y = 4 / 0
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
info: rule `division-by-zero` was selected on the command line
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"
);
Ok(())
}
/// ty warns about unknown rules specified in a configuration file
#[test]
fn configuration_unknown_rules() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.rules]
division-by-zer = "warn" # incorrect rule name
"#,
),
("test.py", "print(10)"),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: true
exit_code: 0
----- stdout -----
warning[unknown-rule]
--> pyproject.toml:3:1
|
2 | [tool.ty.rules]
3 | division-by-zer = "warn" # incorrect rule name
| ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer`
|
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"#);
Ok(())
}
/// ty warns about unknown rules specified in a CLI argument
#[test]
fn cli_unknown_rules() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", "print(10)")?;
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r"
success: true
exit_code: 0
----- stdout -----
warning[unknown-rule]: Unknown lint rule `division-by-zer`
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}

View File

@@ -1,7 +1,7 @@
use std::cmp::Ordering;
use ruff_db::files::File;
use ruff_db::parsed::{ParsedModule, parsed_module};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast;
use ruff_python_parser::{Token, TokenAt, TokenKind};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -15,9 +15,9 @@ pub struct Completion {
}
pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion> {
let parsed = parsed_module(db.upcast(), file);
let parsed = parsed_module(db.upcast(), file).load(db.upcast());
let Some(target) = CompletionTargetTokens::find(parsed, offset).ast(parsed) else {
let Some(target) = CompletionTargetTokens::find(&parsed, offset).ast(&parsed) else {
return vec![];
};
@@ -63,7 +63,7 @@ enum CompletionTargetTokens<'t> {
impl<'t> CompletionTargetTokens<'t> {
/// Look for the best matching token pattern at the given offset.
fn find(parsed: &ParsedModule, offset: TextSize) -> CompletionTargetTokens<'_> {
fn find(parsed: &ParsedModuleRef, offset: TextSize) -> CompletionTargetTokens<'_> {
static OBJECT_DOT_EMPTY: [TokenKind; 2] = [TokenKind::Name, TokenKind::Dot];
static OBJECT_DOT_NON_EMPTY: [TokenKind; 3] =
[TokenKind::Name, TokenKind::Dot, TokenKind::Name];
@@ -97,7 +97,7 @@ impl<'t> CompletionTargetTokens<'t> {
/// Returns a corresponding AST node for these tokens.
///
/// If no plausible AST node could be found, then `None` is returned.
fn ast(&self, parsed: &'t ParsedModule) -> Option<CompletionTargetAst<'t>> {
fn ast(&self, parsed: &'t ParsedModuleRef) -> Option<CompletionTargetAst<'t>> {
match *self {
CompletionTargetTokens::ObjectDot { object, .. } => {
let covering_node = covering_node(parsed.syntax().into(), object.range())

View File

@@ -1,7 +1,7 @@
use crate::find_node::covering_node;
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModule, parsed_module};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -13,8 +13,8 @@ pub fn goto_type_definition(
file: File,
offset: TextSize,
) -> Option<RangedValue<NavigationTargets>> {
let parsed = parsed_module(db.upcast(), file);
let goto_target = find_goto_target(parsed, offset)?;
let module = parsed_module(db.upcast(), file).load(db.upcast());
let goto_target = find_goto_target(&module, offset)?;
let model = SemanticModel::new(db.upcast(), file);
let ty = goto_target.inferred_type(&model)?;
@@ -128,8 +128,8 @@ pub(crate) enum GotoTarget<'a> {
},
}
impl<'db> GotoTarget<'db> {
pub(crate) fn inferred_type(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
impl GotoTarget<'_> {
pub(crate) fn inferred_type<'db>(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
let ty = match self {
GotoTarget::Expression(expression) => expression.inferred_type(model),
GotoTarget::FunctionDef(function) => function.inferred_type(model),
@@ -183,7 +183,10 @@ impl Ranged for GotoTarget<'_> {
}
}
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
pub(crate) fn find_goto_target(
parsed: &ParsedModuleRef,
offset: TextSize,
) -> Option<GotoTarget<'_>> {
let token = parsed
.tokens()
.at_offset(offset)

View File

@@ -8,9 +8,9 @@ use std::fmt::Formatter;
use ty_python_semantic::SemanticModel;
use ty_python_semantic::types::Type;
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover>> {
let parsed = parsed_module(db.upcast(), file);
let goto_target = find_goto_target(parsed, offset)?;
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
let parsed = parsed_module(db.upcast(), file).load(db.upcast());
let goto_target = find_goto_target(&parsed, offset)?;
if let GotoTarget::Expression(expr) = goto_target {
if expr.is_literal_expr() {

View File

@@ -54,7 +54,7 @@ impl fmt::Display for DisplayInlayHint<'_, '_> {
pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec<InlayHint<'_>> {
let mut visitor = InlayHintVisitor::new(db, file, range);
let ast = parsed_module(db.upcast(), file);
let ast = parsed_module(db.upcast(), file).load(db.upcast());
visitor.visit_body(ast.suite());

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