Compare commits

..

21 Commits

Author SHA1 Message Date
Micha Reiser
6ca7407868 Options: Support #[serde(alias = name)]
Signed-off-by: Micha Reiser <micha@reiser.io>
2023-10-18 15:39:39 +09:00
Micha Reiser
f1b00cafd4 Respect tab-size setting in formatter 2023-10-18 15:29:43 +09:00
Steve C
dda4ceda71 add autofix for D301 (#7970)
## Summary

Add fix for `D301`

## Test Plan

`cargo test` and manually
2023-10-18 02:19:29 +00:00
Charlie Marsh
195c000f5a Avoid failed assertion when showing fixes from stdin (#8029)
## Summary

When linting, we store a map from file path to fixes, which we then use
to show a fix summary in the printer.

In the printer, we assume that if the map is non-empty, then we have at
least one fix. But this isn't enforced by the fix struct, since you can
have an entry from (file path) to (empty fix table). In practice, this
only bites us when linting from `stdin`, since when linting across
multiple files, we have an `AddAssign` on `Diagnostics` that avoids
adding empty entries to the map. When linting from `stdin`, we create
the map directly, and so it _is_ possible to have a non-empty map that
doesn't contain any fixes, leading to a panic.

This PR introduces a dedicated struct to make these constraints part of
the formal interface.

Closes https://github.com/astral-sh/ruff/issues/8027.

## Test Plan

`cargo test` (notice two failures are removed)
2023-10-17 21:50:39 -04:00
Charlie Marsh
a62c735f9e Lazily evaluate all PEP 695 type alias values (#8033)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

In https://github.com/astral-sh/ruff/pull/7968, I introduced a
regression whereby we started to treat imports used _only_ in type
annotation bounds (with `__future__` annotations) as unused.

The root of the issue is that I started using `visit_annotation` for
these bounds. So we'd queue up the bound in the list of deferred type
parameters, then when visiting, we'd further queue it up in the list of
deferred type annotations... Which we'd then never visit, since deferred
type annotations are visited _before_ deferred type parameters.

Anyway, the better solution here is to use a dedicated flag for these,
since they have slightly different behavior than type annotations.

I've also fixed what I _think_ is a bug whereby we previously failed to
resolve `Callable` in:

```python
type RecordCallback[R: Record] = Callable[[R], None]

from collections.abc import Callable
```

IIUC, the values in type aliases should be evaluated lazily, like type
parameters.

Closes https://github.com/astral-sh/ruff/issues/8017.

## Test Plan

`cargo test`
2023-10-17 21:50:26 -04:00
Micha Reiser
94b4bb0f57 Add lint.preview (#8002) 2023-10-18 01:26:37 +00:00
Micha Reiser
fe485d791c Add [format|lint].exclude options (#8000) 2023-10-18 01:15:25 +00:00
Charlie Marsh
d685107638 Move {AnyNodeRef, AstNode} to ruff_python_ast crate root (#8030)
This is a do-over of https://github.com/astral-sh/ruff/pull/8011, which
I accidentally merged into a non-`main` branch. Sorry!
2023-10-18 00:01:18 +00:00
Ahmed Ashraf
d85950ce5a Update rule B005 docs (#8028)
## Summary

Rule B005 of flake8-bugbear docs has a typo in one of the examples that
leads to a confusion in the correctness of `.strip()` method


![image](https://github.com/astral-sh/ruff/assets/104530599/b4e19751-558e-4ebb-b82f-25c321ddc32b)

```python
# Wrong output (used in docs) 
"text.txt".strip(".txt")  # "ex" 

# Correct output
"text.txt".strip(".txt")  # "e"
```
2023-10-17 18:32:39 -04:00
Eiko Wagenknecht
88c0106421 [docs] fix typo (#8013)
## Summary

Fix a typo in the docs for quote style.

> a = "a string without any quotes"
> b = "It's monday morning"
> Ruff will change a to use single quotes when using quote-style =
"single". However, a will be unchanged, as converting to single quotes
would require the inner ' to be escaped, which leads to less readable
code: 'It\'s monday morning'.

It should read "However, **b** will be unchanged".

## Test Plan

N/A.
2023-10-17 14:16:28 +00:00
Zanie Blue
f60aa85471 Update GitHub actions example in docs to use --output-format (#8014) 2023-10-17 09:13:24 -05:00
Charlie Marsh
d942a777d7 Avoid flagging bad-dunder-method-name for _ (#8015)
This is almost certainly _not_ an accidentally mistyped dunder method.
Closes https://github.com/astral-sh/ruff/issues/8005.
2023-10-17 10:13:04 -04:00
Steve C
8a529925b3 Add autofix for D300 (#7967)
## Summary

Add fix for `D300`

## Test Plan

`cargo test` and manually
2023-10-17 09:37:46 -04:00
dependabot[bot]
dc6b4ad2b4 Bump tracing from 0.1.37 to 0.1.39 (#7978)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-10-17 07:46:53 +00:00
Steve C
21ea290d6a [pylint] Implement PLR0916 (too-many-boolean-expressions) (#7975)
## Summary

Add
[R0916](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/too-many-boolean-expressions.html),
no autofix available.

See: #970 

## Test Plan

`cargo test` and manually.
2023-10-17 04:44:25 +00:00
Steve C
5da0f9111e implement PLR6201 with autofix (#7973)
## Summary

Implements
[R6201](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/use-set-for-membership.html)
along with the autofix!

See #970 

## Test Plan

`cargo test`, and manually
2023-10-17 04:15:21 +00:00
Charlie Marsh
cb6d74c27b Use set bracket replacement for iteration-over-set (#8001) 2023-10-17 04:12:05 +00:00
Sven Hager
73049df3ed Implement the pylint rule PLW1514 (unspecified-encoding) (#7939)
## Summary

Implemented the pylint rule W1514 ( unspecified-encoding).
See also:
https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/unspecified-encoding.html


## Test Plan

Tested it with the submitted test case.
Additionally, we tested the new ruff rule (PLW1514) on our proprietary
Python code base.
2023-10-17 03:49:48 +00:00
Clément Schreiner
bf0e5788ef [pylint] Implement misplaced-bare-raise (E0704) (#7961)
## Summary

### What it does
This rule triggers an error when a bare raise statement is not in an
except or finally block.
### Why is this bad?
If raise statement is not in an except or finally block, there is no
active exception to
re-raise, so it will fail with a `RuntimeError` exception.
### Example
```python
def validate_positive(x):
   if x <= 0:
       raise
```
Use instead:
```python
def validate_positive(x):
   if x <= 0:
       raise ValueError(f"{x} is not positive")
```

## Test Plan

Added unit test and snapshot.
Manually compared ruff and pylint outputs on pylint's tests.

## References

- [pylint
documentation](https://pylint.pycqa.org/en/stable/user_guide/messages/error/misplaced-bare-raise.html)
- [pylint
implementation](https://github.com/pylint-dev/pylint/blob/main/pylint/checkers/exceptions.py#L339)
2023-10-17 03:07:46 +00:00
Zanie Blue
4113d65836 Rename RuleGroup::Unspecified to Stable (#7991)
Should help with #7989 and seems more accurate for our new model
2023-10-16 14:53:27 -05:00
Clément Schreiner
4c2c9bf7e0 [docs] Clarify that new rules should be added to RuleGroup::Preview. (#7989)
In the contributing page, clarify that new rules must be added to
`RuleGroup::Preview` when mapping their code.
2023-10-16 15:14:09 -04:00
155 changed files with 3252 additions and 1256 deletions

View File

@@ -170,7 +170,8 @@ At a high level, the steps involved in adding a new lint rule are as follows:
statements, like imports) or `analyze/expression.rs` (if your rule is based on analyzing
expressions, like function calls).
1. Map the violation struct to a rule code in `crates/ruff_linter/src/codes.rs` (e.g., `B011`).
1. Map the violation struct to a rule code in `crates/ruff_linter/src/codes.rs` (e.g., `B011`). New rules
should be added in `RuleGroup::Preview`.
1. Add proper [testing](#rule-testing-fixtures-and-snapshots) for your rule.

35
Cargo.lock generated
View File

@@ -2259,6 +2259,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"serde_derive_internals 0.29.0",
"syn 2.0.38",
]
@@ -2624,7 +2625,7 @@ checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"serde_derive_internals 0.26.0",
"syn 1.0.109",
]
@@ -2664,9 +2665,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.188"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
dependencies = [
"serde_derive",
]
@@ -2684,9 +2685,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.188"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
dependencies = [
"proc-macro2",
"quote",
@@ -2704,6 +2705,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "serde_json"
version = "1.0.107"
@@ -3104,11 +3116,10 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.37"
version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
@@ -3117,9 +3128,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.26"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
@@ -3128,9 +3139,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.31"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",

View File

@@ -34,7 +34,7 @@ quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.15" }
serde = { version = "1.0.152", features = ["derive"] }
serde = { version = "1.0.189", features = ["derive"] }
serde_json = { version = "1.0.107" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.3.0", features = ["inline"] }
@@ -46,7 +46,7 @@ syn = { version = "2.0.38" }
test-case = { version = "3.2.1" }
thiserror = { version = "1.0.49" }
toml = { version = "0.7.8" }
tracing = { version = "0.1.37" }
tracing = { version = "0.1.39" }
tracing-indicatif = { version = "0.3.4" }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-ident = { version = "1.0.12" }

View File

@@ -17,8 +17,8 @@ use ruff_linter::settings::DEFAULT_SELECTORS;
use ruff_linter::warn_user;
use ruff_workspace::options::{
Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintOptions,
McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintCommonOptions,
LintOptions, McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
};
use ruff_workspace::pyproject::Pyproject;
@@ -99,7 +99,7 @@ pub(crate) fn convert(
// Parse each supported option.
let mut options = Options::default();
let mut lint_options = LintOptions::default();
let mut lint_options = LintCommonOptions::default();
let mut flake8_annotations = Flake8AnnotationsOptions::default();
let mut flake8_bugbear = Flake8BugbearOptions::default();
let mut flake8_builtins = Flake8BuiltinsOptions::default();
@@ -433,8 +433,11 @@ pub(crate) fn convert(
}
}
if lint_options != LintOptions::default() {
options.lint = Some(lint_options);
if lint_options != LintCommonOptions::default() {
options.lint = Some(LintOptions {
common: lint_options,
..LintOptions::default()
});
}
// Create the pyproject.toml.
@@ -465,7 +468,9 @@ mod tests {
use ruff_linter::rules::flake8_quotes;
use ruff_linter::rules::pydocstyle::settings::Convention;
use ruff_linter::settings::types::PythonVersion;
use ruff_workspace::options::{Flake8QuotesOptions, LintOptions, Options, PydocstyleOptions};
use ruff_workspace::options::{
Flake8QuotesOptions, LintCommonOptions, LintOptions, Options, PydocstyleOptions,
};
use ruff_workspace::pyproject::Pyproject;
use crate::converter::DEFAULT_SELECTORS;
@@ -475,8 +480,8 @@ mod tests {
use super::super::plugin::Plugin;
use super::convert;
fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintOptions {
LintOptions {
fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintCommonOptions {
LintCommonOptions {
ignore: Some(vec![]),
select: Some(
DEFAULT_SELECTORS
@@ -486,7 +491,7 @@ mod tests {
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
),
..LintOptions::default()
..LintCommonOptions::default()
}
}
@@ -498,7 +503,10 @@ mod tests {
None,
);
let expected = Pyproject::new(Options {
lint: Some(lint_default_options([])),
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
@@ -516,7 +524,10 @@ mod tests {
);
let expected = Pyproject::new(Options {
line_length: Some(LineLength::try_from(100).unwrap()),
lint: Some(lint_default_options([])),
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
@@ -534,7 +545,10 @@ mod tests {
);
let expected = Pyproject::new(Options {
line_length: Some(LineLength::try_from(100).unwrap()),
lint: Some(lint_default_options([])),
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
@@ -551,7 +565,10 @@ mod tests {
Some(vec![]),
);
let expected = Pyproject::new(Options {
lint: Some(lint_default_options([])),
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);
@@ -569,13 +586,16 @@ mod tests {
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..lint_default_options([])
common: LintCommonOptions {
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..lint_default_options([])
},
..LintOptions::default()
}),
..Options::default()
});
@@ -597,12 +617,15 @@ mod tests {
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
pydocstyle: Some(PydocstyleOptions {
convention: Some(Convention::Numpy),
ignore_decorators: None,
property_decorators: None,
}),
..lint_default_options([Linter::Pydocstyle.into()])
common: LintCommonOptions {
pydocstyle: Some(PydocstyleOptions {
convention: Some(Convention::Numpy),
ignore_decorators: None,
property_decorators: None,
}),
..lint_default_options([Linter::Pydocstyle.into()])
},
..LintOptions::default()
}),
..Options::default()
});
@@ -621,13 +644,16 @@ mod tests {
);
let expected = Pyproject::new(Options {
lint: Some(LintOptions {
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..lint_default_options([Linter::Flake8Quotes.into()])
common: LintCommonOptions {
flake8_quotes: Some(Flake8QuotesOptions {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
..lint_default_options([Linter::Flake8Quotes.into()])
},
..LintOptions::default()
}),
..Options::default()
});
@@ -648,7 +674,10 @@ mod tests {
);
let expected = Pyproject::new(Options {
target_version: Some(PythonVersion::Py38),
lint: Some(lint_default_options([])),
lint: Some(LintOptions {
common: lint_default_options([]),
..LintOptions::default()
}),
..Options::default()
});
assert_eq!(actual, expected);

View File

@@ -366,6 +366,15 @@ pub struct FormatCommand {
respect_gitignore: bool,
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
no_respect_gitignore: bool,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub exclude: Option<Vec<FilePattern>>,
/// Enforce exclusions, even for paths passed to Ruff directly on the command-line.
/// Use `--no-force-exclude` to disable.
#[arg(
@@ -386,9 +395,9 @@ pub struct FormatCommand {
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// Enable preview mode; checks will include unstable rules and fixes.
/// Enable preview mode; enables unstable formatting.
/// Use `--no-preview` to disable.
#[arg(long, overrides_with("no_preview"), hide = true)]
#[arg(long, overrides_with("no_preview"))]
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
@@ -522,6 +531,7 @@ impl FormatCommand {
self.respect_gitignore,
self.no_respect_gitignore,
),
exclude: self.exclude,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
// Unsupported on the formatter CLI, but required on `Overrides`.
@@ -658,6 +668,8 @@ impl ConfigurationTransformer for CliOverrides {
}
if let Some(preview) = &self.preview {
config.preview = Some(*preview);
config.lint.preview = Some(*preview);
config.format.preview = Some(*preview);
}
if let Some(per_file_ignores) = &self.per_file_ignores {
config.lint.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));

View File

@@ -10,7 +10,7 @@ use ruff_linter::linter::add_noqa_to_path;
use ruff_linter::source_kind::SourceKind;
use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::CliOverrides;
@@ -36,7 +36,7 @@ pub(crate) fn add_noqa(
&paths
.iter()
.flatten()
.map(ignore::DirEntry::path)
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
pyproject_config,
);
@@ -45,14 +45,15 @@ pub(crate) fn add_noqa(
let modifications: usize = paths
.par_iter()
.flatten()
.filter_map(|entry| {
let path = entry.path();
.filter_map(|resolved_file| {
let SourceType::Python(source_type @ (PySourceType::Python | PySourceType::Stub)) =
SourceType::from(path)
SourceType::from(resolved_file.path())
else {
return None;
};
let package = path
let path = resolved_file.path();
let package = resolved_file
.path()
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);

View File

@@ -22,7 +22,10 @@ use ruff_linter::{fs, warn_user_once, IOError};
use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, PyprojectDiscoveryStrategy};
use ruff_workspace::resolver::{
match_exclusion, python_files_in_path, PyprojectConfig, PyprojectDiscoveryStrategy,
ResolvedFile,
};
use crate::args::CliOverrides;
use crate::cache::{self, Cache};
@@ -42,8 +45,7 @@ pub(crate) fn check(
// Collect all the Python files to check.
let start = Instant::now();
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
debug!("Identified files to lint in: {:?}", start.elapsed());
if paths.is_empty() {
warn_user_once!("No Python files found under the given path(s)");
@@ -77,7 +79,7 @@ pub(crate) fn check(
&paths
.iter()
.flatten()
.map(ignore::DirEntry::path)
.map(ResolvedFile::path)
.collect::<Vec<_>>(),
pyproject_config,
);
@@ -98,95 +100,114 @@ pub(crate) fn check(
});
let start = Instant::now();
let mut diagnostics: Diagnostics = paths
.par_iter()
.map(|entry| {
match entry {
Ok(entry) => {
let path = entry.path();
let package = path
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let diagnostics_per_file = paths.par_iter().filter_map(|resolved_file| {
let result = match resolved_file {
Ok(resolved_file) => {
let path = resolved_file.path();
let package = path
.parent()
.and_then(|parent| package_roots.get(parent))
.and_then(|package| *package);
let settings = resolver.resolve(path, pyproject_config);
let settings = resolver.resolve(path, pyproject_config);
let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path));
let cache = caches.as_ref().and_then(|caches| {
if let Some(cache) = caches.get(&cache_root) {
Some(cache)
} else {
debug!("No cache found for {}", cache_root.display());
None
}
});
lint_path(
path,
package,
&settings.linter,
cache,
noqa,
fix_mode,
unsafe_fixes,
if !resolved_file.is_root()
&& match_exclusion(
resolved_file.path(),
resolved_file.file_name(),
&settings.linter.exclude,
)
.map_err(|e| {
(Some(path.to_owned()), {
let mut error = e.to_string();
for cause in e.chain() {
write!(&mut error, "\n Cause: {cause}").unwrap();
}
error
})
})
{
return None;
}
Err(e) => Err((
if let Error::WithPath { path, .. } = e {
Some(path.clone())
} else {
None
},
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, pyproject_config);
if settings.linter.rules.enabled(Rule::IOError) {
let dummy =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
Diagnostics::new(
vec![Message::from_diagnostic(
Diagnostic::new(IOError { message }, TextRange::default()),
dummy,
TextSize::default(),
)],
ImportMap::default(),
FxHashMap::default(),
)
let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path));
let cache = caches.as_ref().and_then(|caches| {
if let Some(cache) = caches.get(&cache_root) {
Some(cache)
} else {
warn!(
"{}{}{} {message}",
"Failed to lint ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
Diagnostics::default()
debug!("No cache found for {}", cache_root.display());
None
}
});
lint_path(
path,
package,
&settings.linter,
cache,
noqa,
fix_mode,
unsafe_fixes,
)
.map_err(|e| {
(Some(path.to_path_buf()), {
let mut error = e.to_string();
for cause in e.chain() {
write!(&mut error, "\n Cause: {cause}").unwrap();
}
error
})
})
}
Err(e) => Err((
if let Error::WithPath { path, .. } = e {
Some(path.clone())
} else {
warn!("{} {message}", "Encountered error:".bold());
None
},
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
};
Some(result.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, pyproject_config);
if settings.linter.rules.enabled(Rule::IOError) {
let dummy =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
Diagnostics::new(
vec![Message::from_diagnostic(
Diagnostic::new(IOError { message }, TextRange::default()),
dummy,
TextSize::default(),
)],
ImportMap::default(),
FxHashMap::default(),
)
} else {
warn!(
"{}{}{} {message}",
"Failed to lint ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
Diagnostics::default()
}
})
})
.reduce(Diagnostics::default, |mut acc, item| {
acc += item;
acc
});
} else {
warn!("{} {message}", "Encountered error:".bold());
Diagnostics::default()
}
}))
});
diagnostics.messages.sort();
// Aggregate the diagnostics of all checked files and count the checked files.
// This can't be a regular for loop because we use `par_iter`.
let (mut all_diagnostics, checked_files) = diagnostics_per_file
.fold(
|| (Diagnostics::default(), 0u64),
|(all_diagnostics, checked_files), file_diagnostics| {
(all_diagnostics + file_diagnostics, checked_files + 1)
},
)
.reduce(
|| (Diagnostics::default(), 0u64),
|a, b| (a.0 + b.0, a.1 + b.1),
);
all_diagnostics.messages.sort();
// Store the caches.
if let Some(caches) = caches {
@@ -196,9 +217,9 @@ pub(crate) fn check(
}
let duration = start.elapsed();
debug!("Checked {:?} files in: {:?}", paths.len(), duration);
debug!("Checked {:?} files in: {:?}", checked_files, duration);
Ok(diagnostics)
Ok(all_diagnostics)
}
/// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use ruff_linter::packaging;
use ruff_linter::settings::flags;
use ruff_workspace::resolver::{python_file_at_path, PyprojectConfig};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig};
use crate::args::CliOverrides;
use crate::diagnostics::{lint_stdin, Diagnostics};
@@ -22,6 +22,14 @@ pub(crate) fn check_stdin(
if !python_file_at_path(filename, pyproject_config, overrides)? {
return Ok(Diagnostics::default());
}
let lint_settings = &pyproject_config.settings.linter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
{
return Ok(Diagnostics::default());
}
}
let package_root = filename.and_then(Path::parent).and_then(|path| {
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)

View File

@@ -20,7 +20,7 @@ use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, FormatModuleError};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::python_files_in_path;
use ruff_workspace::resolver::{match_exclusion, python_files_in_path};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -61,26 +61,42 @@ pub(crate) fn format(
}
let start = Instant::now();
let (results, errors): (Vec<_>, Vec<_>) = paths
let (mut results, errors): (Vec<_>, Vec<_>) = paths
.into_par_iter()
.filter_map(|entry| {
match entry {
Ok(entry) => {
let path = entry.into_path();
Ok(resolved_file) => {
let path = resolved_file.path();
let SourceType::Python(source_type) = SourceType::from(&path) else {
// Ignore any non-Python files.
return None;
};
let resolved_settings = resolver.resolve(&path, &pyproject_config);
let resolved_settings = resolver.resolve(path, &pyproject_config);
// Ignore files that are excluded from formatting
if !resolved_file.is_root()
&& match_exclusion(
path,
resolved_file.file_name(),
&resolved_settings.formatter.exclude,
)
{
return None;
}
Some(
match catch_unwind(|| {
format_path(&path, &resolved_settings.formatter, source_type, mode)
format_path(path, &resolved_settings.formatter, source_type, mode)
}) {
Ok(inner) => inner.map(|result| FormatPathResult { path, result }),
Err(error) => Err(FormatCommandError::Panic(Some(path), error)),
Ok(inner) => inner.map(|result| FormatPathResult {
path: resolved_file.into_path(),
result,
}),
Err(error) => Err(FormatCommandError::Panic(
Some(resolved_file.into_path()),
error,
)),
},
)
}
@@ -104,6 +120,8 @@ pub(crate) fn format(
error!("{error}");
}
results.sort_unstable_by(|a, b| a.path.cmp(&b.path));
let summary = FormatSummary::new(results.as_slice(), mode);
// Report on the formatting changes.
@@ -137,7 +155,7 @@ pub(crate) fn format(
}
/// Format the file at the given [`Path`].
#[tracing::instrument(skip_all, fields(path = %path.display()))]
#[tracing::instrument(level="debug", skip_all, fields(path = %path.display()))]
fn format_path(
path: &Path,
settings: &FormatterSettings,

View File

@@ -6,7 +6,7 @@ use log::error;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::python_file_at_path;
use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -33,6 +33,14 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
if !python_file_at_path(filename, &pyproject_config, overrides)? {
return Ok(ExitStatus::Success);
}
let format_settings = &pyproject_config.settings.formatter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{
return Ok(ExitStatus::Success);
}
}
let path = cli.stdin_filename.as_deref();

View File

@@ -5,7 +5,7 @@ use anyhow::Result;
use itertools::Itertools;
use ruff_linter::warn_user_once;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::CliOverrides;
@@ -25,12 +25,13 @@ pub(crate) fn show_files(
}
// Print the list of files.
for entry in paths
.iter()
for path in paths
.into_iter()
.flatten()
.sorted_by(|a, b| a.path().cmp(b.path()))
.map(ResolvedFile::into_path)
.sorted_unstable()
{
writeln!(writer, "{}", entry.path().to_string_lossy())?;
writeln!(writer, "{}", path.to_string_lossy())?;
}
Ok(())

View File

@@ -4,7 +4,7 @@ use std::path::PathBuf;
use anyhow::{bail, Result};
use itertools::Itertools;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
use crate::args::CliOverrides;
@@ -19,16 +19,17 @@ pub(crate) fn show_settings(
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
// Print the list of files.
let Some(entry) = paths
.iter()
let Some(path) = paths
.into_iter()
.flatten()
.sorted_by(|a, b| a.path().cmp(b.path()))
.map(ResolvedFile::into_path)
.sorted_unstable()
.next()
else {
bail!("No files found under the given path");
};
let path = entry.path();
let settings = resolver.resolve(path, pyproject_config);
let settings = resolver.resolve(&path, pyproject_config);
writeln!(writer, "Resolved settings for: {path:?}")?;
if let Some(settings_path) = pyproject_config.path.as_ref() {

View File

@@ -2,7 +2,7 @@
use std::fs::File;
use std::io;
use std::ops::AddAssign;
use std::ops::{Add, AddAssign};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
@@ -11,7 +11,6 @@ use anyhow::{Context, Result};
use colored::Colorize;
use filetime::FileTime;
use log::{debug, error, warn};
use ruff_linter::settings::types::UnsafeFixes;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@@ -20,6 +19,7 @@ use ruff_linter::logging::DisplayParseError;
use ruff_linter::message::Message;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::registry::AsRule;
use ruff_linter::settings::types::UnsafeFixes;
use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::{SourceError, SourceKind};
use ruff_linter::{fs, IOError, SyntaxError};
@@ -61,7 +61,7 @@ impl FileCacheKey {
#[derive(Debug, Default, PartialEq)]
pub(crate) struct Diagnostics {
pub(crate) messages: Vec<Message>,
pub(crate) fixed: FxHashMap<String, FixTable>,
pub(crate) fixed: FixMap,
pub(crate) imports: ImportMap,
pub(crate) notebook_indexes: FxHashMap<String, NotebookIndex>,
}
@@ -74,7 +74,7 @@ impl Diagnostics {
) -> Self {
Self {
messages,
fixed: FxHashMap::default(),
fixed: FixMap::default(),
imports,
notebook_indexes,
}
@@ -142,22 +142,68 @@ impl Diagnostics {
}
}
impl Add for Diagnostics {
type Output = Diagnostics;
fn add(mut self, other: Self) -> Self::Output {
self += other;
self
}
}
impl AddAssign for Diagnostics {
fn add_assign(&mut self, other: Self) {
self.messages.extend(other.messages);
self.imports.extend(other.imports);
for (filename, fixed) in other.fixed {
self.fixed += other.fixed;
self.notebook_indexes.extend(other.notebook_indexes);
}
}
/// A collection of fixes indexed by file path.
#[derive(Debug, Default, PartialEq)]
pub(crate) struct FixMap(FxHashMap<String, FixTable>);
impl FixMap {
/// Returns `true` if there are no fixes in the map.
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Returns an iterator over the fixes in the map, along with the file path.
pub(crate) fn iter(&self) -> impl Iterator<Item = (&String, &FixTable)> {
self.0.iter()
}
/// Returns an iterator over the fixes in the map.
pub(crate) fn values(&self) -> impl Iterator<Item = &FixTable> {
self.0.values()
}
}
impl FromIterator<(String, FixTable)> for FixMap {
fn from_iter<T: IntoIterator<Item = (String, FixTable)>>(iter: T) -> Self {
Self(
iter.into_iter()
.filter(|(_, fixes)| !fixes.is_empty())
.collect(),
)
}
}
impl AddAssign for FixMap {
fn add_assign(&mut self, rhs: Self) {
for (filename, fixed) in rhs.0 {
if fixed.is_empty() {
continue;
}
let fixed_in_file = self.fixed.entry(filename).or_default();
let fixed_in_file = self.0.entry(filename).or_default();
for (rule, count) in fixed {
if count > 0 {
*fixed_in_file.entry(rule).or_default() += count;
}
}
}
self.notebook_indexes.extend(other.notebook_indexes);
}
}
@@ -318,7 +364,7 @@ pub(crate) fn lint_path(
Ok(Diagnostics {
messages,
fixed: FxHashMap::from_iter([(fs::relativize_path(path), fixed)]),
fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]),
imports,
notebook_indexes,
})
@@ -436,7 +482,7 @@ pub(crate) fn lint_stdin(
Ok(Diagnostics {
messages,
fixed: FxHashMap::from_iter([(
fixed: FixMap::from_iter([(
fs::relativize_path(path.unwrap_or_else(|| Path::new("-"))),
fixed,
)]),

View File

@@ -7,11 +7,9 @@ use anyhow::Result;
use bitflags::bitflags;
use colored::Colorize;
use itertools::{iterate, Itertools};
use rustc_hash::FxHashMap;
use serde::Serialize;
use ruff_linter::fs::relativize_path;
use ruff_linter::linter::FixTable;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
@@ -22,7 +20,7 @@ use ruff_linter::registry::{AsRule, Rule};
use ruff_linter::settings::flags::{self};
use ruff_linter::settings::types::{SerializationFormat, UnsafeFixes};
use crate::diagnostics::Diagnostics;
use crate::diagnostics::{Diagnostics, FixMap};
bitflags! {
#[derive(Default, Debug, Copy, Clone)]
@@ -462,7 +460,7 @@ fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableStatistics
(!fix_mode.is_apply()) && fixables.is_some_and(FixableStatistics::any_applicable_fixes)
}
fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap<String, FixTable>) -> Result<()> {
fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap) -> Result<()> {
let total = fixed
.values()
.map(|table| table.values().sum::<usize>())

View File

@@ -13,7 +13,7 @@ const BIN_NAME: &str = "ruff";
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated"])
.args(["format", "--isolated", "--stdin-filename", "test.py"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
@@ -50,6 +50,9 @@ fn format_options() -> Result<()> {
fs::write(
&ruff_toml,
r#"
tab-size = 8
line-length = 84
[format]
indent-style = "tab"
quote-style = "single"
@@ -64,7 +67,7 @@ line-ending = "cr-lf"
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't change quotes")
print("Shouldn't change quotes. It exceeds the line width with the tab size 8")
if condition:
@@ -76,7 +79,9 @@ if condition:
exit_code: 0
----- stdout -----
def foo(arg1, arg2):
print("Shouldn't change quotes")
print(
"Shouldn't change quotes. It exceeds the line width with the tab size 8"
)
if condition:
@@ -88,6 +93,108 @@ if condition:
Ok(())
}
#[test]
fn exclude() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-exclude = ["out"]
[format]
exclude = ["test.py", "generated.py"]
"#,
)?;
fs::write(
tempdir.path().join("main.py"),
r#"
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#,
)?;
// Excluded file but passed to the CLI directly, should be formatted
let test_path = tempdir.path().join("test.py");
fs::write(
&test_path,
r#"
def say_hy(name: str):
print(f"Hy {name}")"#,
)?;
fs::write(
tempdir.path().join("generated.py"),
r#"NUMBERS = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
]
OTHER = "OTHER"
"#,
)?;
let out_dir = tempdir.path().join("out");
fs::create_dir(&out_dir)?;
fs::write(out_dir.join("a.py"), "a = a")?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--check", "--config"])
.arg(ruff_toml.file_name().unwrap())
// Explicitly pass test.py, should be formatted regardless of it being excluded by format.exclude
.arg(test_path.file_name().unwrap())
// Format all other files in the directory, should respect the `exclude` and `format.exclude` options
.arg("."), @r###"
success: false
exit_code: 1
----- stdout -----
Would reformat: main.py
Would reformat: test.py
2 files would be reformatted
----- stderr -----
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
Ok(())
}
#[test]
fn exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
[format]
exclude = ["generated.py"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--config", &ruff_toml.file_name().unwrap().to_string_lossy(), "--stdin-filename", "generated.py", "-"])
.pass_stdin(r#"
from test import say_hy
if __name__ == '__main__':
say_hy("dear Ruff contributor")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
Ok(())
}
#[test]
fn format_option_inheritance() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -1252,8 +1252,8 @@ fn diff_does_not_show_display_only_fixes_with_unsafe_fixes_enabled() {
])
.pass_stdin("def add_to_list(item, some_list=[]): ..."),
@r###"
success: false
exit_code: 1
success: true
exit_code: 0
----- stdout -----
----- stderr -----
@@ -1276,8 +1276,8 @@ fn diff_only_unsafe_fixes_available() {
])
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
success: true
exit_code: 0
----- stdout -----
----- stderr -----

View File

@@ -31,14 +31,15 @@ inline-quotes = "single"
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.args(["--stdin-filename", "test.py"])
.arg("-")
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
success: false
exit_code: 1
----- stdout -----
-:1:5: Q000 [*] Double quotes found but single quotes preferred
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
-:1:19: Q000 [*] Double quotes found but single quotes preferred
test.py:1:5: Q000 [*] Double quotes found but single quotes preferred
test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading
test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 2 fixable with the `--fix` option.
@@ -155,3 +156,117 @@ inline-quotes = "single"
"###);
Ok(())
}
#[test]
fn exclude() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
extend-exclude = ["out"]
[lint]
exclude = ["test.py", "generated.py"]
[lint.flake8-quotes]
inline-quotes = "single"
"#,
)?;
fs::write(
tempdir.path().join("main.py"),
r#"
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#,
)?;
// Excluded file but passed to the CLI directly, should be linted
let test_path = tempdir.path().join("test.py");
fs::write(
&test_path,
r#"
def say_hy(name: str):
print(f"Hy {name}")"#,
)?;
fs::write(
tempdir.path().join("generated.py"),
r#"NUMBERS = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
]
OTHER = "OTHER"
"#,
)?;
let out_dir = tempdir.path().join("out");
fs::create_dir(&out_dir)?;
fs::write(out_dir.join("a.py"), r#"a = "a""#)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
// Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
.arg(test_path.file_name().unwrap())
// Lint all other files in the directory, should respect the `exclude` and `lint.exclude` options
.arg("."), @r###"
success: false
exit_code: 1
----- stdout -----
main.py:4:16: Q000 [*] Double quotes found but single quotes preferred
main.py:5:12: Q000 [*] Double quotes found but single quotes preferred
test.py:3:15: Q000 [*] Double quotes found but single quotes preferred
Found 3 errors.
[*] 3 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}
#[test]
fn exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
[lint]
exclude = ["generated.py"]
[lint.flake8-quotes]
inline-quotes = "single"
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("check")
.args(STDIN_BASE_OPTIONS)
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--stdin-filename", "generated.py"])
.arg("-")
.pass_stdin(r#"
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"###);
Ok(())
}

View File

@@ -11,7 +11,6 @@ use std::{fmt, fs, io, iter};
use anyhow::{bail, format_err, Context, Error};
use clap::{CommandFactory, FromArgMatches};
use ignore::DirEntry;
use imara_diff::intern::InternedInput;
use imara_diff::sink::Counter;
use imara_diff::{diff, Algorithm};
@@ -36,14 +35,14 @@ use ruff_linter::settings::types::{FilePattern, FilePatternSet};
use ruff_python_formatter::{
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, Resolver};
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
#[allow(clippy::type_complexity)]
fn ruff_check_paths(
dirs: &[PathBuf],
) -> anyhow::Result<(
Vec<Result<DirEntry, ignore::Error>>,
Vec<Result<ResolvedFile, ignore::Error>>,
Resolver,
PyprojectConfig,
)> {
@@ -467,9 +466,9 @@ fn format_dev_project(
let iter = { paths.into_par_iter() };
#[cfg(feature = "singlethreaded")]
let iter = { paths.into_iter() };
iter.map(|dir_entry| {
iter.map(|path| {
let result = format_dir_entry(
dir_entry,
path,
stability_check,
write,
&black_options,
@@ -527,24 +526,20 @@ fn format_dev_project(
/// Error handling in between walkdir and `format_dev_file`
fn format_dir_entry(
dir_entry: Result<DirEntry, ignore::Error>,
resolved_file: Result<ResolvedFile, ignore::Error>,
stability_check: bool,
write: bool,
options: &BlackOptions,
resolver: &Resolver,
pyproject_config: &PyprojectConfig,
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
let dir_entry = match dir_entry.context("Iterating the files in the repository failed") {
Ok(dir_entry) => dir_entry,
Err(err) => return Err(err),
};
let file = dir_entry.path().to_path_buf();
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
// For some reason it does not filter in the beginning
if dir_entry.file_name() == "pyproject.toml" {
return Ok((Ok(Statistics::default()), file));
if resolved_file.file_name() == "pyproject.toml" {
return Ok((Ok(Statistics::default()), resolved_file.into_path()));
}
let path = dir_entry.path().to_path_buf();
let path = resolved_file.into_path();
let mut options = options.to_py_format_options(&path);
let settings = resolver.resolve(&path, pyproject_config);

View File

@@ -1,6 +1,7 @@
//! Generate a Markdown-compatible listing of configuration options for `pyproject.toml`.
//!
//! Used for <https://docs.astral.sh/ruff/settings/>.
use itertools::Itertools;
use std::fmt::Write;
use ruff_workspace::options::Options;
@@ -107,6 +108,24 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
output.push('\n');
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
output.push('\n');
if !field.aliases.is_empty() {
let title = if field.aliases.len() == 1 {
"Alias"
} else {
"Aliases"
};
output.push_str(&format!(
"**{title}**: {}\n",
field
.aliases
.iter()
.map(|alias| format!("`{alias}`"))
.join(", ")
));
output.push('\n');
}
output.push_str(&format!(
"**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n",
if let Some(set_name) = parent_set.name() {

View File

@@ -95,7 +95,7 @@ impl std::fmt::Display for IndentStyle {
///
/// Determines the visual width of a tab character (`\t`) and the number of
/// spaces per indent when using [`IndentStyle::Space`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct IndentWidth(NonZeroU8);

View File

@@ -54,7 +54,7 @@ impl<'a> Printer<'a> {
/// Prints the passed in element as well as all its content,
/// starting at the specified indentation level
#[tracing::instrument(name = "Printer::print", skip_all)]
#[tracing::instrument(level = "debug", name = "Printer::print", skip_all)]
pub fn print_with_indent(
mut self,
document: &'a Document,

View File

@@ -0,0 +1,10 @@
def with_backslash():
"""Sum\\mary."""
def ends_in_quote():
'Sum\\mary."'
def contains_quote():
'Sum"\\mary.'

View File

@@ -10,6 +10,10 @@ def double_quotes_backslash_uppercase():
R"""Sum\\mary."""
def shouldnt_add_raw_here():
"Ruff \U000026a1"
def make_unique_pod_id(pod_id: str) -> str | None:
r"""
Generate a unique Pod name.

View File

@@ -0,0 +1,17 @@
"""Test that type parameters are considered used."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
from .foo import Record as Record1
from .bar import Record as Record2
type RecordCallback[R: Record1] = Callable[[R], None]
def process_record[R: Record2](record: R) -> None:
...

View File

@@ -0,0 +1,5 @@
"""Test lazy evaluation of type alias values."""
type RecordCallback[R: Record] = Callable[[R], None]
from collections.abc import Callable

View File

@@ -68,6 +68,10 @@ class Apples:
def _missing_(cls, value):
pass
# Allow anonymous functions.
def _(self):
pass
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
...

View File

@@ -6,6 +6,14 @@ for item in {1}:
for item in {"apples", "lemons", "water"}: # flags in-line set literals
print(f"I like {item}.")
for item in {1,}:
print(f"I can count to {item}!")
for item in {
"apples", "lemons", "water"
}: # flags in-line set literals
print(f"I like {item}.")
numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions

View File

@@ -0,0 +1,10 @@
# Errors
1 in [1, 2, 3]
1 in (1, 2, 3)
1 in (
1, 2, 3
)
# OK
fruits = ["cherry", "grapes"]
"cherry" in fruits

View File

@@ -0,0 +1,71 @@
# bare raise is an except block
try:
pass
except Exception:
raise
try:
pass
except Exception:
if True:
raise
# bare raise is in a finally block which is in an except block
try:
raise Exception
except Exception:
try:
raise Exception
finally:
raise
# bare raise is in an __exit__ method
class ContextManager:
def __enter__(self):
return self
def __exit__(self, *args):
raise
try:
raise # [misplaced-bare-raise]
except Exception:
pass
def f():
try:
raise # [misplaced-bare-raise]
except Exception:
pass
def g():
raise # [misplaced-bare-raise]
def h():
try:
if True:
def i():
raise # [misplaced-bare-raise]
except Exception:
pass
raise # [misplaced-bare-raise]
raise # [misplaced-bare-raise]
try:
pass
except:
def i():
raise # [misplaced-bare-raise]
try:
pass
except:
class C:
raise # [misplaced-bare-raise]
try:
pass
except:
pass
finally:
raise # [misplaced-bare-raise]

View File

@@ -0,0 +1,54 @@
if a:
...
elif (a and b):
...
elif (a and b) and c:
...
elif (a and b) and c and d:
...
elif (a and b) and c and d and e:
...
elif (a and b) and c and d and e and f:
...
elif (a and b) and c and d and e and f and g:
...
elif (a and b) and c and d and e and f and g and h:
...
elif (a and b) and c and d and e and f and g and h and i:
...
elif (a and b) and c and d and e and f and g and h and i and j:
...
elif (a and b) and c and d and e and f and g and h and i and j and k:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y:
...
elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y and z:
...
else:
...

View File

@@ -0,0 +1,44 @@
import io
import sys
import tempfile
import io as hugo
import codecs
# Errors.
open("test.txt")
io.TextIOWrapper(io.FileIO("test.txt"))
hugo.TextIOWrapper(hugo.FileIO("test.txt"))
tempfile.NamedTemporaryFile("w")
tempfile.TemporaryFile("w")
codecs.open("test.txt")
tempfile.SpooledTemporaryFile(0, "w")
# Non-errors.
open("test.txt", encoding="utf-8")
open("test.bin", "wb")
open("test.bin", mode="wb")
open("test.txt", "r", -1, "utf-8")
open("test.txt", mode=sys.argv[1])
def func(*args, **kwargs):
open(*args)
open("text.txt", **kwargs)
io.TextIOWrapper(io.FileIO("test.txt"), encoding="utf-8")
io.TextIOWrapper(io.FileIO("test.txt"), "utf-8")
tempfile.TemporaryFile("w", encoding="utf-8")
tempfile.TemporaryFile("w", -1, "utf-8")
tempfile.TemporaryFile("wb")
tempfile.TemporaryFile()
tempfile.NamedTemporaryFile("w", encoding="utf-8")
tempfile.NamedTemporaryFile("w", -1, "utf-8")
tempfile.NamedTemporaryFile("wb")
tempfile.NamedTemporaryFile()
codecs.open("test.txt", encoding="utf-8")
codecs.open("test.bin", "wb")
codecs.open("test.bin", mode="wb")
codecs.open("test.txt", "r", -1, "utf-8")
tempfile.SpooledTemporaryFile(0, "w", encoding="utf-8")
tempfile.SpooledTemporaryFile(0, "w", -1, "utf-8")
tempfile.SpooledTemporaryFile(0, "wb")
tempfile.SpooledTemporaryFile(0, )

View File

@@ -786,6 +786,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SubprocessRunWithoutCheck) {
pylint::rules::subprocess_run_without_check(checker, call);
}
if checker.enabled(Rule::UnspecifiedEncoding) {
pylint::rules::unspecified_encoding(checker, call);
}
if checker.any_enabled(&[
Rule::PytestRaisesWithoutException,
Rule::PytestRaisesTooBroad,
@@ -1194,6 +1197,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::ComparisonWithItself) {
pylint::rules::comparison_with_itself(checker, left, ops, comparators);
}
if checker.enabled(Rule::LiteralMembership) {
pylint::rules::literal_membership(checker, compare);
}
if checker.enabled(Rule::ComparisonOfConstant) {
pylint::rules::comparison_of_constant(checker, left, ops, comparators);
}

View File

@@ -964,7 +964,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::Raise(ast::StmtRaise { exc, .. }) => {
Stmt::Raise(raise @ ast::StmtRaise { exc, .. }) => {
if checker.enabled(Rule::RaiseNotImplemented) {
if let Some(expr) = exc {
pyflakes::rules::raise_not_implemented(checker, expr);
@@ -1004,6 +1004,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_raise::rules::unnecessary_paren_on_raise_exception(checker, expr);
}
}
if checker.enabled(Rule::MisplacedBareRaise) {
pylint::rules::misplaced_bare_raise(checker, raise);
}
}
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => {
if checker.enabled(Rule::GlobalStatement) {
@@ -1067,6 +1070,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::CheckAndRemoveFromSet) {
refurb::rules::check_and_remove_from_set(checker, if_);
}
if checker.enabled(Rule::TooManyBooleanExpressions) {
pylint::rules::too_many_boolean_expressions(checker, if_);
}
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{Expr, TypeParam};
use ruff_python_ast::Expr;
use ruff_python_semantic::{ScopeId, Snapshot};
use ruff_text_size::TextRange;
@@ -10,7 +10,7 @@ pub(crate) struct Deferred<'a> {
pub(crate) scopes: Vec<ScopeId>,
pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>,
pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>,
pub(crate) type_param_definitions: Vec<(&'a TypeParam, Snapshot)>,
pub(crate) type_param_definitions: Vec<(&'a Expr, Snapshot)>,
pub(crate) functions: Vec<Snapshot>,
pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>,
pub(crate) for_loops: Vec<Snapshot>,

View File

@@ -524,6 +524,7 @@ where
);
self.semantic.push_definition(definition);
self.semantic.push_scope(ScopeKind::Function(function_def));
self.semantic.flags -= SemanticModelFlags::EXCEPTION_HANDLER;
self.deferred.functions.push(self.semantic.snapshot());
@@ -562,6 +563,7 @@ where
);
self.semantic.push_definition(definition);
self.semantic.push_scope(ScopeKind::Class(class_def));
self.semantic.flags -= SemanticModelFlags::EXCEPTION_HANDLER;
// Extract any global bindings from the class body.
if let Some(globals) = Globals::from_body(body) {
@@ -580,9 +582,9 @@ where
if let Some(type_params) = type_params {
self.visit_type_params(type_params);
}
// The value in a `type` alias has annotation semantics, in that it's never
// evaluated at runtime.
self.visit_annotation(value);
self.deferred
.type_param_definitions
.push((value, self.semantic.snapshot()));
self.semantic.pop_scope();
self.visit_expr(name);
}
@@ -1387,9 +1389,14 @@ where
}
}
// Step 2: Traversal
self.deferred
.type_param_definitions
.push((type_param, self.semantic.snapshot()));
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
bound: Some(bound), ..
}) = type_param
{
self.deferred
.type_param_definitions
.push((bound, self.semantic.snapshot()));
}
}
}
@@ -1764,12 +1771,9 @@ impl<'a> Checker<'a> {
for (type_param, snapshot) in type_params {
self.semantic.restore(snapshot);
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
bound: Some(bound), ..
}) = type_param
{
self.visit_annotation(bound);
}
self.semantic.flags |=
SemanticModelFlags::TYPE_PARAM_DEFINITION | SemanticModelFlags::TYPE_DEFINITION;
self.visit_expr(type_param);
}
}
self.semantic.restore(snapshot);

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;

View File

@@ -253,3 +253,9 @@ impl From<NonZeroU8> for TabSize {
Self(tab_size)
}
}
impl From<TabSize> for NonZeroU8 {
fn from(value: TabSize) -> Self {
value.0
}
}

View File

@@ -34,7 +34,7 @@ use crate::checkers::ast::Checker;
///
/// ## Example
/// ```python
/// "text.txt".strip(".txt") # "ex"
/// "text.txt".strip(".txt") # "e"
/// ```
///
/// Use instead:

View File

@@ -5,8 +5,8 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::node::AstNode;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::AstNode;
use ruff_python_ast::{self as ast, Arguments, Constant, Decorator, Expr, ExprContext};
use ruff_python_codegen::Generator;
use ruff_python_trivia::CommentRanges;

View File

@@ -6,7 +6,7 @@ use log::error;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, whitespace, Constant, ElifElseClause, Expr, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};

View File

@@ -1,8 +1,8 @@
use ruff_diagnostics::Edit;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, Arguments, CmpOp, Comprehension, Expr};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange};

View File

@@ -87,6 +87,7 @@ mod tests {
#[test_case(Rule::EscapeSequenceInDocstring, Path::new("D.py"))]
#[test_case(Rule::EscapeSequenceInDocstring, Path::new("D301.py"))]
#[test_case(Rule::TripleSingleQuotes, Path::new("D.py"))]
#[test_case(Rule::TripleSingleQuotes, Path::new("D300.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,6 +1,6 @@
use memchr::memchr_iter;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
@@ -46,18 +46,21 @@ use crate::docstrings::Docstring;
#[violation]
pub struct EscapeSequenceInDocstring;
impl Violation for EscapeSequenceInDocstring {
impl AlwaysFixableViolation for EscapeSequenceInDocstring {
#[derive_message_formats]
fn message(&self) -> String {
format!(r#"Use `r"""` if any backslashes in a docstring"#)
}
fn fix_title(&self) -> String {
format!(r#"Add `r` prefix"#)
}
}
/// D301
pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) {
// Docstring is already raw.
let contents = docstring.contents;
if contents.starts_with('r') || contents.starts_with("ur") {
if docstring.leading_quote().contains(['r', 'R']) {
return;
}
@@ -67,11 +70,15 @@ pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) {
if memchr_iter(b'\\', bytes).any(|position| {
let escaped_char = bytes.get(position.saturating_add(1));
// Allow continuations (backslashes followed by newlines) and Unicode escapes.
!matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'N'))
!matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'U' | b'N'))
}) {
checker.diagnostics.push(Diagnostic::new(
EscapeSequenceInDocstring,
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
"r".to_owned() + docstring.contents,
docstring.range(),
));
)));
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_codegen::Quote;
use ruff_text_size::Ranged;
@@ -37,6 +37,8 @@ pub struct TripleSingleQuotes {
}
impl Violation for TripleSingleQuotes {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let TripleSingleQuotes { expected_quote } = self;
@@ -45,12 +47,25 @@ impl Violation for TripleSingleQuotes {
Quote::Single => format!(r#"Use triple single quotes `'''`"#),
}
}
fn fix_title(&self) -> Option<String> {
let TripleSingleQuotes { expected_quote } = self;
Some(match expected_quote {
Quote::Double => format!("Convert to triple double quotes"),
Quote::Single => format!("Convert to triple single quotes"),
})
}
}
/// D300
pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
let leading_quote = docstring.leading_quote();
let prefixes = docstring
.leading_quote()
.trim_end_matches(|c| c == '\'' || c == '"')
.to_owned();
let expected_quote = if docstring.body().contains("\"\"\"") {
Quote::Single
} else {
@@ -60,18 +75,34 @@ pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
match expected_quote {
Quote::Single => {
if !leading_quote.ends_with("'''") {
checker.diagnostics.push(Diagnostic::new(
TripleSingleQuotes { expected_quote },
docstring.range(),
));
let mut diagnostic =
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
let body = docstring.body().as_str();
if !body.ends_with('\'') {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{prefixes}'''{body}'''"),
docstring.range(),
)));
}
checker.diagnostics.push(diagnostic);
}
}
Quote::Double => {
if !leading_quote.ends_with("\"\"\"") {
checker.diagnostics.push(Diagnostic::new(
TripleSingleQuotes { expected_quote },
docstring.range(),
));
let mut diagnostic =
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
let body = docstring.body().as_str();
if !body.ends_with('"') {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{prefixes}\"\"\"{body}\"\"\""),
docstring.range(),
)));
}
checker.diagnostics.push(diagnostic);
}
}
}

View File

@@ -1,47 +1,102 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D.py:307:5: D300 Use triple double quotes `"""`
D.py:307:5: D300 [*] Use triple double quotes `"""`
|
305 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
306 | def triple_single_quotes_raw():
307 | r'''Summary.'''
| ^^^^^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
D.py:312:5: D300 Use triple double quotes `"""`
Fix
304 304 |
305 305 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
306 306 | def triple_single_quotes_raw():
307 |- r'''Summary.'''
307 |+ r"""Summary."""
308 308 |
309 309 |
310 310 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
D.py:312:5: D300 [*] Use triple double quotes `"""`
|
310 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
311 | def triple_single_quotes_raw_uppercase():
312 | R'''Summary.'''
| ^^^^^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
D.py:317:5: D300 Use triple double quotes `"""`
Fix
309 309 |
310 310 | @expect('D300: Use """triple double quotes""" (found \'\'\'-quotes)')
311 311 | def triple_single_quotes_raw_uppercase():
312 |- R'''Summary.'''
312 |+ R"""Summary."""
313 313 |
314 314 |
315 315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
D.py:317:5: D300 [*] Use triple double quotes `"""`
|
315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
316 | def single_quotes_raw():
317 | r'Summary.'
| ^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
D.py:322:5: D300 Use triple double quotes `"""`
Fix
314 314 |
315 315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
316 316 | def single_quotes_raw():
317 |- r'Summary.'
317 |+ r"""Summary."""
318 318 |
319 319 |
320 320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
D.py:322:5: D300 [*] Use triple double quotes `"""`
|
320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
321 | def single_quotes_raw_uppercase():
322 | R'Summary.'
| ^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
D.py:328:5: D300 Use triple double quotes `"""`
Fix
319 319 |
320 320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
321 321 | def single_quotes_raw_uppercase():
322 |- R'Summary.'
322 |+ R"""Summary."""
323 323 |
324 324 |
325 325 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
D.py:328:5: D300 [*] Use triple double quotes `"""`
|
326 | @expect('D301: Use r""" if any backslashes in a docstring')
327 | def single_quotes_raw_uppercase_backslash():
328 | R'Sum\mary.'
| ^^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
D.py:645:5: D300 Use triple double quotes `"""`
Fix
325 325 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
326 326 | @expect('D301: Use r""" if any backslashes in a docstring')
327 327 | def single_quotes_raw_uppercase_backslash():
328 |- R'Sum\mary.'
328 |+ R"""Sum\mary."""
329 329 |
330 330 |
331 331 | @expect('D301: Use r""" if any backslashes in a docstring')
D.py:645:5: D300 [*] Use triple double quotes `"""`
|
644 | def single_line_docstring_with_an_escaped_backslash():
645 | "\
@@ -51,8 +106,21 @@ D.py:645:5: D300 Use triple double quotes `"""`
647 |
648 | class StatementOnSameLineAsDocstring:
|
= help: Convert to triple double quotes
D.py:649:5: D300 Use triple double quotes `"""`
Fix
642 642 |
643 643 |
644 644 | def single_line_docstring_with_an_escaped_backslash():
645 |- "\
646 |- "
645 |+ """\
646 |+ """
647 647 |
648 648 | class StatementOnSameLineAsDocstring:
649 649 | "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
D.py:649:5: D300 [*] Use triple double quotes `"""`
|
648 | class StatementOnSameLineAsDocstring:
649 | "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
@@ -60,15 +128,37 @@ D.py:649:5: D300 Use triple double quotes `"""`
650 | def sort_services(self):
651 | pass
|
= help: Convert to triple double quotes
D.py:654:5: D300 Use triple double quotes `"""`
Fix
646 646 | "
647 647 |
648 648 | class StatementOnSameLineAsDocstring:
649 |- "After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
649 |+ """After this docstring there's another statement on the same line separated by a semicolon.""" ; priorities=1
650 650 | def sort_services(self):
651 651 | pass
652 652 |
D.py:654:5: D300 [*] Use triple double quotes `"""`
|
653 | class StatementOnSameLineAsDocstring:
654 | "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
D.py:658:5: D300 Use triple double quotes `"""`
Fix
651 651 | pass
652 652 |
653 653 | class StatementOnSameLineAsDocstring:
654 |- "After this docstring there's another statement on the same line separated by a semicolon."; priorities=1
654 |+ """After this docstring there's another statement on the same line separated by a semicolon."""; priorities=1
655 655 |
656 656 |
657 657 | class CommentAfterDocstring:
D.py:658:5: D300 [*] Use triple double quotes `"""`
|
657 | class CommentAfterDocstring:
658 | "After this docstring there's a comment." # priorities=1
@@ -76,8 +166,19 @@ D.py:658:5: D300 Use triple double quotes `"""`
659 | def sort_services(self):
660 | pass
|
= help: Convert to triple double quotes
D.py:664:5: D300 Use triple double quotes `"""`
Fix
655 655 |
656 656 |
657 657 | class CommentAfterDocstring:
658 |- "After this docstring there's a comment." # priorities=1
658 |+ """After this docstring there's a comment.""" # priorities=1
659 659 | def sort_services(self):
660 660 | pass
661 661 |
D.py:664:5: D300 [*] Use triple double quotes `"""`
|
663 | def newline_after_closing_quote(self):
664 | "We enforce a newline after the closing quote for a multi-line docstring \
@@ -85,5 +186,15 @@ D.py:664:5: D300 Use triple double quotes `"""`
665 | | but continuations shouldn't be considered multi-line"
| |_________________________________________________________^ D300
|
= help: Convert to triple double quotes
Fix
661 661 |
662 662 |
663 663 | def newline_after_closing_quote(self):
664 |- "We enforce a newline after the closing quote for a multi-line docstring \
665 |- but continuations shouldn't be considered multi-line"
664 |+ """We enforce a newline after the closing quote for a multi-line docstring \
665 |+ but continuations shouldn't be considered multi-line"""

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D300.py:6:5: D300 Use triple double quotes `"""`
|
5 | def ends_in_quote():
6 | 'Sum\\mary."'
| ^^^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
D300.py:10:5: D300 [*] Use triple double quotes `"""`
|
9 | def contains_quote():
10 | 'Sum"\\mary.'
| ^^^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
Fix
7 7 |
8 8 |
9 9 | def contains_quote():
10 |- 'Sum"\\mary.'
10 |+ """Sum"\\mary."""

View File

@@ -1,28 +1,23 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D.py:328:5: D301 Use `r"""` if any backslashes in a docstring
|
326 | @expect('D301: Use r""" if any backslashes in a docstring')
327 | def single_quotes_raw_uppercase_backslash():
328 | R'Sum\mary.'
| ^^^^^^^^^^^^ D301
|
D.py:333:5: D301 Use `r"""` if any backslashes in a docstring
D.py:333:5: D301 [*] Use `r"""` if any backslashes in a docstring
|
331 | @expect('D301: Use r""" if any backslashes in a docstring')
332 | def double_quotes_backslash():
333 | """Sum\\mary."""
| ^^^^^^^^^^^^^^^^ D301
|
= help: Add `r` prefix
D.py:338:5: D301 Use `r"""` if any backslashes in a docstring
|
336 | @expect('D301: Use r""" if any backslashes in a docstring')
337 | def double_quotes_backslash_uppercase():
338 | R"""Sum\\mary."""
| ^^^^^^^^^^^^^^^^^ D301
|
Suggested fix
330 330 |
331 331 | @expect('D301: Use r""" if any backslashes in a docstring')
332 332 | def double_quotes_backslash():
333 |- """Sum\\mary."""
333 |+ r"""Sum\\mary."""
334 334 |
335 335 |
336 336 | @expect('D301: Use r""" if any backslashes in a docstring')

View File

@@ -1,18 +1,20 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D301.py:2:5: D301 Use `r"""` if any backslashes in a docstring
D301.py:2:5: D301 [*] Use `r"""` if any backslashes in a docstring
|
1 | def double_quotes_backslash():
2 | """Sum\\mary."""
| ^^^^^^^^^^^^^^^^ D301
|
= help: Add `r` prefix
D301.py:10:5: D301 Use `r"""` if any backslashes in a docstring
|
9 | def double_quotes_backslash_uppercase():
10 | R"""Sum\\mary."""
| ^^^^^^^^^^^^^^^^^ D301
|
Suggested fix
1 1 | def double_quotes_backslash():
2 |- """Sum\\mary."""
2 |+ r"""Sum\\mary."""
3 3 |
4 4 |
5 5 | def double_quotes_backslash_raw():

View File

@@ -1,10 +1,15 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
bom.py:1:1: D300 Use triple double quotes `"""`
bom.py:1:1: D300 [*] Use triple double quotes `"""`
|
1 | ''' SAM macro definitions '''
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D300
|
= help: Convert to triple double quotes
Fix
1 |-''' SAM macro definitions '''
1 |+""" SAM macro definitions """

View File

@@ -50,6 +50,7 @@ mod tests {
#[test_case(Rule::UnusedImport, Path::new("F401_16.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_17.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_18.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_19.py"))]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
@@ -135,6 +136,7 @@ mod tests {
#[test_case(Rule::UndefinedName, Path::new("F821_17.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_18.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_19.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_20.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))]

View File

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

View File

@@ -0,0 +1,14 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F821_20.py:3:24: F821 Undefined name `Record`
|
1 | """Test lazy evaluation of type alias values."""
2 |
3 | type RecordCallback[R: Record] = Callable[[R], None]
| ^^^^^^ F821
4 |
5 | from collections.abc import Callable
|

View File

@@ -22,7 +22,11 @@ pub(super) fn type_param_name(arguments: &Arguments) -> Option<&str> {
}
}
pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &LinterSettings) -> bool {
pub(super) fn in_dunder_method(
dunder_name: &str,
semantic: &SemanticModel,
settings: &LinterSettings,
) -> bool {
let scope = semantic.current_scope();
let ScopeKind::Function(ast::StmtFunctionDef {
name,
@@ -32,7 +36,7 @@ pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &LinterSettings
else {
return false;
};
if name != "__init__" {
if name != dunder_name {
return false;
}
let Some(parent) = semantic.first_non_type_parent_scope(scope) else {

View File

@@ -133,8 +133,11 @@ mod tests {
Rule::SubprocessRunWithoutCheck,
Path::new("subprocess_run_without_check.py")
)]
#[test_case(Rule::UnspecifiedEncoding, Path::new("unspecified_encoding.py"))]
#[test_case(Rule::BadDunderMethodName, Path::new("bad_dunder_method_name.py"))]
#[test_case(Rule::NoSelfUse, Path::new("no_self_use.py"))]
#[test_case(Rule::MisplacedBareRaise, Path::new("misplaced_bare_raise.py"))]
#[test_case(Rule::LiteralMembership, Path::new("literal_membership.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -228,6 +231,22 @@ mod tests {
Ok(())
}
#[test]
fn max_boolean_expressions() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/too_many_boolean_expressions.py"),
&LinterSettings {
pylint: pylint::settings::Settings {
max_bool_expr: 5,
..pylint::settings::Settings::default()
},
..LinterSettings::for_rule(Rule::TooManyBooleanExpressions)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn max_statements() -> Result<()> {
let diagnostics = test_path(

View File

@@ -54,7 +54,7 @@ pub(crate) fn bad_dunder_method_name(checker: &mut Checker, class_body: &[Stmt])
.iter()
.filter_map(ruff_python_ast::Stmt::as_function_def_stmt)
.filter(|method| {
if is_known_dunder_method(&method.name) {
if is_known_dunder_method(&method.name) || matches!(method.name.as_str(), "_") {
return false;
}
method.name.starts_with('_') && method.name.ends_with('_')

View File

@@ -1,9 +1,7 @@
use ast::ExprContext;
use ruff_python_ast::{self as ast, Expr};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::{Ranged, TextRange};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -38,7 +36,7 @@ impl AlwaysFixableViolation for IterationOverSet {
}
fn fix_title(&self) -> String {
format!("Use a sequence type instead of a `set` when iterating over values")
format!("Convert to `tuple`")
}
}
@@ -54,15 +52,14 @@ pub(crate) fn iteration_over_set(checker: &mut Checker, expr: &Expr) {
let mut diagnostic = Diagnostic::new(IterationOverSet, expr.range());
let tuple = checker.generator().expr(&Expr::Tuple(ast::ExprTuple {
elts: elts.clone(),
ctx: ExprContext::Store,
range: TextRange::default(),
}));
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("({tuple})"),
expr.range(),
)));
let tuple = if let [elt] = elts.as_slice() {
let elt = checker.locator().slice(elt);
format!("({elt},)")
} else {
let set = checker.locator().slice(expr);
format!("({})", &set[1..set.len() - 1])
};
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(tuple, expr.range())));
checker.diagnostics.push(diagnostic);
}

View File

@@ -0,0 +1,65 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for membership tests on `list` and `tuple` literals.
///
/// ## Why is this bad?
/// When testing for membership in a static sequence, prefer a `set` literal
/// over a `list` or `tuple`, as Python optimizes `set` membership tests.
///
/// ## Example
/// ```python
/// 1 in [1, 2, 3]
/// ```
///
/// Use instead:
/// ```python
/// 1 in {1, 2, 3}
/// ```
/// ## References
/// - [Whats New In Python 3.2](https://docs.python.org/3/whatsnew/3.2.html#optimizations)
#[violation]
pub struct LiteralMembership;
impl AlwaysFixableViolation for LiteralMembership {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use a `set` literal when testing for membership")
}
fn fix_title(&self) -> String {
format!("Convert to `set`")
}
}
/// PLR6201
pub(crate) fn literal_membership(checker: &mut Checker, compare: &ast::ExprCompare) {
let [op] = compare.ops.as_slice() else {
return;
};
if !matches!(op, CmpOp::In | CmpOp::NotIn) {
return;
}
let [right] = compare.comparators.as_slice() else {
return;
};
if !matches!(right, Expr::List(_) | Expr::Tuple(_)) {
return;
}
let mut diagnostic = Diagnostic::new(LiteralMembership, right.range());
let literal = checker.locator().slice(right);
let set = format!("{{{}}}", &literal[1..literal.len() - 1]);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(set, right.range())));
checker.diagnostics.push(diagnostic);
}

View File

@@ -0,0 +1,70 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::in_dunder_method;
/// ## What it does
/// Checks for bare `raise` statements outside of exception handlers.
///
/// ## Why is this bad?
/// A bare `raise` statement without an exception object will re-raise the last
/// exception that was active in the current scope, and is typically used
/// within an exception handler to re-raise the caught exception.
///
/// If a bare `raise` is used outside of an exception handler, it will generate
/// an error due to the lack of an active exception.
///
/// Note that a bare `raise` within a `finally` block will work in some cases
/// (namely, when the exception is raised within the `try` block), but should
/// be avoided as it can lead to confusing behavior.
///
/// ## Example
/// ```python
/// from typing import Any
///
///
/// def is_some(obj: Any) -> bool:
/// if obj is None:
/// raise
/// ```
///
/// Use instead:
/// ```python
/// from typing import Any
///
///
/// def is_some(obj: Any) -> bool:
/// if obj is None:
/// raise ValueError("`obj` cannot be `None`")
/// ```
#[violation]
pub struct MisplacedBareRaise;
impl Violation for MisplacedBareRaise {
#[derive_message_formats]
fn message(&self) -> String {
format!("Bare `raise` statement is not inside an exception handler")
}
}
/// PLE0704
pub(crate) fn misplaced_bare_raise(checker: &mut Checker, raise: &ast::StmtRaise) {
if raise.exc.is_some() {
return;
}
if checker.semantic().in_exception_handler() {
return;
}
if in_dunder_method("__exit__", checker.semantic(), checker.settings) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(MisplacedBareRaise, raise.range()));
}

View File

@@ -24,10 +24,12 @@ pub(crate) use invalid_envvar_value::*;
pub(crate) use invalid_str_return::*;
pub(crate) use invalid_string_characters::*;
pub(crate) use iteration_over_set::*;
pub(crate) use literal_membership::*;
pub(crate) use load_before_global_declaration::*;
pub(crate) use logging::*;
pub(crate) use magic_value_comparison::*;
pub(crate) use manual_import_from::*;
pub(crate) use misplaced_bare_raise::*;
pub(crate) use named_expr_without_context::*;
pub(crate) use nested_min_max::*;
pub(crate) use no_self_use::*;
@@ -43,6 +45,7 @@ pub(crate) use subprocess_popen_preexec_fn::*;
pub(crate) use subprocess_run_without_check::*;
pub(crate) use sys_exit_alias::*;
pub(crate) use too_many_arguments::*;
pub(crate) use too_many_boolean_expressions::*;
pub(crate) use too_many_branches::*;
pub(crate) use too_many_public_methods::*;
pub(crate) use too_many_return_statements::*;
@@ -52,6 +55,7 @@ pub(crate) use type_name_incorrect_variance::*;
pub(crate) use type_param_name_mismatch::*;
pub(crate) use unexpected_special_method_signature::*;
pub(crate) use unnecessary_direct_lambda_call::*;
pub(crate) use unspecified_encoding::*;
pub(crate) use useless_else_on_loop::*;
pub(crate) use useless_import_alias::*;
pub(crate) use useless_return::*;
@@ -84,10 +88,12 @@ mod invalid_envvar_value;
mod invalid_str_return;
mod invalid_string_characters;
mod iteration_over_set;
mod literal_membership;
mod load_before_global_declaration;
mod logging;
mod magic_value_comparison;
mod manual_import_from;
mod misplaced_bare_raise;
mod named_expr_without_context;
mod nested_min_max;
mod no_self_use;
@@ -103,6 +109,7 @@ mod subprocess_popen_preexec_fn;
mod subprocess_run_without_check;
mod sys_exit_alias;
mod too_many_arguments;
mod too_many_boolean_expressions;
mod too_many_branches;
mod too_many_public_methods;
mod too_many_return_statements;
@@ -112,6 +119,7 @@ mod type_name_incorrect_variance;
mod type_param_name_mismatch;
mod unexpected_special_method_signature;
mod unnecessary_direct_lambda_call;
mod unspecified_encoding;
mod useless_else_on_loop;
mod useless_import_alias;
mod useless_return;

View File

@@ -6,7 +6,7 @@ use ruff_python_ast::helpers::is_const_none;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::in_dunder_init;
use crate::rules::pylint::helpers::in_dunder_method;
/// ## What it does
/// Checks for `__init__` methods that return values.
@@ -58,7 +58,7 @@ pub(crate) fn return_in_init(checker: &mut Checker, stmt: &Stmt) {
}
}
if in_dunder_init(checker.semantic(), checker.settings) {
if in_dunder_method("__init__", checker.semantic(), checker.settings) {
checker
.diagnostics
.push(Diagnostic::new(ReturnInInit, stmt.range()));

View File

@@ -0,0 +1,89 @@
use ast::{Expr, StmtIf};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for too many Boolean expressions in an `if` statement.
///
/// By default, this rule allows up to 5 expressions. This can be configured
/// using the [`pylint.max-bool-expr`] option.
///
/// ## Why is this bad?
/// `if` statements with many Boolean expressions are harder to understand
/// and maintain. Consider assigning the result of the Boolean expression,
/// or any of its sub-expressions, to a variable.
///
/// ## Example
/// ```python
/// if a and b and c and d and e and f and g and h:
/// ...
/// ```
///
/// ## Options
/// - `pylint.max-bool-expr`
#[violation]
pub struct TooManyBooleanExpressions {
expressions: usize,
max_expressions: usize,
}
impl Violation for TooManyBooleanExpressions {
#[derive_message_formats]
fn message(&self) -> String {
let TooManyBooleanExpressions {
expressions,
max_expressions,
} = self;
format!("Too many Boolean expressions ({expressions} > {max_expressions})")
}
}
/// PLR0916
pub(crate) fn too_many_boolean_expressions(checker: &mut Checker, stmt: &StmtIf) {
if let Some(bool_op) = stmt.test.as_bool_op_expr() {
let expressions = count_bools(bool_op);
if expressions > checker.settings.pylint.max_bool_expr {
checker.diagnostics.push(Diagnostic::new(
TooManyBooleanExpressions {
expressions,
max_expressions: checker.settings.pylint.max_bool_expr,
},
bool_op.range(),
));
}
}
for elif in &stmt.elif_else_clauses {
if let Some(bool_op) = elif.test.as_ref().and_then(Expr::as_bool_op_expr) {
let expressions = count_bools(bool_op);
if expressions > checker.settings.pylint.max_bool_expr {
checker.diagnostics.push(Diagnostic::new(
TooManyBooleanExpressions {
expressions,
max_expressions: checker.settings.pylint.max_bool_expr,
},
bool_op.range(),
));
}
}
}
}
/// Count the number of Boolean expressions in a `bool_op` expression.
fn count_bools(bool_op: &ast::ExprBoolOp) -> usize {
bool_op
.values
.iter()
.map(|expr| {
if let Expr::BoolOp(bool_op) = expr {
count_bools(bool_op)
} else {
1
}
})
.sum::<usize>()
}

View File

@@ -0,0 +1,157 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::call_path::{format_call_path, CallPath};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `open` and related calls without an explicit `encoding`
/// argument.
///
/// ## Why is this bad?
/// Using `open` in text mode without an explicit encoding can lead to
/// non-portable code, with differing behavior across platforms.
///
/// Instead, consider using the `encoding` parameter to enforce a specific
/// encoding.
///
/// ## Example
/// ```python
/// open("file.txt")
/// ```
///
/// Use instead:
/// ```python
/// open("file.txt", encoding="utf-8")
/// ```
///
/// ## References
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
#[violation]
pub struct UnspecifiedEncoding {
function_name: String,
mode: Mode,
}
impl Violation for UnspecifiedEncoding {
#[derive_message_formats]
fn message(&self) -> String {
let UnspecifiedEncoding {
function_name,
mode,
} = self;
match mode {
Mode::Supported => {
format!("`{function_name}` in text mode without explicit `encoding` argument")
}
Mode::Unsupported => {
format!("`{function_name}` without explicit `encoding` argument")
}
}
}
}
/// PLW1514
pub(crate) fn unspecified_encoding(checker: &mut Checker, call: &ast::ExprCall) {
let Some((function_name, mode)) = checker
.semantic()
.resolve_call_path(&call.func)
.filter(|call_path| is_violation(call, call_path))
.map(|call_path| {
(
format_call_path(call_path.as_slice()),
Mode::from(&call_path),
)
})
else {
return;
};
checker.diagnostics.push(Diagnostic::new(
UnspecifiedEncoding {
function_name,
mode,
},
call.func.range(),
));
}
/// Returns `true` if the given expression is a string literal containing a `b` character.
fn is_binary_mode(expr: &ast::Expr) -> Option<bool> {
Some(expr.as_constant_expr()?.value.as_str()?.value.contains('b'))
}
/// Returns `true` if the given call lacks an explicit `encoding`.
fn is_violation(call: &ast::ExprCall, call_path: &CallPath) -> bool {
// If we have something like `*args`, which might contain the encoding argument, abort.
if call
.arguments
.args
.iter()
.any(ruff_python_ast::Expr::is_starred_expr)
{
return false;
}
// If we have something like `**kwargs`, which might contain the encoding argument, abort.
if call
.arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
return false;
}
match call_path.as_slice() {
["" | "codecs" | "_io", "open"] => {
if let Some(mode_arg) = call.arguments.find_argument("mode", 1) {
if is_binary_mode(mode_arg).unwrap_or(true) {
// binary mode or unknown mode is no violation
return false;
}
}
// else mode not specified, defaults to text mode
call.arguments.find_argument("encoding", 3).is_none()
}
["tempfile", "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile"] => {
let mode_pos = usize::from(call_path[1] == "SpooledTemporaryFile");
if let Some(mode_arg) = call.arguments.find_argument("mode", mode_pos) {
if is_binary_mode(mode_arg).unwrap_or(true) {
// binary mode or unknown mode is no violation
return false;
}
} else {
// defaults to binary mode
return false;
}
call.arguments
.find_argument("encoding", mode_pos + 2)
.is_none()
}
["io" | "_io", "TextIOWrapper"] => call.arguments.find_argument("encoding", 1).is_none(),
_ => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
/// The call supports a `mode` argument.
Supported,
/// The call does not support a `mode` argument.
Unsupported,
}
impl From<&CallPath<'_>> for Mode {
fn from(value: &CallPath<'_>) -> Self {
match value.as_slice() {
["" | "codecs" | "_io", "open"] => Mode::Supported,
["tempfile", "TemporaryFile" | "NamedTemporaryFile" | "SpooledTemporaryFile"] => {
Mode::Supported
}
["io" | "_io", "TextIOWrapper"] => Mode::Unsupported,
_ => Mode::Unsupported,
}
}
}

View File

@@ -5,7 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::in_dunder_init;
use crate::rules::pylint::helpers::in_dunder_method;
/// ## What it does
/// Checks for `__init__` methods that are turned into generators by the
@@ -40,7 +40,7 @@ impl Violation for YieldInInit {
/// PLE0100
pub(crate) fn yield_in_init(checker: &mut Checker, expr: &Expr) {
if in_dunder_init(checker.semantic(), checker.settings) {
if in_dunder_method("__init__", checker.semantic(), checker.settings) {
checker
.diagnostics
.push(Diagnostic::new(YieldInInit, expr.range()));

View File

@@ -40,6 +40,7 @@ pub struct Settings {
pub allow_magic_value_types: Vec<ConstantType>,
pub max_args: usize,
pub max_returns: usize,
pub max_bool_expr: usize,
pub max_branches: usize,
pub max_statements: usize,
pub max_public_methods: usize,
@@ -51,6 +52,7 @@ impl Default for Settings {
allow_magic_value_types: vec![ConstantType::Str, ConstantType::Bytes],
max_args: 5,
max_returns: 6,
max_bool_expr: 5,
max_branches: 12,
max_statements: 50,
max_public_methods: 20,

View File

@@ -9,7 +9,7 @@ iteration_over_set.py:3:13: PLC0208 [*] Use a sequence type instead of a `set` w
| ^^^ PLC0208
4 | print(f"I can count to {item}!")
|
= help: Use a sequence type instead of a `set` when iterating over values
= help: Convert to `tuple`
Fix
1 1 | # Errors
@@ -28,7 +28,7 @@ iteration_over_set.py:6:13: PLC0208 [*] Use a sequence type instead of a `set` w
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0208
7 | print(f"I like {item}.")
|
= help: Use a sequence type instead of a `set` when iterating over values
= help: Convert to `tuple`
Fix
3 3 | for item in {1}:
@@ -38,90 +38,136 @@ iteration_over_set.py:6:13: PLC0208 [*] Use a sequence type instead of a `set` w
6 |+for item in ("apples", "lemons", "water"): # flags in-line set literals
7 7 | print(f"I like {item}.")
8 8 |
9 9 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
9 9 | for item in {1,}:
iteration_over_set.py:9:28: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
iteration_over_set.py:9:13: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
|
7 | print(f"I like {item}.")
8 |
9 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
| ^^^^^^^^^ PLC0208
10 |
11 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
9 | for item in {1,}:
| ^^^^ PLC0208
10 | print(f"I can count to {item}!")
|
= help: Use a sequence type instead of a `set` when iterating over values
= help: Convert to `tuple`
Fix
6 6 | for item in {"apples", "lemons", "water"}: # flags in-line set literals
7 7 | print(f"I like {item}.")
8 8 |
9 |-numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
9 |+numbers_list = [i for i in (1, 2, 3)] # flags sets in list comprehensions
10 10 |
11 11 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
12 12 |
9 |-for item in {1,}:
9 |+for item in (1,):
10 10 | print(f"I can count to {item}!")
11 11 |
12 12 | for item in {
iteration_over_set.py:11:27: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
iteration_over_set.py:12:13: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
|
9 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
10 |
11 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
| ^^^^^^^^^ PLC0208
12 |
13 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
10 | print(f"I can count to {item}!")
11 |
12 | for item in {
| _____________^
13 | | "apples", "lemons", "water"
14 | | }: # flags in-line set literals
| |_^ PLC0208
15 | print(f"I like {item}.")
|
= help: Use a sequence type instead of a `set` when iterating over values
= help: Convert to `tuple`
Fix
8 8 |
9 9 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
10 10 |
11 |-numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
11 |+numbers_set = {i for i in (1, 2, 3)} # flags sets in set comprehensions
12 12 |
13 13 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
14 14 |
iteration_over_set.py:13:36: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
|
11 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
12 |
13 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
| ^^^^^^^^^ PLC0208
14 |
15 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
|
= help: Use a sequence type instead of a `set` when iterating over values
Fix
10 10 |
11 11 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
12 12 |
13 |-numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
13 |+numbers_dict = {str(i): i for i in (1, 2, 3)} # flags sets in dict comprehensions
14 14 |
15 15 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
9 9 | for item in {1,}:
10 10 | print(f"I can count to {item}!")
11 11 |
12 |-for item in {
12 |+for item in (
13 13 | "apples", "lemons", "water"
14 |-}: # flags in-line set literals
14 |+): # flags in-line set literals
15 15 | print(f"I like {item}.")
16 16 |
17 17 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
iteration_over_set.py:15:27: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
iteration_over_set.py:17:28: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
|
13 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
14 |
15 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
| ^^^^^^^^^ PLC0208
15 | print(f"I like {item}.")
16 |
17 | # Non-errors
17 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
| ^^^^^^^^^ PLC0208
18 |
19 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
|
= help: Use a sequence type instead of a `set` when iterating over values
= help: Convert to `tuple`
Fix
12 12 |
13 13 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
14 14 |
15 |-numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
15 |+numbers_gen = (i for i in (1, 2, 3)) # flags sets in generator expressions
14 14 | }: # flags in-line set literals
15 15 | print(f"I like {item}.")
16 16 |
17 17 | # Non-errors
17 |-numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
17 |+numbers_list = [i for i in (1, 2, 3)] # flags sets in list comprehensions
18 18 |
19 19 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
20 20 |
iteration_over_set.py:19:27: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
|
17 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
18 |
19 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
| ^^^^^^^^^ PLC0208
20 |
21 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
|
= help: Convert to `tuple`
Fix
16 16 |
17 17 | numbers_list = [i for i in {1, 2, 3}] # flags sets in list comprehensions
18 18 |
19 |-numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
19 |+numbers_set = {i for i in (1, 2, 3)} # flags sets in set comprehensions
20 20 |
21 21 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
22 22 |
iteration_over_set.py:21:36: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
|
19 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
20 |
21 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
| ^^^^^^^^^ PLC0208
22 |
23 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
|
= help: Convert to `tuple`
Fix
18 18 |
19 19 | numbers_set = {i for i in {1, 2, 3}} # flags sets in set comprehensions
20 20 |
21 |-numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
21 |+numbers_dict = {str(i): i for i in (1, 2, 3)} # flags sets in dict comprehensions
22 22 |
23 23 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
24 24 |
iteration_over_set.py:23:27: PLC0208 [*] Use a sequence type instead of a `set` when iterating over values
|
21 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
22 |
23 | numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
| ^^^^^^^^^ PLC0208
24 |
25 | # Non-errors
|
= help: Convert to `tuple`
Fix
20 20 |
21 21 | numbers_dict = {str(i): i for i in {1, 2, 3}} # flags sets in dict comprehensions
22 22 |
23 |-numbers_gen = (i for i in {1, 2, 3}) # flags sets in generator expressions
23 |+numbers_gen = (i for i in (1, 2, 3)) # flags sets in generator expressions
24 24 |
25 25 | # Non-errors
26 26 |

View File

@@ -0,0 +1,90 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
misplaced_bare_raise.py:30:5: PLE0704 Bare `raise` statement is not inside an exception handler
|
29 | try:
30 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
31 | except Exception:
32 | pass
|
misplaced_bare_raise.py:36:9: PLE0704 Bare `raise` statement is not inside an exception handler
|
34 | def f():
35 | try:
36 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
37 | except Exception:
38 | pass
|
misplaced_bare_raise.py:41:5: PLE0704 Bare `raise` statement is not inside an exception handler
|
40 | def g():
41 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
42 |
43 | def h():
|
misplaced_bare_raise.py:47:17: PLE0704 Bare `raise` statement is not inside an exception handler
|
45 | if True:
46 | def i():
47 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
48 | except Exception:
49 | pass
|
misplaced_bare_raise.py:50:5: PLE0704 Bare `raise` statement is not inside an exception handler
|
48 | except Exception:
49 | pass
50 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
51 |
52 | raise # [misplaced-bare-raise]
|
misplaced_bare_raise.py:52:1: PLE0704 Bare `raise` statement is not inside an exception handler
|
50 | raise # [misplaced-bare-raise]
51 |
52 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
53 |
54 | try:
|
misplaced_bare_raise.py:58:9: PLE0704 Bare `raise` statement is not inside an exception handler
|
56 | except:
57 | def i():
58 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
59 |
60 | try:
|
misplaced_bare_raise.py:64:9: PLE0704 Bare `raise` statement is not inside an exception handler
|
62 | except:
63 | class C:
64 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
65 |
66 | try:
|
misplaced_bare_raise.py:71:5: PLE0704 Bare `raise` statement is not inside an exception handler
|
69 | pass
70 | finally:
71 | raise # [misplaced-bare-raise]
| ^^^^^ PLE0704
|

View File

@@ -0,0 +1,69 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
literal_membership.py:2:6: PLR6201 [*] Use a `set` literal when testing for membership
|
1 | # Errors
2 | 1 in [1, 2, 3]
| ^^^^^^^^^ PLR6201
3 | 1 in (1, 2, 3)
4 | 1 in (
|
= help: Convert to `set`
Fix
1 1 | # Errors
2 |-1 in [1, 2, 3]
2 |+1 in {1, 2, 3}
3 3 | 1 in (1, 2, 3)
4 4 | 1 in (
5 5 | 1, 2, 3
literal_membership.py:3:6: PLR6201 [*] Use a `set` literal when testing for membership
|
1 | # Errors
2 | 1 in [1, 2, 3]
3 | 1 in (1, 2, 3)
| ^^^^^^^^^ PLR6201
4 | 1 in (
5 | 1, 2, 3
|
= help: Convert to `set`
Fix
1 1 | # Errors
2 2 | 1 in [1, 2, 3]
3 |-1 in (1, 2, 3)
3 |+1 in {1, 2, 3}
4 4 | 1 in (
5 5 | 1, 2, 3
6 6 | )
literal_membership.py:4:6: PLR6201 [*] Use a `set` literal when testing for membership
|
2 | 1 in [1, 2, 3]
3 | 1 in (1, 2, 3)
4 | 1 in (
| ______^
5 | | 1, 2, 3
6 | | )
| |_^ PLR6201
7 |
8 | # OK
|
= help: Convert to `set`
Fix
1 1 | # Errors
2 2 | 1 in [1, 2, 3]
3 3 | 1 in (1, 2, 3)
4 |-1 in (
4 |+1 in {
5 5 | 1, 2, 3
6 |-)
6 |+}
7 7 |
8 8 | # OK
9 9 | fruits = ["cherry", "grapes"]

View File

@@ -0,0 +1,72 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
unspecified_encoding.py:8:1: PLW1514 `open` in text mode without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
| ^^^^ PLW1514
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
|
unspecified_encoding.py:9:1: PLW1514 `io.TextIOWrapper` without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
9 | io.TextIOWrapper(io.FileIO("test.txt"))
| ^^^^^^^^^^^^^^^^ PLW1514
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
|
unspecified_encoding.py:10:1: PLW1514 `io.TextIOWrapper` without explicit `encoding` argument
|
8 | open("test.txt")
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
| ^^^^^^^^^^^^^^^^^^ PLW1514
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
|
unspecified_encoding.py:11:1: PLW1514 `tempfile.NamedTemporaryFile` in text mode without explicit `encoding` argument
|
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW1514
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
|
unspecified_encoding.py:12:1: PLW1514 `tempfile.TemporaryFile` in text mode without explicit `encoding` argument
|
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
| ^^^^^^^^^^^^^^^^^^^^^^ PLW1514
13 | codecs.open("test.txt")
14 | tempfile.SpooledTemporaryFile(0, "w")
|
unspecified_encoding.py:13:1: PLW1514 `codecs.open` in text mode without explicit `encoding` argument
|
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
| ^^^^^^^^^^^ PLW1514
14 | tempfile.SpooledTemporaryFile(0, "w")
|
unspecified_encoding.py:14:1: PLW1514 `tempfile.SpooledTemporaryFile` in text mode without explicit `encoding` argument
|
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
14 | tempfile.SpooledTemporaryFile(0, "w")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW1514
15 |
16 | # Non-errors.
|

View File

@@ -0,0 +1,214 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
too_many_boolean_expressions.py:11:6: PLR0916 Too many Boolean expressions (6 > 5)
|
9 | elif (a and b) and c and d and e:
10 | ...
11 | elif (a and b) and c and d and e and f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
12 | ...
13 | elif (a and b) and c and d and e and f and g:
|
too_many_boolean_expressions.py:13:6: PLR0916 Too many Boolean expressions (7 > 5)
|
11 | elif (a and b) and c and d and e and f:
12 | ...
13 | elif (a and b) and c and d and e and f and g:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
14 | ...
15 | elif (a and b) and c and d and e and f and g and h:
|
too_many_boolean_expressions.py:15:6: PLR0916 Too many Boolean expressions (8 > 5)
|
13 | elif (a and b) and c and d and e and f and g:
14 | ...
15 | elif (a and b) and c and d and e and f and g and h:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
16 | ...
17 | elif (a and b) and c and d and e and f and g and h and i:
|
too_many_boolean_expressions.py:17:6: PLR0916 Too many Boolean expressions (9 > 5)
|
15 | elif (a and b) and c and d and e and f and g and h:
16 | ...
17 | elif (a and b) and c and d and e and f and g and h and i:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
18 | ...
19 | elif (a and b) and c and d and e and f and g and h and i and j:
|
too_many_boolean_expressions.py:19:6: PLR0916 Too many Boolean expressions (10 > 5)
|
17 | elif (a and b) and c and d and e and f and g and h and i:
18 | ...
19 | elif (a and b) and c and d and e and f and g and h and i and j:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
20 | ...
21 | elif (a and b) and c and d and e and f and g and h and i and j and k:
|
too_many_boolean_expressions.py:21:6: PLR0916 Too many Boolean expressions (11 > 5)
|
19 | elif (a and b) and c and d and e and f and g and h and i and j:
20 | ...
21 | elif (a and b) and c and d and e and f and g and h and i and j and k:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
22 | ...
23 | elif (a and b) and c and d and e and f and g and h and i and j and k and l:
|
too_many_boolean_expressions.py:23:6: PLR0916 Too many Boolean expressions (12 > 5)
|
21 | elif (a and b) and c and d and e and f and g and h and i and j and k:
22 | ...
23 | elif (a and b) and c and d and e and f and g and h and i and j and k and l:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
24 | ...
25 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m:
|
too_many_boolean_expressions.py:25:6: PLR0916 Too many Boolean expressions (13 > 5)
|
23 | elif (a and b) and c and d and e and f and g and h and i and j and k and l:
24 | ...
25 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
26 | ...
27 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n:
|
too_many_boolean_expressions.py:27:6: PLR0916 Too many Boolean expressions (14 > 5)
|
25 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m:
26 | ...
27 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
28 | ...
29 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o:
|
too_many_boolean_expressions.py:29:6: PLR0916 Too many Boolean expressions (15 > 5)
|
27 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n:
28 | ...
29 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
30 | ...
31 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p:
|
too_many_boolean_expressions.py:31:6: PLR0916 Too many Boolean expressions (16 > 5)
|
29 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o:
30 | ...
31 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
32 | ...
33 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q:
|
too_many_boolean_expressions.py:33:6: PLR0916 Too many Boolean expressions (17 > 5)
|
31 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p:
32 | ...
33 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
34 | ...
35 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r:
|
too_many_boolean_expressions.py:35:6: PLR0916 Too many Boolean expressions (18 > 5)
|
33 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q:
34 | ...
35 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
36 | ...
37 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s:
|
too_many_boolean_expressions.py:37:6: PLR0916 Too many Boolean expressions (19 > 5)
|
35 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r:
36 | ...
37 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
38 | ...
39 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t:
|
too_many_boolean_expressions.py:39:6: PLR0916 Too many Boolean expressions (20 > 5)
|
37 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s:
38 | ...
39 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
40 | ...
41 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u:
|
too_many_boolean_expressions.py:41:6: PLR0916 Too many Boolean expressions (21 > 5)
|
39 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t:
40 | ...
41 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
42 | ...
43 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v:
|
too_many_boolean_expressions.py:43:6: PLR0916 Too many Boolean expressions (22 > 5)
|
41 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u:
42 | ...
43 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
44 | ...
45 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w:
|
too_many_boolean_expressions.py:45:6: PLR0916 Too many Boolean expressions (23 > 5)
|
43 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v:
44 | ...
45 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
46 | ...
47 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x:
|
too_many_boolean_expressions.py:47:6: PLR0916 Too many Boolean expressions (24 > 5)
|
45 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w:
46 | ...
47 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
48 | ...
49 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y:
|
too_many_boolean_expressions.py:49:6: PLR0916 Too many Boolean expressions (25 > 5)
|
47 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x:
48 | ...
49 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
50 | ...
51 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y and z:
|
too_many_boolean_expressions.py:51:6: PLR0916 Too many Boolean expressions (26 > 5)
|
49 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y:
50 | ...
51 | elif (a and b) and c and d and e and f and g and h and i and j and k and l and m and n and o and p and q and r and s and t and u and v and w and x and y and z:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR0916
52 | ...
53 | else:
|

View File

@@ -3,8 +3,8 @@ use itertools::Itertools;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::node::AstNode;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::AstNode;
use ruff_python_ast::{self as ast, Arguments, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;

View File

@@ -23,7 +23,7 @@ use crate::rules::{
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade,
};
use crate::settings::types::{PerFileIgnore, PythonVersion};
use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion};
use crate::{codes, RuleSelector};
use super::line_width::{LineLength, TabSize};
@@ -38,6 +38,7 @@ pub mod types;
#[derive(Debug, CacheKey)]
pub struct LinterSettings {
pub exclude: FilePatternSet,
pub project_root: PathBuf,
pub rules: RuleTable,
@@ -131,6 +132,7 @@ impl LinterSettings {
pub fn new(project_root: &Path) -> Self {
Self {
exclude: FilePatternSet::default(),
target_version: PythonVersion::default(),
project_root: project_root.to_path_buf(),
rules: DEFAULT_SELECTORS

View File

@@ -16,6 +16,7 @@ doctest = false
[dependencies]
ruff_python_trivia = { path = "../ruff_python_trivia" }
serde_derive_internals = "0.29.0"
proc-macro2 = { workspace = true }
quote = { workspace = true }

View File

@@ -1,11 +1,11 @@
use proc_macro2::TokenTree;
use quote::{quote, quote_spanned};
use serde_derive_internals::Ctxt;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::{
AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field,
Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Token, Type, TypePath,
Fields, Lit, LitStr, Path, PathArguments, PathSegment, Token, Type, TypePath,
};
use ruff_python_trivia::textwrap::dedent;
@@ -38,25 +38,14 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
.any(|attr| attr.path().is_ident("option_group"))
{
output.push(handle_option_group(field)?);
} else if let Some(serde) = field
.attrs
.iter()
.find(|attr| attr.path().is_ident("serde"))
{
} else if let Type::Path(ty) = &field.ty {
let serde_field = serde_field_metadata(field)?;
// If a field has the `serde(flatten)` attribute, flatten the options into the parent
// by calling `Type::record` instead of `visitor.visit_set`
if let (Type::Path(ty), Meta::List(list)) = (&field.ty, &serde.meta) {
for token in list.tokens.clone() {
if let TokenTree::Ident(ident) = token {
if ident == "flatten" {
let ty_name = ty.path.require_ident()?;
output.push(quote_spanned!(
ident.span() => (#ty_name::record(visit))
));
break;
}
}
}
if serde_field.flatten() {
let ty_name = ty.path.require_ident()?;
output.push(quote_spanned!(ident.span() => (#ty_name::record(visit))));
}
}
}
@@ -193,6 +182,10 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
} = attr.parse_args::<FieldAttributes>()?;
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
let serde_field = serde_field_metadata(field)?;
let attributed_aliases = serde_field.aliases();
let aliases = quote!(BTreeSet::from_iter([#(#attributed_aliases),*]));
Ok(quote_spanned!(
ident.span() => {
visit.record_field(#kebab_name, crate::options_base::OptionField{
@@ -200,6 +193,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
default: &#default,
value_type: &#value_type,
example: &#example,
aliases: #aliases
})
}
))
@@ -248,3 +242,17 @@ fn _parse_key_value(input: ParseStream, name: &str) -> syn::Result<String> {
_ => Err(syn::Error::new(value.span(), "Expected literal string")),
}
}
fn serde_field_metadata(field: &Field) -> syn::Result<serde_derive_internals::attr::Field> {
let context = Ctxt::new();
let field = serde_derive_internals::attr::Field::from_ast(
&context,
0,
field,
None,
&serde_derive_internals::attr::Default::Default,
);
context.check()?;
Ok(field)
}

View File

@@ -58,7 +58,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
};
// Map from: linter (e.g., `Flake8Bugbear`) to rule code (e.g.,`"002"`) to rule data (e.g.,
// `(Rule::UnaryPrefixIncrement, RuleGroup::Unspecified, vec![])`).
// `(Rule::UnaryPrefixIncrement, RuleGroup::Stable, vec![])`).
let mut linter_to_rules: BTreeMap<Ident, BTreeMap<String, Rule>> = BTreeMap::new();
for arm in arms {

View File

@@ -1,6 +1,6 @@
use ruff_text_size::{Ranged, TextRange};
use crate::node::AnyNodeRef;
use crate::AnyNodeRef;
use crate::{self as ast, Expr};
/// Unowned pendant to [`ast::Expr`] that stores a reference instead of a owned value.

View File

@@ -8,9 +8,9 @@ use smallvec::SmallVec;
use ruff_text_size::{Ranged, TextRange};
use crate::call_path::CallPath;
use crate::node::AnyNodeRef;
use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::{walk_body, walk_stmt, StatementVisitor};
use crate::AnyNodeRef;
use crate::{
self as ast, Arguments, CmpOp, Constant, ExceptHandler, Expr, MatchCase, Pattern, Stmt,
TypeParam,

View File

@@ -2,6 +2,7 @@ use std::path::Path;
pub use expression::*;
pub use int::*;
pub use node::{AnyNode, AnyNodeRef, AstNode, NodeKind};
pub use nodes::*;
pub mod all;
@@ -14,7 +15,7 @@ pub mod helpers;
pub mod identifier;
pub mod imports;
mod int;
pub mod node;
mod node;
mod nodes;
pub mod parenthesize;
pub mod relocate;

View File

@@ -1,7 +1,7 @@
use ruff_python_trivia::{BackwardsTokenizer, CommentRanges, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextLen, TextRange};
use crate::node::AnyNodeRef;
use crate::AnyNodeRef;
use crate::ExpressionRef;
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is

View File

@@ -1,10 +1,10 @@
use crate::node::{AnyNodeRef, AstNode};
use crate::{
Alias, Arguments, BoolOp, CmpOp, Comprehension, Constant, Decorator, ElifElseClause,
ExceptHandler, Expr, Keyword, MatchCase, Mod, Operator, Parameter, ParameterWithDefault,
Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParams, UnaryOp,
WithItem,
};
use crate::{AnyNodeRef, AstNode};
/// Visitor that traverses all nodes recursively in pre-order.
pub trait PreorderVisitor<'a> {

View File

@@ -2,12 +2,12 @@ use std::fmt::{Debug, Write};
use insta::assert_snapshot;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::visitor::preorder::{
walk_alias, walk_comprehension, walk_except_handler, walk_expr, walk_keyword, walk_match_case,
walk_module, walk_parameter, walk_parameters, walk_pattern, walk_stmt, walk_type_param,
walk_with_item, PreorderVisitor,
};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{
Alias, BoolOp, CmpOp, Comprehension, Constant, ExceptHandler, Expr, Keyword, MatchCase, Mod,
Operator, Parameter, Parameters, Pattern, Stmt, TypeParam, UnaryOp, WithItem,

View File

@@ -5,12 +5,12 @@ use ruff_python_ast as ast;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::visitor::{
walk_alias, walk_comprehension, walk_except_handler, walk_expr, walk_keyword, walk_match_case,
walk_parameter, walk_parameters, walk_pattern, walk_stmt, walk_type_param, walk_with_item,
Visitor,
};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{
Alias, BoolOp, CmpOp, Comprehension, ExceptHandler, Expr, Keyword, MatchCase, Operator,
Parameter, Parameters, Pattern, Stmt, TypeParam, UnaryOp, WithItem,

View File

@@ -150,7 +150,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
N: Ranged,
Separator: Format<PyFormatContext<'ast>>,
{
self.result = self.result.and_then(|_| {
self.result = self.result.and_then(|()| {
if self.entries.is_one_or_more() {
write!(self.fmt, [token(","), separator])?;
}
@@ -190,7 +190,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
}
pub(crate) fn finish(&mut self) -> FormatResult<()> {
self.result.and_then(|_| {
self.result.and_then(|()| {
if let Some(last_end) = self.entries.position() {
let magic_trailing_comma = has_magic_trailing_comma(
TextRange::new(last_end, self.sequence_end),

View File

@@ -180,7 +180,7 @@ mod tests {
use insta::assert_debug_snapshot;
use ruff_formatter::SourceCode;
use ruff_python_ast::node::AnyNode;
use ruff_python_ast::AnyNode;
use ruff_python_ast::{StmtBreak, StmtContinue};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::{TextRange, TextSize};

View File

@@ -1,8 +1,8 @@
use std::borrow::Cow;
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
use ruff_python_ast::node::{AnyNodeRef, AstNode};
use ruff_python_ast::PySourceType;
use ruff_python_ast::{AnyNodeRef, AstNode};
use ruff_python_trivia::{
is_pragma_comment, lines_after, lines_after_ignoring_trivia, lines_before,
};

View File

@@ -96,8 +96,8 @@ pub(crate) use format::{
leading_alternate_branch_comments, leading_comments, leading_node_comments, trailing_comments,
};
use ruff_formatter::{SourceCode, SourceCodeSlice};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::visitor::preorder::{PreorderVisitor, TraversalSignal};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::Mod;
use ruff_python_trivia::{CommentRanges, PythonWhitespace};
use ruff_source_file::Locator;

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher};
@@ -52,7 +52,7 @@ impl<'a> From<AnyNodeRef<'a>> for NodeRefEqualityKey<'a> {
#[cfg(test)]
mod tests {
use crate::comments::node_key::NodeRefEqualityKey;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::StmtContinue;
use ruff_text_size::TextRange;
use std::collections::hash_map::DefaultHasher;

View File

@@ -1,7 +1,7 @@
use std::cmp::Ordering;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, Comprehension, Expr, MatchCase, ModModule, Parameters};
use ruff_python_trivia::{
find_only_token_in_range, indentation_at_offset, BackwardsTokenizer, CommentRanges,

View File

@@ -2,7 +2,7 @@ use std::fmt::Debug;
use std::iter::Peekable;
use ruff_formatter::{SourceCode, SourceCodeSlice};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{Mod, Stmt};
// The interface is designed to only export the members relevant for iterating nodes in
// pre-order.

View File

@@ -1,5 +1,5 @@
use ruff_formatter::{write, FormatRuleWithOptions};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{Constant, Expr, ExprAttribute, ExprConstant};
use ruff_python_trivia::{find_only_token_in_range, SimpleTokenKind};
use ruff_text_size::{Ranged, TextRange};

View File

@@ -1,5 +1,5 @@
use ruff_formatter::write;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprAwait;
use crate::expression::maybe_parenthesize_expression;

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{Expr, ExprBinOp};
use crate::comments::SourceComment;

View File

@@ -1,5 +1,5 @@
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{BoolOp, ExprBoolOp};
use crate::expression::binary_like::BinaryLike;

View File

@@ -1,5 +1,5 @@
use ruff_formatter::FormatRuleWithOptions;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{Expr, ExprCall};
use crate::comments::{dangling_comments, SourceComment};

View File

@@ -1,5 +1,5 @@
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{CmpOp, Expr, ExprCompare};
use crate::comments::SourceComment;

View File

@@ -1,5 +1,5 @@
use ruff_formatter::FormatRuleWithOptions;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{Constant, ExprConstant};
use ruff_text_size::{Ranged, TextLen, TextRange};

View File

@@ -1,5 +1,5 @@
use ruff_formatter::{format_args, write};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{Expr, ExprDict};
use ruff_text_size::{Ranged, TextRange};

View File

@@ -1,5 +1,5 @@
use ruff_formatter::write;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprDictComp;
use ruff_text_size::Ranged;

View File

@@ -2,7 +2,7 @@ use memchr::memchr2;
use crate::comments::SourceComment;
use ruff_formatter::FormatResult;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprFString;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprFormattedValue;
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};

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