Compare commits

...

12 Commits

Author SHA1 Message Date
Charlie Marsh
295dc542b3 Use #[cold] attribute on non-ASCII lexer path 2024-01-14 20:44:55 -05:00
Steve C
0c0d3db1b5 [flake8-bugbear] Add fix for duplicate-value (B033) (#9510)
## Summary

Adds autofix for
[B033](https://docs.astral.sh/ruff/rules/duplicate-value/)

## Test Plan

`cargo test`
2024-01-14 23:20:16 +00:00
Charlie Marsh
953d48b7f4 [flake8-simplify] Avoid some more enumerate-for-loop false positives (SIM113) (#9515)
Avoids, e.g., [this false
positive](a4fad5dda1/zerver/data_import/slack.py (L634))
from the ecosystem check.
2024-01-14 13:02:13 -05:00
Chammika Mannakkara
0003c730e0 [flake8-simplify] Implement enumerate-for-loop (SIM113) (#7777)
Implements SIM113 from #998

Added tests
Limitations 
   - No fix yet
   - Only flag cases where index variable immediately precede `for` loop

@charliermarsh please review and let me know any improvements

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-01-14 11:00:59 -05:00
port19
b6ce4f5f3a update emacs integration section to include emacs-ruff-format (#9403)
## Summary

For emacs users just seeking a ruff formatter, apheleia is overkill.
Instead, a melpa package
[emacs-ruff-format](https://github.com/scop/emacs-ruff-format) exists
now.
This change prepends a mention of this package, without removing
apheleia as an alternative.

## Test Plan

This is how I integrated ruff into my emacs.
2024-01-14 02:55:51 +00:00
Hoël Bagard
e8d7a6dfce Use the LinterSettings's tab size when expanding indent (#9506)
## Summary
In the `logical_lines`'s `expand_indent` , respect the
`LinterSettings::tab_size` setting instead of hardcoding the size of
tabs to 8.

Also see [this
conversation](https://github.com/astral-sh/ruff/pull/9266#discussion_r1447102212)

## Test Plan

Tested by running `cargo test`
2024-01-13 21:06:50 -05:00
pabepadu
ef2798f758 fix(docs): admonition in dark mode (#9502)
## Summary

The admonition in dark mode are in actually same as in light mode here:
https://docs.astral.sh/ruff/formatter/#ruff-format

I propose to move admonition in real dark mode as shown below:
- Before the PR https://github.com/astral-sh/ruff/pull/9385

![image](https://github.com/astral-sh/ruff/assets/45884742/abb7e0db-bf12-49cd-9f9b-e353accd571f)
- Current version

![image](https://github.com/astral-sh/ruff/assets/45884742/da8ab46d-dc87-412e-be2b-ac937cd87666)
- Proposal

![image](https://github.com/astral-sh/ruff/assets/45884742/7279cb02-e6af-4320-9dee-486fbfddcbc8)

Close #9501

## Test Plan

Documentation was regenerated via mkdocs and the supplied requirements.
2024-01-13 07:43:13 -05:00
Charlie Marsh
957a1f35c4 Ignore unnecessary dunder calls within dunder definitions (#9496)
Closes https://github.com/astral-sh/ruff/issues/9486.
2024-01-12 14:48:42 -05:00
Charlie Marsh
009430e034 [ruff] Avoid treating named expressions as static keys (RUF011) (#9494)
Closes https://github.com/astral-sh/ruff/issues/9487.
2024-01-12 14:33:45 -05:00
Jane Lewis
7504bf347b --show-settings displays active settings in a far more readable format (#9464)
<!--
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

Fixes #8334.

`Display` has been implemented for `ruff_workspace::Settings`, which
gives a much nicer and more readable output to `--show-settings`.

Internally, a `display_settings` utility macro has been implemented to
reduce the boilerplate of the display code.

### Work to be done

- [x] A lot of formatting for `Vec<_>` and `HashSet<_>` types have been
stubbed out, using `Debug` as a fallback. There should be a way to add
generic formatting support for these types as a modifier in
`display_settings`.
- [x] Several complex types were also stubbed out and need proper
`Display` implementations rather than falling back on `Debug`.
- [x] An open question needs to be answered: how important is it that
the output be valid TOML? Some types in settings, such as a hash-map
from a glob pattern to a multi-variant enum, will be hard to rework into
valid _and_ readable TOML.
- [x] Tests need to be implemented.

## Test Plan

Tests consist of a snapshot test for the default `--show-settings`
output and a doctest for `display_settings!`.
2024-01-12 14:30:29 -05:00
Charlie Marsh
fee64b52ba Limit inplace diagnostics to methods that accept inplace (#9495)
## Summary

This should reduce false positives like
https://github.com/astral-sh/ruff/issues/9491, by ignoring methods that
are clearly not on a DataFrame.

Closes https://github.com/astral-sh/ruff/issues/9491.
2024-01-12 14:12:54 -05:00
Charlie Marsh
3261d16e61 Add --extension support to the formatter (#9483)
## Summary

We added `--extension` to `ruff check`, but it's equally applicable to
`ruff format`.

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

Resolves https://github.com/astral-sh/ruff/discussions/9481.

## Test Plan

`cargo test`
2024-01-12 18:53:25 +00:00
81 changed files with 2713 additions and 181 deletions

View File

@@ -290,6 +290,10 @@ pub struct CheckCommand {
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For
/// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`.
#[arg(long, value_delimiter = ',')]
pub extension: Option<Vec<ExtensionPair>>,
/// Exit with status code "0", even upon detecting lint violations.
#[arg(
short,
@@ -352,9 +356,6 @@ pub struct CheckCommand {
conflicts_with = "watch",
)]
pub show_settings: bool,
/// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]).
#[arg(long, value_delimiter = ',', hide = true)]
pub extension: Option<Vec<ExtensionPair>>,
/// Dev-only argument to show fixes
#[arg(long, hide = true)]
pub ecosystem_ci: bool,
@@ -423,6 +424,10 @@ pub struct FormatCommand {
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For
/// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`.
#[arg(long, value_delimiter = ',')]
pub extension: Option<Vec<ExtensionPair>>,
/// The minimum Python version that should be supported.
#[arg(long, value_enum)]
pub target_version: Option<PythonVersion>,
@@ -571,6 +576,7 @@ impl FormatCommand {
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
target_version: self.target_version,
cache_dir: self.cache_dir,
extension: self.extension,
// Unsupported on the formatter CLI, but required on `Overrides`.
..CliOverrides::default()
@@ -739,7 +745,7 @@ impl ConfigurationTransformer for CliOverrides {
config.target_version = Some(*target_version);
}
if let Some(extension) = &self.extension {
config.lint.extension = Some(extension.clone().into_iter().collect());
config.extension = Some(extension.iter().cloned().collect());
}
config

View File

@@ -106,13 +106,19 @@ pub(crate) fn format(
match entry {
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 settings = resolver.resolve(path);
let source_type = match settings.formatter.extension.get(path) {
None => match SourceType::from(path) {
SourceType::Python(source_type) => source_type,
SourceType::Toml(_) => {
// Ignore any non-Python files.
return None;
}
},
Some(language) => PySourceType::from(language),
};
// Ignore files that are excluded from formatting
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion(

View File

@@ -53,16 +53,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
}
let path = cli.stdin_filename.as_deref();
let settings = &resolver.base_settings().formatter;
let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success);
let source_type = match path.and_then(|path| settings.extension.get(path)) {
None => match path.map(SourceType::from).unwrap_or_default() {
SourceType::Python(source_type) => source_type,
SourceType::Toml(_) => {
if mode.is_write() {
parrot_stdin()?;
}
return Ok(ExitStatus::Success);
}
},
Some(language) => PySourceType::from(language),
};
// Format the file.
match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) {
match format_source_code(path, settings, source_type, mode) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => {

View File

@@ -35,7 +35,7 @@ pub(crate) fn show_settings(
if let Some(settings_path) = pyproject_config.path.as_ref() {
writeln!(writer, "Settings path: {settings_path:?}")?;
}
writeln!(writer, "{settings:#?}")?;
write!(writer, "{settings}")?;
Ok(())
}

View File

@@ -17,7 +17,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::{ExtensionMapping, UnsafeFixes};
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};
@@ -179,11 +179,6 @@ impl AddAssign for FixMap {
}
}
fn override_source_type(path: Option<&Path>, extension: &ExtensionMapping) -> Option<PySourceType> {
let ext = path?.extension()?.to_str()?;
extension.get(ext).map(PySourceType::from)
}
/// Lint the source code at the given `Path`.
pub(crate) fn lint_path(
path: &Path,
@@ -228,7 +223,7 @@ pub(crate) fn lint_path(
debug!("Checking: {}", path.display());
let source_type = match override_source_type(Some(path), &settings.extension) {
let source_type = match settings.extension.get(path).map(PySourceType::from) {
Some(source_type) => source_type,
None => match SourceType::from(path) {
SourceType::Toml(TomlSourceType::Pyproject) => {
@@ -398,15 +393,14 @@ pub(crate) fn lint_stdin(
fix_mode: flags::FixMode,
) -> Result<Diagnostics> {
// TODO(charlie): Support `pyproject.toml`.
let source_type = if let Some(source_type) =
override_source_type(path, &settings.linter.extension)
{
source_type
} else {
let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else {
return Ok(Diagnostics::default());
};
source_type
let source_type = match path.and_then(|path| settings.linter.extension.get(path)) {
None => match path.map(SourceType::from).unwrap_or_default() {
SourceType::Python(source_type) => source_type,
SourceType::Toml(_) => {
return Ok(Diagnostics::default());
}
},
Some(language) => PySourceType::from(language),
};
// Extract the sources from the file.

View File

@@ -1468,3 +1468,73 @@ fn test_notebook_trailing_semicolon() {
----- stderr -----
"###);
}
#[test]
fn extension() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
include = ["*.ipy"]
"#,
)?;
fs::write(
tempdir.path().join("main.ipy"),
r#"
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
"metadata": {},
"outputs": [],
"source": [
"x=1"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.arg("format")
.arg("--no-cache")
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
.args(["--extension", "ipy:ipynb"])
.arg("."), @r###"
success: true
exit_code: 0
----- stdout -----
1 file reformatted
----- stderr -----
"###);
Ok(())
}

View File

@@ -436,3 +436,75 @@ ignore = ["D203", "D212"]
"###);
Ok(())
}
#[test]
fn extension() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
include = ["*.ipy"]
"#,
)?;
fs::write(
tempdir.path().join("main.ipy"),
r#"
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d",
"metadata": {},
"outputs": [],
"source": [
"import os"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
"#,
)?;
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(["--extension", "ipy:ipynb"])
.arg("."), @r###"
success: false
exit_code: 1
----- stdout -----
main.ipy:cell 1:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
"###);
Ok(())
}

View File

@@ -0,0 +1,29 @@
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use std::path::Path;
use std::process::Command;
const BIN_NAME: &str = "ruff";
#[cfg(not(target_os = "windows"))]
const TEST_FILTERS: &[(&str, &str)] = &[
("\"[^\\*\"]*/pyproject.toml", "\"[BASEPATH]/pyproject.toml"),
("\".*/crates", "\"[BASEPATH]/crates"),
("\".*/\\.ruff_cache", "\"[BASEPATH]/.ruff_cache"),
("\".*/ruff\"", "\"[BASEPATH]\""),
];
#[cfg(target_os = "windows")]
const TEST_FILTERS: &[(&str, &str)] = &[
(r#""[^\*"]*\\pyproject.toml"#, "\"[BASEPATH]/pyproject.toml"),
(r#"".*\\crates"#, "\"[BASEPATH]/crates"),
(r#"".*\\\.ruff_cache"#, "\"[BASEPATH]/.ruff_cache"),
(r#"".*\\ruff""#, "\"[BASEPATH]\""),
(r#"\\+(\w\w|\s|")"#, "/$1"),
];
#[test]
fn display_default_settings() {
insta::with_settings!({ filters => TEST_FILTERS.to_vec() }, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--show-settings", "unformatted.py"]).current_dir(Path::new("./resources/test/fixtures")));
});
}

View File

@@ -0,0 +1,368 @@
---
source: crates/ruff_cli/tests/show_settings.rs
info:
program: ruff
args:
- check
- "--show-settings"
- unformatted.py
---
success: true
exit_code: 0
----- stdout -----
Resolved settings for: "[BASEPATH]/crates/ruff_cli/resources/test/fixtures/unformatted.py"
Settings path: "[BASEPATH]/pyproject.toml"
# General Settings
cache_dir = "[BASEPATH]/.ruff_cache"
fix = false
fix_only = false
output_format = text
show_fixes = false
show_source = false
unsafe_fixes = hint
# File Resolver Settings
file_resolver.exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
file_resolver.extend_exclude = [
"crates/ruff_linter/resources/",
"crates/ruff_python_formatter/resources/",
]
file_resolver.force_exclude = false
file_resolver.include = [
"*.py",
"*.pyi",
"**/pyproject.toml",
]
file_resolver.extend_include = []
file_resolver.respect_gitignore = true
file_resolver.project_root = "[BASEPATH]"
# Linter Settings
linter.exclude = []
linter.project_root = "[BASEPATH]"
linter.rules.enabled = [
MultipleImportsOnOneLine,
ModuleImportNotAtTopOfFile,
MultipleStatementsOnOneLineColon,
MultipleStatementsOnOneLineSemicolon,
UselessSemicolon,
NoneComparison,
TrueFalseComparison,
NotInTest,
NotIsTest,
TypeComparison,
BareExcept,
LambdaAssignment,
AmbiguousVariableName,
AmbiguousClassName,
AmbiguousFunctionName,
IOError,
SyntaxError,
UnusedImport,
ImportShadowedByLoopVar,
UndefinedLocalWithImportStar,
LateFutureImport,
UndefinedLocalWithImportStarUsage,
UndefinedLocalWithNestedImportStarUsage,
FutureFeatureNotDefined,
PercentFormatInvalidFormat,
PercentFormatExpectedMapping,
PercentFormatExpectedSequence,
PercentFormatExtraNamedArguments,
PercentFormatMissingArgument,
PercentFormatMixedPositionalAndNamed,
PercentFormatPositionalCountMismatch,
PercentFormatStarRequiresSequence,
PercentFormatUnsupportedFormatCharacter,
StringDotFormatInvalidFormat,
StringDotFormatExtraNamedArguments,
StringDotFormatExtraPositionalArguments,
StringDotFormatMissingArguments,
StringDotFormatMixingAutomatic,
FStringMissingPlaceholders,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable,
ExpressionsInStarAssignment,
MultipleStarredExpressions,
AssertTuple,
IsLiteral,
InvalidPrintSyntax,
IfTuple,
BreakOutsideLoop,
ContinueOutsideLoop,
YieldOutsideFunction,
ReturnOutsideFunction,
DefaultExceptNotLast,
ForwardAnnotationSyntaxError,
RedefinedWhileUnused,
UndefinedName,
UndefinedExport,
UndefinedLocal,
UnusedVariable,
UnusedAnnotation,
RaiseNotImplemented,
]
linter.rules.should_fix = [
MultipleImportsOnOneLine,
ModuleImportNotAtTopOfFile,
MultipleStatementsOnOneLineColon,
MultipleStatementsOnOneLineSemicolon,
UselessSemicolon,
NoneComparison,
TrueFalseComparison,
NotInTest,
NotIsTest,
TypeComparison,
BareExcept,
LambdaAssignment,
AmbiguousVariableName,
AmbiguousClassName,
AmbiguousFunctionName,
IOError,
SyntaxError,
UnusedImport,
ImportShadowedByLoopVar,
UndefinedLocalWithImportStar,
LateFutureImport,
UndefinedLocalWithImportStarUsage,
UndefinedLocalWithNestedImportStarUsage,
FutureFeatureNotDefined,
PercentFormatInvalidFormat,
PercentFormatExpectedMapping,
PercentFormatExpectedSequence,
PercentFormatExtraNamedArguments,
PercentFormatMissingArgument,
PercentFormatMixedPositionalAndNamed,
PercentFormatPositionalCountMismatch,
PercentFormatStarRequiresSequence,
PercentFormatUnsupportedFormatCharacter,
StringDotFormatInvalidFormat,
StringDotFormatExtraNamedArguments,
StringDotFormatExtraPositionalArguments,
StringDotFormatMissingArguments,
StringDotFormatMixingAutomatic,
FStringMissingPlaceholders,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable,
ExpressionsInStarAssignment,
MultipleStarredExpressions,
AssertTuple,
IsLiteral,
InvalidPrintSyntax,
IfTuple,
BreakOutsideLoop,
ContinueOutsideLoop,
YieldOutsideFunction,
ReturnOutsideFunction,
DefaultExceptNotLast,
ForwardAnnotationSyntaxError,
RedefinedWhileUnused,
UndefinedName,
UndefinedExport,
UndefinedLocal,
UnusedVariable,
UnusedAnnotation,
RaiseNotImplemented,
]
linter.per_file_ignores = {}
linter.safety_table.forced_safe = []
linter.safety_table.forced_unsafe = []
linter.target_version = Py37
linter.preview = disabled
linter.explicit_preview_rules = false
linter.extension.mapping = {}
linter.allowed_confusables = []
linter.builtins = []
linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
linter.external = []
linter.ignore_init_module_imports = false
linter.logger_objects = []
linter.namespace_packages = []
linter.src = ["[BASEPATH]"]
linter.tab_size = 4
linter.line_length = 88
linter.task_tags = [
TODO,
FIXME,
XXX,
]
linter.typing_modules = []
# Linter Plugins
linter.flake8_annotations.mypy_init_return = false
linter.flake8_annotations.suppress_dummy_args = false
linter.flake8_annotations.suppress_none_returning = false
linter.flake8_annotations.allow_star_arg_any = false
linter.flake8_annotations.ignore_fully_untyped = false
linter.flake8_bandit.hardcoded_tmp_directory = [
/tmp,
/var/tmp,
/dev/shm,
]
linter.flake8_bandit.check_typed_exception = false
linter.flake8_bugbear.extend_immutable_calls = []
linter.flake8_builtins.builtins_ignorelist = []
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*
linter.flake8_copyright.author = none
linter.flake8_copyright.min_file_size = 0
linter.flake8_errmsg.max_string_length = 0
linter.flake8_gettext.functions_names = [
_,
gettext,
ngettext,
]
linter.flake8_implicit_str_concat.allow_multiline = true
linter.flake8_import_conventions.aliases = {"matplotlib": "mpl", "matplotlib.pyplot": "plt", "pandas": "pd", "seaborn": "sns", "tensorflow": "tf", "networkx": "nx", "plotly.express": "px", "polars": "pl", "numpy": "np", "panel": "pn", "pyarrow": "pa", "altair": "alt", "tkinter": "tk", "holoviews": "hv"}
linter.flake8_import_conventions.banned_aliases = {}
linter.flake8_import_conventions.banned_from = []
linter.flake8_pytest_style.fixture_parentheses = true
linter.flake8_pytest_style.parametrize_names_type = tuple
linter.flake8_pytest_style.parametrize_values_type = list
linter.flake8_pytest_style.parametrize_values_row_type = tuple
linter.flake8_pytest_style.raises_require_match_for = [
BaseException,
Exception,
ValueError,
OSError,
IOError,
EnvironmentError,
socket.error,
]
linter.flake8_pytest_style.raises_extend_require_match_for = []
linter.flake8_pytest_style.mark_parentheses = true
linter.flake8_quotes.inline_quotes = double
linter.flake8_quotes.multiline_quotes = double
linter.flake8_quotes.docstring_quotes = double
linter.flake8_quotes.avoid_escape = true
linter.flake8_self.ignore_names = [
_make,
_asdict,
_replace,
_fields,
_field_defaults,
_name_,
_value_,
]
linter.flake8_tidy_imports.ban_relative_imports = "parents"
linter.flake8_tidy_imports.banned_api = {}
linter.flake8_tidy_imports.banned_module_level_imports = []
linter.flake8_type_checking.strict = false
linter.flake8_type_checking.exempt_modules = [
typing,
typing_extensions,
]
linter.flake8_type_checking.runtime_required_base_classes = []
linter.flake8_type_checking.runtime_required_decorators = []
linter.flake8_type_checking.quote_annotations = false
linter.flake8_unused_arguments.ignore_variadic_names = false
linter.isort.required_imports = []
linter.isort.combine_as_imports = false
linter.isort.force_single_line = false
linter.isort.force_sort_within_sections = false
linter.isort.detect_same_package = true
linter.isort.case_sensitive = false
linter.isort.force_wrap_aliases = false
linter.isort.force_to_top = []
linter.isort.known_modules = {}
linter.isort.order_by_type = true
linter.isort.relative_imports_order = furthest_to_closest
linter.isort.single_line_exclusions = []
linter.isort.split_on_trailing_comma = true
linter.isort.classes = []
linter.isort.constants = []
linter.isort.variables = []
linter.isort.no_lines_before = []
linter.isort.lines_after_imports = -1
linter.isort.lines_between_types = 0
linter.isort.forced_separate = []
linter.isort.section_order = [
known { type = future },
known { type = standard_library },
known { type = third_party },
known { type = first_party },
known { type = local_folder },
]
linter.isort.no_sections = false
linter.isort.from_first = false
linter.isort.length_sort = false
linter.isort.length_sort_straight = false
linter.mccabe.max_complexity = 10
linter.pep8_naming.ignore_names = [
setUp,
tearDown,
setUpClass,
tearDownClass,
setUpModule,
tearDownModule,
asyncSetUp,
asyncTearDown,
setUpTestData,
failureException,
longMessage,
maxDiff,
]
linter.pep8_naming.classmethod_decorators = []
linter.pep8_naming.staticmethod_decorators = []
linter.pycodestyle.max_line_length = 88
linter.pycodestyle.max_doc_length = none
linter.pycodestyle.ignore_overlong_task_comments = false
linter.pyflakes.extend_generics = []
linter.pylint.allow_magic_value_types = [
str,
bytes,
]
linter.pylint.allow_dunder_method_names = []
linter.pylint.max_args = 5
linter.pylint.max_positional_args = 5
linter.pylint.max_returns = 6
linter.pylint.max_bool_expr = 5
linter.pylint.max_branches = 12
linter.pylint.max_statements = 50
linter.pylint.max_public_methods = 20
linter.pylint.max_locals = 15
linter.pyupgrade.keep_runtime_typing = false
# Formatter Settings
formatter.exclude = []
formatter.target_version = Py37
formatter.preview = disabled
formatter.line_width = 88
formatter.line_ending = auto
formatter.indent_style = space
formatter.indent_width = 4
formatter.quote_style = double
formatter.magic_trailing_comma = respect
formatter.docstring_code_format = disabled
formatter.docstring_code_line_width = dynamic
----- stderr -----

View File

@@ -35,6 +35,7 @@ mod source_code;
use crate::formatter::Formatter;
use crate::group_id::UniqueGroupIdBuilder;
use crate::prelude::TagKind;
use std::fmt;
use std::fmt::{Debug, Display};
use std::marker::PhantomData;
use std::num::{NonZeroU16, NonZeroU8, TryFromIntError};
@@ -113,6 +114,12 @@ impl Default for IndentWidth {
}
}
impl Display for IndentWidth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl TryFrom<u8> for IndentWidth {
type Error = TryFromIntError;
@@ -146,6 +153,12 @@ impl Default for LineWidth {
}
}
impl Display for LineWidth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl TryFrom<u16> for LineWidth {
type Error = TryFromIntError;

View File

@@ -2,10 +2,28 @@
# Errors.
###
incorrect_set = {"value1", 23, 5, "value1"}
incorrect_set = {1, 1, 2}
incorrect_set_multiline = {
"value1",
23,
5,
"value1",
# B033
}
incorrect_set = {1, 1}
incorrect_set = {1, 1,}
incorrect_set = {0, 1, 1,}
incorrect_set = {0, 1, 1}
incorrect_set = {
0,
1,
1,
}
###
# Non-errors.
###
correct_set = {"value1", 23, 5}
correct_set = {5, "5"}
correct_set = {5}
correct_set = {}

View File

@@ -0,0 +1,195 @@
def func():
# SIM113
idx = 0
for x in range(5):
g(x, idx)
idx += 1
h(x)
def func():
# SIM113
sum = 0
idx = 0
for x in range(5):
if g(x):
break
idx += 1
sum += h(x, idx)
def func():
# SIM113
idx = 0
for x, y in zip(range(5), range(5)):
g(x)
h(x, y)
idx += 1
def func():
# SIM113
idx = 0
sum = 0
for x in range(5):
sum += h(x, idx)
idx += 1
def func():
# SIM113
sum, idx = 0, 0
for x in range(5):
g(x, idx)
idx += 1
h(x)
def func():
# OK (index doesn't start at 0
idx = 10
for x in range(5):
g(x, idx)
idx += 1
h(x)
def func():
# OK (incremented by more than 1)
idx = 0
for x in range(5):
g(x, idx)
idx += 2
h(x)
def func():
# OK (incremented in `if`)
idx = 0
for x in range(5):
if g(x):
idx += 1
h(x)
def func():
# OK (`continue` in match-case)
idx = 0
for x in range(5):
match g(x):
case 1:
h(x)
case 2:
continue
case _:
h(idx)
idx += 1
def func():
# OK (`continue` inside `with` clause)
idx = 0
for x in range(5):
with context as c:
if g(x):
continue
h(x, idx, c)
idx += 1
def func():
# OK (incremented in `try` block)
idx = 0
for x in range(5):
try:
g(x, idx)
except:
h(x)
continue
idx += 1
def func(idx: 0):
# OK (index is an argument)
for x in range(5):
g(x, idx)
idx += 1
def func(x: int):
# OK (index _may_ be non-zero)
if x > 0:
idx = 1
else:
idx = 0
for x in range(5):
g(x, idx)
idx += 1
h(x)
def func():
# OK
idx = 0
for x in range(5):
g(x, idx)
idx += 1
idx += 1
h(x)
def func():
# OK
idx = 0
for x in range(5):
g(x, idx)
idx += 1
if idx > 10:
idx *= 2
h(x)
def func():
# OK (index used after loop)
idx = 0
for x in range(5):
g(x, idx)
idx += 1
h(x)
print(idx)
def func():
# OK (index within nested loop)
idx = 0
for x in range(5):
for y in range(5):
g(x, idx)
idx += 1
def func():
# OK (index used within inner)
idx = 0
def inner():
print(idx)
for x in range(5):
for y in range(5):
g(x, idx)
idx += 1
def func():
# OK (index used as nonlocal inner)
idx = 0
def inner():
nonlocal idx
idx = 3
for x in range(5):
for y in range(5):
g(x, idx)
idx += 1

View File

@@ -31,3 +31,6 @@ import torch
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call
(x.drop(["a"], axis=1, inplace=True))
# This method doesn't take exist in Pandas, so ignore it.
x.rotate_z(45, inplace=True)

View File

@@ -24,6 +24,12 @@ class Thing:
super().__init__() # OK
super().__class__(stuff=(1, 2, 3)) # OK
def __getattribute__(self, item):
return object.__getattribute__(self, item) # OK
def do_thing(self, item):
return object.__getattribute__(self, item) # PLC2801
blah = lambda: {"a": 1}.__delitem__("a") # OK

View File

@@ -10,6 +10,8 @@ constant = 5
{value.attribute: value.upper() for value in data for constant in data}
{constant[value]: value.upper() for value in data for constant in data}
{value[constant]: value.upper() for value in data for constant in data}
{local_id: token for token in tokens if (local_id := _extract_local_id(token)) is not None}
{key: kwargs.get(key) for key in kwargs.keys() if not params.get(key)}
# Errors
{"key": value.upper() for value in data}
@@ -20,3 +22,5 @@ constant = 5
{constant + constant: value.upper() for value in data}
{constant.attribute: value.upper() for value in data}
{constant[0]: value.upper() for value in data}
{tokens: token for token in tokens}

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::Stmt;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bugbear, perflint, pyupgrade, refurb};
use crate::rules::{flake8_bugbear, flake8_simplify, perflint, pyupgrade, refurb};
/// Run lint rules over all deferred for-loops in the [`SemanticModel`].
pub(crate) fn deferred_for_loops(checker: &mut Checker) {
@@ -27,6 +27,9 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryEnumerate) {
refurb::rules::unnecessary_enumerate(checker, stmt_for);
}
if checker.enabled(Rule::EnumerateForLoop) {
flake8_simplify::rules::enumerate_for_loop(checker, stmt_for);
}
}
}
}

View File

@@ -980,9 +980,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_pie::rules::unnecessary_spread(checker, dict);
}
}
Expr::Set(ast::ExprSet { elts, range: _ }) => {
Expr::Set(set) => {
if checker.enabled(Rule::DuplicateValue) {
flake8_bugbear::rules::duplicate_value(checker, elts);
flake8_bugbear::rules::duplicate_value(checker, set);
}
}
Expr::Yield(_) => {

View File

@@ -1258,9 +1258,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
},
) => {
if checker.any_enabled(&[
Rule::UnusedLoopControlVariable,
Rule::EnumerateForLoop,
Rule::IncorrectDictIterator,
Rule::UnnecessaryEnumerate,
Rule::UnusedLoopControlVariable,
Rule::YieldInForLoop,
]) {
checker.deferred.for_loops.push(checker.semantic.snapshot());

View File

@@ -14,14 +14,15 @@ use crate::rules::pycodestyle::rules::logical_lines::{
};
use crate::settings::LinterSettings;
/// Return the amount of indentation, expanding tabs to the next multiple of 8.
fn expand_indent(line: &str) -> usize {
/// Return the amount of indentation, expanding tabs to the next multiple of the settings' tab size.
fn expand_indent(line: &str, settings: &LinterSettings) -> usize {
let line = line.trim_end_matches(['\n', '\r']);
let mut indent = 0;
let tab_size = settings.tab_size.as_usize();
for c in line.bytes() {
match c {
b'\t' => indent = (indent / 8) * 8 + 8,
b'\t' => indent = (indent / tab_size) * tab_size + tab_size,
b' ' => indent += 1,
_ => break,
}
@@ -84,7 +85,7 @@ pub(crate) fn check_logical_lines(
TextRange::new(locator.line_start(first_token.start()), first_token.start())
};
let indent_level = expand_indent(locator.slice(range));
let indent_level = expand_indent(locator.slice(range), settings);
let indent_size = 4;

View File

@@ -454,6 +454,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "109") => (RuleGroup::Stable, rules::flake8_simplify::rules::CompareWithTuple),
(Flake8Simplify, "110") => (RuleGroup::Stable, rules::flake8_simplify::rules::ReimplementedBuiltin),
(Flake8Simplify, "112") => (RuleGroup::Stable, rules::flake8_simplify::rules::UncapitalizedEnvironmentVariables),
(Flake8Simplify, "113") => (RuleGroup::Preview, rules::flake8_simplify::rules::EnumerateForLoop),
(Flake8Simplify, "114") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfWithSameArms),
(Flake8Simplify, "115") => (RuleGroup::Stable, rules::flake8_simplify::rules::OpenFileWithContextHandler),
(Flake8Simplify, "116") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictLookup),

View File

@@ -1,4 +1,5 @@
use std::error::Error;
use std::fmt;
use std::hash::Hasher;
use std::num::{NonZeroU16, NonZeroU8, ParseIntError};
use std::str::FromStr;
@@ -39,6 +40,12 @@ impl Default for LineLength {
}
}
impl fmt::Display for LineLength {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl CacheKey for LineLength {
fn cache_key(&self, state: &mut CacheKeyHasher) {
state.write_u16(self.0.get());
@@ -248,6 +255,12 @@ impl Default for IndentWidth {
}
}
impl fmt::Display for IndentWidth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl From<NonZeroU8> for IndentWidth {
fn from(tab_size: NonZeroU8) -> Self {
Self(tab_size)

View File

@@ -1,6 +1,6 @@
use crate::registry::Rule;
use ruff_macros::CacheKey;
use std::fmt::{Debug, Formatter};
use std::fmt::{Debug, Display, Formatter};
use std::iter::FusedIterator;
const RULESET_SIZE: usize = 13;
@@ -269,6 +269,21 @@ impl Debug for RuleSet {
}
}
impl Display for RuleSet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
write!(f, "[]")?;
} else {
writeln!(f, "[")?;
for rule in self {
writeln!(f, "\t{rule:?},")?;
}
write!(f, "]")?;
}
Ok(())
}
}
impl FromIterator<Rule> for RuleSet {
fn from_iter<T: IntoIterator<Item = Rule>>(iter: T) -> Self {
let mut set = RuleSet::empty();

View File

@@ -1,6 +1,8 @@
//! Settings for the `flake-annotations` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, Default, CacheKey)]
#[allow(clippy::struct_excessive_bools)]
@@ -11,3 +13,20 @@ pub struct Settings {
pub allow_star_arg_any: bool,
pub ignore_fully_untyped: bool,
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_annotations",
fields = [
self.mypy_init_return,
self.suppress_dummy_args,
self.suppress_none_returning,
self.allow_star_arg_any,
self.ignore_fully_untyped
]
}
Ok(())
}
}

View File

@@ -1,6 +1,8 @@
//! Settings for the `flake8-bandit` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
pub fn default_tmp_dirs() -> Vec<String> {
["/tmp", "/var/tmp", "/dev/shm"]
@@ -22,3 +24,17 @@ impl Default for Settings {
}
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_bandit",
fields = [
self.hardcoded_tmp_directory | array,
self.check_typed_exception
]
}
Ok(())
}
}

View File

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

View File

@@ -1,9 +1,11 @@
use ruff_python_ast::Expr;
use anyhow::{Context, Result};
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -31,28 +33,82 @@ pub struct DuplicateValue {
}
impl Violation for DuplicateValue {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let DuplicateValue { value } = self;
format!("Sets should not contain duplicate item `{value}`")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Remove duplicate item"))
}
}
/// B033
pub(crate) fn duplicate_value(checker: &mut Checker, elts: &Vec<Expr>) {
pub(crate) fn duplicate_value(checker: &mut Checker, set: &ast::ExprSet) {
let mut seen_values: FxHashSet<ComparableExpr> = FxHashSet::default();
for elt in elts {
for (index, elt) in set.elts.iter().enumerate() {
if elt.is_literal_expr() {
let comparable_value: ComparableExpr = elt.into();
if !seen_values.insert(comparable_value) {
checker.diagnostics.push(Diagnostic::new(
let mut diagnostic = Diagnostic::new(
DuplicateValue {
value: checker.generator().expr(elt),
},
elt.range(),
));
);
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
remove_member(set, index, checker.locator().contents()).map(Fix::safe_edit)
});
}
checker.diagnostics.push(diagnostic);
}
};
}
}
/// Remove the member at the given index from the [`ast::ExprSet`].
fn remove_member(set: &ast::ExprSet, index: usize, source: &str) -> Result<Edit> {
if index < set.elts.len() - 1 {
// Case 1: the expression is _not_ the last node, so delete from the start of the
// expression to the end of the subsequent comma.
// Ex) Delete `"a"` in `{"a", "b", "c"}`.
let mut tokenizer = SimpleTokenizer::starts_at(set.elts[index].end(), source);
// Find the trailing comma.
tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
// Find the next non-whitespace token.
let next = tokenizer
.find(|token| {
token.kind != SimpleTokenKind::Whitespace && token.kind != SimpleTokenKind::Newline
})
.context("Unable to find next token")?;
Ok(Edit::deletion(set.elts[index].start(), next.start()))
} else if index > 0 {
// Case 2: the expression is the last node, but not the _only_ node, so delete from the
// start of the previous comma to the end of the expression.
// Ex) Delete `"c"` in `{"a", "b", "c"}`.
let mut tokenizer = SimpleTokenizer::starts_at(set.elts[index - 1].end(), source);
// Find the trailing comma.
let comma = tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
Ok(Edit::deletion(comma.start(), set.elts[index].end()))
} else {
// Case 3: expression is the only node, so delete it.
// Ex) Delete `"a"` in `{"a"}`.
Ok(Edit::range_deletion(set.elts[index].range()))
}
}

View File

@@ -2,7 +2,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::helpers;
use ruff_python_ast::helpers::NameFinder;
use ruff_python_ast::helpers::{NameFinder, StoredNameFinder};
use ruff_python_ast::visitor::Visitor;
use ruff_text_size::Ranged;
@@ -80,7 +80,7 @@ impl Violation for UnusedLoopControlVariable {
/// B007
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, stmt_for: &ast::StmtFor) {
let control_names = {
let mut finder = NameFinder::default();
let mut finder = StoredNameFinder::default();
finder.visit_expr(stmt_for.target.as_ref());
finder.names
};

View File

@@ -1,8 +1,23 @@
//! Settings for the `flake8-bugbear` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub extend_immutable_calls: Vec<String>,
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_bugbear",
fields = [
self.extend_immutable_calls | array
]
}
Ok(())
}
}

View File

@@ -7,17 +7,85 @@ B033.py:4:35: B033 Sets should not contain duplicate item `"value1"`
3 | ###
4 | incorrect_set = {"value1", 23, 5, "value1"}
| ^^^^^^^^ B033
5 | incorrect_set = {1, 1}
5 | incorrect_set = {1, 1, 2}
6 | incorrect_set_multiline = {
|
= help: Remove duplicate item
B033.py:5:21: B033 Sets should not contain duplicate item `1`
|
3 | ###
4 | incorrect_set = {"value1", 23, 5, "value1"}
5 | incorrect_set = {1, 1}
5 | incorrect_set = {1, 1, 2}
| ^ B033
6 |
7 | ###
6 | incorrect_set_multiline = {
7 | "value1",
|
= help: Remove duplicate item
B033.py:10:5: B033 Sets should not contain duplicate item `"value1"`
|
8 | 23,
9 | 5,
10 | "value1",
| ^^^^^^^^ B033
11 | # B033
12 | }
|
= help: Remove duplicate item
B033.py:13:21: B033 Sets should not contain duplicate item `1`
|
11 | # B033
12 | }
13 | incorrect_set = {1, 1}
| ^ B033
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
|
= help: Remove duplicate item
B033.py:14:21: B033 Sets should not contain duplicate item `1`
|
12 | }
13 | incorrect_set = {1, 1}
14 | incorrect_set = {1, 1,}
| ^ B033
15 | incorrect_set = {0, 1, 1,}
16 | incorrect_set = {0, 1, 1}
|
= help: Remove duplicate item
B033.py:15:24: B033 Sets should not contain duplicate item `1`
|
13 | incorrect_set = {1, 1}
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
| ^ B033
16 | incorrect_set = {0, 1, 1}
17 | incorrect_set = {
|
= help: Remove duplicate item
B033.py:16:24: B033 Sets should not contain duplicate item `1`
|
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
16 | incorrect_set = {0, 1, 1}
| ^ B033
17 | incorrect_set = {
18 | 0,
|
= help: Remove duplicate item
B033.py:20:5: B033 Sets should not contain duplicate item `1`
|
18 | 0,
19 | 1,
20 | 1,
| ^ B033
21 | }
|
= help: Remove duplicate item

View File

@@ -0,0 +1,169 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B033.py:4:35: B033 [*] Sets should not contain duplicate item `"value1"`
|
2 | # Errors.
3 | ###
4 | incorrect_set = {"value1", 23, 5, "value1"}
| ^^^^^^^^ B033
5 | incorrect_set = {1, 1, 2}
6 | incorrect_set_multiline = {
|
= help: Remove duplicate item
Safe fix
1 1 | ###
2 2 | # Errors.
3 3 | ###
4 |-incorrect_set = {"value1", 23, 5, "value1"}
4 |+incorrect_set = {"value1", 23, 5}
5 5 | incorrect_set = {1, 1, 2}
6 6 | incorrect_set_multiline = {
7 7 | "value1",
B033.py:5:21: B033 [*] Sets should not contain duplicate item `1`
|
3 | ###
4 | incorrect_set = {"value1", 23, 5, "value1"}
5 | incorrect_set = {1, 1, 2}
| ^ B033
6 | incorrect_set_multiline = {
7 | "value1",
|
= help: Remove duplicate item
Safe fix
2 2 | # Errors.
3 3 | ###
4 4 | incorrect_set = {"value1", 23, 5, "value1"}
5 |-incorrect_set = {1, 1, 2}
5 |+incorrect_set = {1, 2}
6 6 | incorrect_set_multiline = {
7 7 | "value1",
8 8 | 23,
B033.py:10:5: B033 [*] Sets should not contain duplicate item `"value1"`
|
8 | 23,
9 | 5,
10 | "value1",
| ^^^^^^^^ B033
11 | # B033
12 | }
|
= help: Remove duplicate item
Safe fix
7 7 | "value1",
8 8 | 23,
9 9 | 5,
10 |- "value1",
11 10 | # B033
12 11 | }
13 12 | incorrect_set = {1, 1}
B033.py:13:21: B033 [*] Sets should not contain duplicate item `1`
|
11 | # B033
12 | }
13 | incorrect_set = {1, 1}
| ^ B033
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
|
= help: Remove duplicate item
Safe fix
10 10 | "value1",
11 11 | # B033
12 12 | }
13 |-incorrect_set = {1, 1}
13 |+incorrect_set = {1}
14 14 | incorrect_set = {1, 1,}
15 15 | incorrect_set = {0, 1, 1,}
16 16 | incorrect_set = {0, 1, 1}
B033.py:14:21: B033 [*] Sets should not contain duplicate item `1`
|
12 | }
13 | incorrect_set = {1, 1}
14 | incorrect_set = {1, 1,}
| ^ B033
15 | incorrect_set = {0, 1, 1,}
16 | incorrect_set = {0, 1, 1}
|
= help: Remove duplicate item
Safe fix
11 11 | # B033
12 12 | }
13 13 | incorrect_set = {1, 1}
14 |-incorrect_set = {1, 1,}
14 |+incorrect_set = {1,}
15 15 | incorrect_set = {0, 1, 1,}
16 16 | incorrect_set = {0, 1, 1}
17 17 | incorrect_set = {
B033.py:15:24: B033 [*] Sets should not contain duplicate item `1`
|
13 | incorrect_set = {1, 1}
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
| ^ B033
16 | incorrect_set = {0, 1, 1}
17 | incorrect_set = {
|
= help: Remove duplicate item
Safe fix
12 12 | }
13 13 | incorrect_set = {1, 1}
14 14 | incorrect_set = {1, 1,}
15 |-incorrect_set = {0, 1, 1,}
15 |+incorrect_set = {0, 1,}
16 16 | incorrect_set = {0, 1, 1}
17 17 | incorrect_set = {
18 18 | 0,
B033.py:16:24: B033 [*] Sets should not contain duplicate item `1`
|
14 | incorrect_set = {1, 1,}
15 | incorrect_set = {0, 1, 1,}
16 | incorrect_set = {0, 1, 1}
| ^ B033
17 | incorrect_set = {
18 | 0,
|
= help: Remove duplicate item
Safe fix
13 13 | incorrect_set = {1, 1}
14 14 | incorrect_set = {1, 1,}
15 15 | incorrect_set = {0, 1, 1,}
16 |-incorrect_set = {0, 1, 1}
16 |+incorrect_set = {0, 1}
17 17 | incorrect_set = {
18 18 | 0,
19 19 | 1,
B033.py:20:5: B033 [*] Sets should not contain duplicate item `1`
|
18 | 0,
19 | 1,
20 | 1,
| ^ B033
21 | }
|
= help: Remove duplicate item
Safe fix
17 17 | incorrect_set = {
18 18 | 0,
19 19 | 1,
20 |- 1,
21 20 | }
22 21 |
23 22 | ###

View File

@@ -1,8 +1,23 @@
//! Settings for the `flake8-builtins` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub builtins_ignorelist: Vec<String>,
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_builtins",
fields = [
self.builtins_ignorelist | array
]
}
Ok(())
}
}

View File

@@ -1,8 +1,23 @@
//! Settings for the `flake8-comprehensions` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub allow_dict_calls_with_keyword_arguments: bool,
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_comprehensions",
fields = [
self.allow_dict_calls_with_keyword_arguments
]
}
Ok(())
}
}

View File

@@ -2,7 +2,9 @@
use once_cell::sync::Lazy;
use regex::Regex;
use std::fmt::{Display, Formatter};
use crate::display_settings;
use ruff_macros::CacheKey;
#[derive(Debug, CacheKey)]
@@ -24,3 +26,18 @@ impl Default for Settings {
}
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_copyright",
fields = [
self.notice_rgx,
self.author | optional,
self.min_file_size,
]
}
Ok(())
}
}

View File

@@ -1,8 +1,23 @@
//! Settings for the `flake8-errmsg` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub max_string_length: usize,
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_errmsg",
fields = [
self.max_string_length
]
}
Ok(())
}
}

View File

@@ -1,4 +1,6 @@
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, CacheKey)]
pub struct Settings {
@@ -20,3 +22,16 @@ impl Default for Settings {
}
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_gettext",
fields = [
self.functions_names | array
]
}
Ok(())
}
}

View File

@@ -1,6 +1,8 @@
//! Settings for the `flake8-implicit-str-concat` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, CacheKey)]
pub struct Settings {
@@ -14,3 +16,16 @@ impl Default for Settings {
}
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_implicit_str_concat",
fields = [
self.allow_multiline
]
}
Ok(())
}
}

View File

@@ -1,7 +1,9 @@
//! Settings for import conventions.
use rustc_hash::{FxHashMap, FxHashSet};
use std::fmt::{Display, Formatter};
use crate::display_settings;
use ruff_macros::CacheKey;
const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[
@@ -44,3 +46,18 @@ impl Default for Settings {
}
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_import_conventions",
fields = [
self.aliases | debug,
self.banned_aliases | debug,
self.banned_from | array,
]
}
Ok(())
}
}

View File

@@ -1,7 +1,9 @@
//! Settings for the `flake8-pytest-style` plugin.
use std::error::Error;
use std::fmt;
use std::fmt::Formatter;
use crate::display_settings;
use ruff_macros::CacheKey;
use crate::settings::types::IdentifierPattern;
@@ -47,6 +49,25 @@ impl Default for Settings {
}
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_pytest_style",
fields = [
self.fixture_parentheses,
self.parametrize_names_type,
self.parametrize_values_type,
self.parametrize_values_row_type,
self.raises_require_match_for | array,
self.raises_extend_require_match_for | array,
self.mark_parentheses
]
}
Ok(())
}
}
/// Error returned by the [`TryFrom`] implementation of [`Settings`].
#[derive(Debug)]
pub enum SettingsError {

View File

@@ -1,7 +1,9 @@
//! Settings for the `flake8-quotes` plugin.
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use crate::display_settings;
use ruff_macros::CacheKey;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
@@ -39,6 +41,22 @@ impl Default for Settings {
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_quotes",
fields = [
self.inline_quotes,
self.multiline_quotes,
self.docstring_quotes,
self.avoid_escape
]
}
Ok(())
}
}
impl Quote {
#[must_use]
pub const fn opposite(self) -> Self {
@@ -56,3 +74,12 @@ impl Quote {
}
}
}
impl Display for Quote {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Double => write!(f, "double"),
Self::Single => write!(f, "single"),
}
}
}

View File

@@ -1,6 +1,8 @@
//! Settings for the `flake8-self` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
// By default, ignore the `namedtuple` methods and attributes, as well as the
// _sunder_ names in Enum, which are underscore-prefixed to prevent conflicts
@@ -27,3 +29,16 @@ impl Default for Settings {
}
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_self",
fields = [
self.ignore_names | array
]
}
Ok(())
}
}

View File

@@ -27,6 +27,7 @@ mod tests {
#[test_case(Rule::ReimplementedBuiltin, Path::new("SIM110.py"))]
#[test_case(Rule::ReimplementedBuiltin, Path::new("SIM111.py"))]
#[test_case(Rule::UncapitalizedEnvironmentVariables, Path::new("SIM112.py"))]
#[test_case(Rule::EnumerateForLoop, Path::new("SIM113.py"))]
#[test_case(Rule::OpenFileWithContextHandler, Path::new("SIM115.py"))]
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
#[test_case(Rule::InDictKeys, Path::new("SIM118.py"))]

View File

@@ -287,7 +287,7 @@ impl AlwaysFixableViolation for ExprAndFalse {
}
/// Return `true` if two `Expr` instances are equivalent names.
fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> {
pub(crate) fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> {
if let (Expr::Name(ast::ExprName { id: a, .. }), Expr::Name(ast::ExprName { id: b, .. })) =
(&a, &b)
{

View File

@@ -0,0 +1,192 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
use ruff_python_ast::{self as ast, Expr, Int, Number, Operator, Stmt};
use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `for` loops with explicit loop-index variables that can be replaced
/// with `enumerate()`.
///
/// ## Why is this bad?
/// When iterating over a sequence, it's often desirable to keep track of the
/// index of each element alongside the element itself. Prefer the `enumerate`
/// builtin over manually incrementing a counter variable within the loop, as
/// `enumerate` is more concise and idiomatic.
///
/// ## Example
/// ```python
/// fruits = ["apple", "banana", "cherry"]
/// for fruit in fruits:
/// print(f"{i + 1}. {fruit}")
/// i += 1
/// ```
///
/// Use instead:
/// ```python
/// fruits = ["apple", "banana", "cherry"]
/// for i, fruit in enumerate(fruits):
/// print(f"{i + 1}. {fruit}")
/// ```
///
/// ## References
/// - [Python documentation: `enumerate`](https://docs.python.org/3/library/functions.html#enumerate)
#[violation]
pub struct EnumerateForLoop {
index: String,
}
impl Violation for EnumerateForLoop {
#[derive_message_formats]
fn message(&self) -> String {
let EnumerateForLoop { index } = self;
format!("Use `enumerate()` for index variable `{index}` in `for` loop")
}
}
/// SIM113
pub(crate) fn enumerate_for_loop(checker: &mut Checker, for_stmt: &ast::StmtFor) {
// If the loop contains a `continue`, abort.
let mut visitor = LoopControlFlowVisitor::default();
visitor.visit_body(&for_stmt.body);
if visitor.has_continue {
return;
}
for stmt in &for_stmt.body {
// Find the augmented assignment expression (e.g., `i += 1`).
if let Some(index) = match_index_increment(stmt) {
// Find the binding corresponding to the initialization (e.g., `i = 1`).
let Some(id) = checker.semantic().resolve_name(index) else {
continue;
};
// If it's not an assignment (e.g., it's a function argument), ignore it.
let binding = checker.semantic().binding(id);
if !binding.kind.is_assignment() {
continue;
}
// If the variable is global or nonlocal, ignore it.
if binding.is_global() || binding.is_nonlocal() {
continue;
}
// Ensure that the index variable was initialized to 0.
let Some(value) = typing::find_binding_value(&index.id, binding, checker.semantic())
else {
continue;
};
if !matches!(
value,
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: Number::Int(Int::ZERO),
..
})
) {
continue;
}
// If the binding is not at the same level as the `for` loop (e.g., it's in an `if`),
// ignore it.
let Some(for_loop_id) = checker.semantic().current_statement_id() else {
continue;
};
let Some(assignment_id) = binding.source else {
continue;
};
if checker.semantic().parent_statement_id(for_loop_id)
!= checker.semantic().parent_statement_id(assignment_id)
{
continue;
}
// Identify the binding created by the augmented assignment.
// TODO(charlie): There should be a way to go from `ExprName` to `BindingId` (like
// `resolve_name`, but for bindings rather than references).
let binding = {
let mut bindings = checker
.semantic()
.current_scope()
.get_all(&index.id)
.map(|id| checker.semantic().binding(id))
.filter(|binding| for_stmt.range().contains_range(binding.range()));
let Some(binding) = bindings.next() else {
continue;
};
// If there are multiple assignments to this variable _within_ the loop, ignore it.
if bindings.next().is_some() {
continue;
}
binding
};
// If the variable is used outside the loop, ignore it.
if binding.references.iter().any(|id| {
let reference = checker.semantic().reference(*id);
!for_stmt.range().contains_range(reference.range())
}) {
continue;
}
let diagnostic = Diagnostic::new(
EnumerateForLoop {
index: index.id.to_string(),
},
stmt.range(),
);
checker.diagnostics.push(diagnostic);
}
}
}
/// If the statement is an index increment statement (e.g., `i += 1`), return
/// the name of the index variable.
fn match_index_increment(stmt: &Stmt) -> Option<&ast::ExprName> {
let Stmt::AugAssign(ast::StmtAugAssign {
target,
op: Operator::Add,
value,
..
}) = stmt
else {
return None;
};
let name = target.as_name_expr()?;
if matches!(
value.as_ref(),
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: Number::Int(Int::ONE),
..
})
) {
return Some(name);
}
None
}
#[derive(Debug, Default)]
struct LoopControlFlowVisitor {
has_continue: bool,
}
impl StatementVisitor<'_> for LoopControlFlowVisitor {
fn visit_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Continue(_) => self.has_continue = true,
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
// Don't recurse.
}
_ => walk_stmt(self, stmt),
}
}
}

View File

@@ -4,6 +4,7 @@ pub(crate) use ast_ifexp::*;
pub(crate) use ast_unary_op::*;
pub(crate) use ast_with::*;
pub(crate) use collapsible_if::*;
pub(crate) use enumerate_for_loop::*;
pub(crate) use if_else_block_instead_of_dict_get::*;
pub(crate) use if_else_block_instead_of_dict_lookup::*;
pub(crate) use if_else_block_instead_of_if_exp::*;
@@ -23,6 +24,7 @@ mod ast_ifexp;
mod ast_unary_op;
mod ast_with;
mod collapsible_if;
mod enumerate_for_loop;
mod fix_with;
mod if_else_block_instead_of_dict_get;
mod if_else_block_instead_of_dict_lookup;

View File

@@ -0,0 +1,47 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM113.py:6:9: SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
4 | for x in range(5):
5 | g(x, idx)
6 | idx += 1
| ^^^^^^^^ SIM113
7 | h(x)
|
SIM113.py:17:9: SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
15 | if g(x):
16 | break
17 | idx += 1
| ^^^^^^^^ SIM113
18 | sum += h(x, idx)
|
SIM113.py:27:9: SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
25 | g(x)
26 | h(x, y)
27 | idx += 1
| ^^^^^^^^ SIM113
|
SIM113.py:36:9: SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
34 | for x in range(5):
35 | sum += h(x, idx)
36 | idx += 1
| ^^^^^^^^ SIM113
|
SIM113.py:44:9: SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
42 | for x in range(5):
43 | g(x, idx)
44 | idx += 1
| ^^^^^^^^ SIM113
45 | h(x)
|

View File

@@ -1,6 +1,8 @@
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use crate::display_settings;
use ruff_macros::CacheKey;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
@@ -22,9 +24,33 @@ pub enum Strictness {
All,
}
impl Display for Strictness {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parents => write!(f, "\"parents\""),
Self::All => write!(f, "\"all\""),
}
}
}
#[derive(Debug, CacheKey, Default)]
pub struct Settings {
pub ban_relative_imports: Strictness,
pub banned_api: FxHashMap<String, ApiBan>,
pub banned_module_level_imports: Vec<String>,
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_tidy_imports",
fields = [
self.ban_relative_imports,
self.banned_api | debug,
self.banned_module_level_imports | array,
]
}
Ok(())
}
}

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprCall, Int};
use ruff_python_ast::{self as ast, Expr, ExprCall, Int, Number};
use ruff_python_semantic::analyze::typing::find_assigned_value;
use ruff_text_size::Ranged;
@@ -65,8 +65,7 @@ pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
match arg {
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
let Some(int) = value.as_int() else { return };
if *int != Int::ZERO {
if !matches!(value, Number::Int(Int::ZERO)) {
return;
}
}
@@ -74,11 +73,13 @@ pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
let Some(value) = find_assigned_value(id, checker.semantic()) else {
return;
};
let Expr::NumberLiteral(ast::ExprNumberLiteral { value: num, .. }) = value else {
return;
};
let Some(int) = num.as_int() else { return };
if *int != Int::ZERO {
if !matches!(
value,
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: Number::Int(Int::ZERO),
..
})
) {
return;
}
}

View File

@@ -1,6 +1,8 @@
//! Settings for the `flake8-type-checking` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, CacheKey)]
pub struct Settings {
@@ -22,3 +24,20 @@ impl Default for Settings {
}
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_type_checking",
fields = [
self.strict,
self.exempt_modules | array,
self.runtime_required_base_classes | array,
self.runtime_required_decorators | array,
self.quote_annotations
]
}
Ok(())
}
}

View File

@@ -1,8 +1,23 @@
//! Settings for the `flake8-unused-arguments` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub ignore_variadic_names: bool,
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.flake8_unused_arguments",
fields = [
self.ignore_variadic_names
]
}
Ok(())
}
}

View File

@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::fmt;
use std::hash::BuildHasherDefault;
use std::path::{Path, PathBuf};
use std::{fs, iter};
@@ -40,6 +41,18 @@ pub enum ImportType {
LocalFolder,
}
impl fmt::Display for ImportType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Future => write!(f, "future"),
Self::StandardLibrary => write!(f, "standard_library"),
Self::ThirdParty => write!(f, "third_party"),
Self::FirstParty => write!(f, "first_party"),
Self::LocalFolder => write!(f, "local_folder"),
}
}
}
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Hash, Serialize, Deserialize, CacheKey)]
#[serde(untagged)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@@ -48,6 +61,15 @@ pub enum ImportSection {
UserDefined(String),
}
impl fmt::Display for ImportSection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Known(import_type) => write!(f, "known {{ type = {import_type} }}",),
Self::UserDefined(string) => fmt::Debug::fmt(string, f),
}
}
}
#[derive(Debug)]
enum Reason<'a> {
NonZeroLevel,
@@ -378,3 +400,18 @@ impl KnownModules {
user_defined
}
}
impl fmt::Display for KnownModules {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.known.is_empty() {
write!(f, "{{}}")?;
} else {
writeln!(f, "{{")?;
for (pattern, import_section) in &self.known {
writeln!(f, "\t{pattern} => {import_section:?},")?;
}
write!(f, "}}")?;
}
Ok(())
}
}

View File

@@ -3,10 +3,12 @@
use std::collections::BTreeSet;
use std::error::Error;
use std::fmt;
use std::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use crate::display_settings;
use ruff_macros::CacheKey;
use crate::rules::isort::categorize::KnownModules;
@@ -32,6 +34,15 @@ impl Default for RelativeImportsOrder {
}
}
impl Display for RelativeImportsOrder {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::ClosestToFurthest => write!(f, "closest_to_furthest"),
Self::FurthestToClosest => write!(f, "furthest_to_closest"),
}
}
}
#[derive(Debug, CacheKey)]
#[allow(clippy::struct_excessive_bools)]
pub struct Settings {
@@ -94,6 +105,43 @@ impl Default for Settings {
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.isort",
fields = [
self.required_imports | array,
self.combine_as_imports,
self.force_single_line,
self.force_sort_within_sections,
self.detect_same_package,
self.case_sensitive,
self.force_wrap_aliases,
self.force_to_top | array,
self.known_modules,
self.order_by_type,
self.relative_imports_order,
self.single_line_exclusions | array,
self.split_on_trailing_comma,
self.classes | array,
self.constants | array,
self.variables | array,
self.no_lines_before | array,
self.lines_after_imports,
self.lines_between_types,
self.forced_separate | array,
self.section_order | array,
self.no_sections,
self.from_first,
self.length_sort,
self.length_sort_straight
]
}
Ok(())
}
}
/// Error returned by the [`TryFrom`] implementation of [`Settings`].
#[derive(Debug)]
pub enum SettingsError {

View File

@@ -1,6 +1,8 @@
//! Settings for the `mccabe` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt::{Display, Formatter};
#[derive(Debug, CacheKey)]
pub struct Settings {
@@ -16,3 +18,16 @@ impl Default for Settings {
}
}
}
impl Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.mccabe",
fields = [
self.max_complexity
]
}
Ok(())
}
}

View File

@@ -61,6 +61,15 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) {
return;
}
// If the function doesn't take an `inplace` argument, abort.
if !call
.func
.as_attribute_expr()
.is_some_and(|func| accepts_inplace_argument(&func.attr))
{
return;
}
let mut seen_star = false;
for keyword in call.arguments.keywords.iter().rev() {
let Some(arg) = &keyword.arg else {
@@ -134,3 +143,35 @@ fn convert_inplace_argument_to_assignment(
Some(Fix::unsafe_edits(insert_assignment, [remove_argument]))
}
/// Returns `true` if the given method accepts an `inplace` argument when used on a Pandas
/// `DataFrame`, `Series`, or `Index`.
///
/// See: <https://pandas.pydata.org/docs/reference/frame.html>
fn accepts_inplace_argument(method: &str) -> bool {
matches!(
method,
"where"
| "mask"
| "query"
| "clip"
| "eval"
| "backfill"
| "bfill"
| "ffill"
| "fillna"
| "interpolate"
| "dropna"
| "pad"
| "replace"
| "drop"
| "drop_duplicates"
| "rename"
| "rename_axis"
| "reset_index"
| "set_index"
| "sort_values"
| "sort_index"
| "set_names"
)
}

View File

@@ -164,6 +164,8 @@ PD002.py:33:24: PD002 [*] `inplace=True` should be avoided; it has inconsistent
32 |
33 | (x.drop(["a"], axis=1, inplace=True))
| ^^^^^^^^^^^^ PD002
34 |
35 | # This method doesn't take exist in Pandas, so ignore it.
|
= help: Assign to variable; remove `inplace` arg
@@ -173,5 +175,8 @@ PD002.py:33:24: PD002 [*] `inplace=True` should be avoided; it has inconsistent
32 32 |
33 |-(x.drop(["a"], axis=1, inplace=True))
33 |+x = (x.drop(["a"], axis=1))
34 34 |
35 35 | # This method doesn't take exist in Pandas, so ignore it.
36 36 | x.rotate_z(45, inplace=True)

View File

@@ -2,7 +2,9 @@
use std::error::Error;
use std::fmt;
use std::fmt::Formatter;
use crate::display_settings;
use ruff_macros::CacheKey;
use crate::settings::types::IdentifierPattern;
@@ -44,6 +46,21 @@ impl Default for Settings {
}
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.pep8_naming",
fields = [
self.ignore_names | array,
self.classmethod_decorators | array,
self.staticmethod_decorators | array
]
}
Ok(())
}
}
/// Error returned by the [`TryFrom`] implementation of [`Settings`].
#[derive(Debug)]
pub enum SettingsError {

View File

@@ -1,6 +1,8 @@
//! Settings for the `pycodestyle` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt;
use crate::line_width::LineLength;
@@ -10,3 +12,18 @@ pub struct Settings {
pub max_doc_length: Option<LineLength>,
pub ignore_overlong_task_comments: bool,
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.pycodestyle",
fields = [
self.max_line_length,
self.max_doc_length | optional,
self.ignore_overlong_task_comments,
]
}
Ok(())
}
}

View File

@@ -1,9 +1,11 @@
//! Settings for the `pydocstyle` plugin.
use std::collections::BTreeSet;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::display_settings;
use ruff_macros::CacheKey;
use crate::registry::Rule;
@@ -71,9 +73,34 @@ impl Convention {
}
}
impl fmt::Display for Convention {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Google => write!(f, "google"),
Self::Numpy => write!(f, "numpy"),
Self::Pep257 => write!(f, "pep257"),
}
}
}
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub convention: Option<Convention>,
pub ignore_decorators: BTreeSet<String>,
pub property_decorators: BTreeSet<String>,
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.pydocstyle",
fields = [
self.convention | optional,
self.ignore_decorators | debug,
self.property_decorators | debug
]
}
Ok(())
}
}

View File

@@ -1,8 +1,23 @@
//! Settings for the `Pyflakes` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt;
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub extend_generics: Vec<String>,
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.pyflakes",
fields = [
self.extend_generics | debug
]
}
Ok(())
}
}

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -80,11 +80,17 @@ pub(crate) fn unnecessary_dunder_call(checker: &mut Checker, call: &ast::ExprCal
return;
}
// Ignore certain dunder methods used in lambda expressions.
// Ignore certain dunder method calls in lambda expressions. These methods would require
// rewriting as a statement, which is not possible in a lambda expression.
if allow_nested_expression(attr, checker.semantic()) {
return;
}
// Ignore dunder method calls within dunder methods definitions.
if in_dunder_method_definition(checker.semantic()) {
return;
}
// Ignore dunder methods used on `super`.
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
if checker.semantic().is_builtin("super") {
@@ -344,3 +350,13 @@ fn allow_nested_expression(dunder_name: &str, semantic: &SemanticModel) -> bool
| "__ior__"
)
}
/// Returns `true` if the [`SemanticModel`] is currently in a dunder method definition.
fn in_dunder_method_definition(semantic: &SemanticModel) -> bool {
semantic.current_statements().any(|statement| {
let Stmt::FunctionDef(func_def) = statement else {
return false;
};
func_def.name.starts_with("__") && func_def.name.ends_with("__")
})
}

View File

@@ -2,7 +2,9 @@
use rustc_hash::FxHashSet;
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::display_settings;
use ruff_macros::CacheKey;
use ruff_python_ast::{ExprNumberLiteral, LiteralExpressionRef, Number};
@@ -34,6 +36,18 @@ impl ConstantType {
}
}
impl fmt::Display for ConstantType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bytes => write!(f, "bytes"),
Self::Complex => write!(f, "complex"),
Self::Float => write!(f, "float"),
Self::Int => write!(f, "int"),
Self::Str => write!(f, "str"),
}
}
}
#[derive(Debug, CacheKey)]
pub struct Settings {
pub allow_magic_value_types: Vec<ConstantType>,
@@ -64,3 +78,25 @@ impl Default for Settings {
}
}
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.pylint",
fields = [
self.allow_magic_value_types | array,
self.allow_dunder_method_names | array,
self.max_args,
self.max_positional_args,
self.max_returns,
self.max_bool_expr,
self.max_branches,
self.max_statements,
self.max_public_methods,
self.max_locals
]
}
Ok(())
}
}

View File

@@ -321,4 +321,12 @@ unnecessary_dunder_call.py:19:7: PLC2801 Unnecessary dunder call to `__neg__`. M
|
= help: Multiply by -1 instead
unnecessary_dunder_call.py:31:16: PLC2801 Unnecessary dunder call to `__getattribute__`. Access attribute directly or use getattr built-in function.
|
30 | def do_thing(self, item):
31 | return object.__getattribute__(self, item) # PLC2801
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC2801
|
= help: Access attribute directly or use getattr built-in function

View File

@@ -1,8 +1,23 @@
//! Settings for the `pyupgrade` plugin.
use crate::display_settings;
use ruff_macros::CacheKey;
use std::fmt;
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub keep_runtime_typing: bool,
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.pyupgrade",
fields = [
self.keep_runtime_typing
]
}
Ok(())
}
}

View File

@@ -16,8 +16,7 @@ mod tests {
use crate::pyproject_toml::lint_pyproject_toml;
use crate::registry::Rule;
use crate::settings::resolve_per_file_ignores;
use crate::settings::types::{PerFileIgnore, PreviewMode, PythonVersion};
use crate::settings::types::{PerFileIgnore, PerFileIgnores, PreviewMode, PythonVersion};
use crate::test::{test_path, test_resource_path};
use crate::{assert_messages, settings};
@@ -171,7 +170,7 @@ mod tests {
let mut settings =
settings::LinterSettings::for_rules(vec![Rule::UnusedNOQA, Rule::UnusedImport]);
settings.per_file_ignores = resolve_per_file_ignores(vec![PerFileIgnore::new(
settings.per_file_ignores = PerFileIgnores::resolve(vec![PerFileIgnore::new(
"RUF100_2.py".to_string(),
&["F401".parse().unwrap()],
None,
@@ -228,7 +227,7 @@ mod tests {
let diagnostics = test_path(
Path::new("ruff/ruff_per_file_ignores.py"),
&settings::LinterSettings {
per_file_ignores: resolve_per_file_ignores(vec![PerFileIgnore::new(
per_file_ignores: PerFileIgnores::resolve(vec![PerFileIgnore::new(
"ruff_per_file_ignores.py".to_string(),
&["F401".parse().unwrap(), "RUF100".parse().unwrap()],
None,

View File

@@ -2,7 +2,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::NameFinder;
use ruff_python_ast::helpers::StoredNameFinder;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
@@ -51,9 +51,9 @@ impl Violation for StaticKeyDictComprehension {
pub(crate) fn static_key_dict_comprehension(checker: &mut Checker, dict_comp: &ast::ExprDictComp) {
// Collect the bound names in the comprehension's generators.
let names = {
let mut visitor = NameFinder::default();
let mut visitor = StoredNameFinder::default();
for generator in &dict_comp.generators {
visitor.visit_expr(&generator.target);
visitor.visit_comprehension(generator);
}
visitor.names
};

View File

@@ -1,80 +1,90 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF011.py:15:2: RUF011 Dictionary comprehension uses static key: `"key"`
RUF011.py:17:2: RUF011 Dictionary comprehension uses static key: `"key"`
|
14 | # Errors
15 | {"key": value.upper() for value in data}
16 | # Errors
17 | {"key": value.upper() for value in data}
| ^^^^^ RUF011
16 | {True: value.upper() for value in data}
17 | {0: value.upper() for value in data}
18 | {True: value.upper() for value in data}
19 | {0: value.upper() for value in data}
|
RUF011.py:16:2: RUF011 Dictionary comprehension uses static key: `True`
RUF011.py:18:2: RUF011 Dictionary comprehension uses static key: `True`
|
14 | # Errors
15 | {"key": value.upper() for value in data}
16 | {True: value.upper() for value in data}
16 | # Errors
17 | {"key": value.upper() for value in data}
18 | {True: value.upper() for value in data}
| ^^^^ RUF011
17 | {0: value.upper() for value in data}
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
19 | {0: value.upper() for value in data}
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
|
RUF011.py:17:2: RUF011 Dictionary comprehension uses static key: `0`
RUF011.py:19:2: RUF011 Dictionary comprehension uses static key: `0`
|
15 | {"key": value.upper() for value in data}
16 | {True: value.upper() for value in data}
17 | {0: value.upper() for value in data}
17 | {"key": value.upper() for value in data}
18 | {True: value.upper() for value in data}
19 | {0: value.upper() for value in data}
| ^ RUF011
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
19 | {constant: value.upper() for value in data}
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
21 | {constant: value.upper() for value in data}
|
RUF011.py:18:2: RUF011 Dictionary comprehension uses static key: `(1, "a")`
RUF011.py:20:2: RUF011 Dictionary comprehension uses static key: `(1, "a")`
|
16 | {True: value.upper() for value in data}
17 | {0: value.upper() for value in data}
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
18 | {True: value.upper() for value in data}
19 | {0: value.upper() for value in data}
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
| ^^^^^^^^ RUF011
19 | {constant: value.upper() for value in data}
20 | {constant + constant: value.upper() for value in data}
21 | {constant: value.upper() for value in data}
22 | {constant + constant: value.upper() for value in data}
|
RUF011.py:19:2: RUF011 Dictionary comprehension uses static key: `constant`
RUF011.py:21:2: RUF011 Dictionary comprehension uses static key: `constant`
|
17 | {0: value.upper() for value in data}
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
19 | {constant: value.upper() for value in data}
19 | {0: value.upper() for value in data}
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
21 | {constant: value.upper() for value in data}
| ^^^^^^^^ RUF011
20 | {constant + constant: value.upper() for value in data}
21 | {constant.attribute: value.upper() for value in data}
22 | {constant + constant: value.upper() for value in data}
23 | {constant.attribute: value.upper() for value in data}
|
RUF011.py:20:2: RUF011 Dictionary comprehension uses static key: `constant + constant`
RUF011.py:22:2: RUF011 Dictionary comprehension uses static key: `constant + constant`
|
18 | {(1, "a"): value.upper() for value in data} # Constant tuple
19 | {constant: value.upper() for value in data}
20 | {constant + constant: value.upper() for value in data}
20 | {(1, "a"): value.upper() for value in data} # Constant tuple
21 | {constant: value.upper() for value in data}
22 | {constant + constant: value.upper() for value in data}
| ^^^^^^^^^^^^^^^^^^^ RUF011
21 | {constant.attribute: value.upper() for value in data}
22 | {constant[0]: value.upper() for value in data}
23 | {constant.attribute: value.upper() for value in data}
24 | {constant[0]: value.upper() for value in data}
|
RUF011.py:21:2: RUF011 Dictionary comprehension uses static key: `constant.attribute`
RUF011.py:23:2: RUF011 Dictionary comprehension uses static key: `constant.attribute`
|
19 | {constant: value.upper() for value in data}
20 | {constant + constant: value.upper() for value in data}
21 | {constant.attribute: value.upper() for value in data}
21 | {constant: value.upper() for value in data}
22 | {constant + constant: value.upper() for value in data}
23 | {constant.attribute: value.upper() for value in data}
| ^^^^^^^^^^^^^^^^^^ RUF011
22 | {constant[0]: value.upper() for value in data}
24 | {constant[0]: value.upper() for value in data}
25 | {tokens: token for token in tokens}
|
RUF011.py:22:2: RUF011 Dictionary comprehension uses static key: `constant[0]`
RUF011.py:24:2: RUF011 Dictionary comprehension uses static key: `constant[0]`
|
20 | {constant + constant: value.upper() for value in data}
21 | {constant.attribute: value.upper() for value in data}
22 | {constant[0]: value.upper() for value in data}
22 | {constant + constant: value.upper() for value in data}
23 | {constant.attribute: value.upper() for value in data}
24 | {constant[0]: value.upper() for value in data}
| ^^^^^^^^^^^ RUF011
25 | {tokens: token for token in tokens}
|
RUF011.py:25:2: RUF011 Dictionary comprehension uses static key: `tokens`
|
23 | {constant.attribute: value.upper() for value in data}
24 | {constant[0]: value.upper() for value in data}
25 | {tokens: token for token in tokens}
| ^^^^^^ RUF011
|

View File

@@ -1,4 +1,4 @@
use std::fmt::Debug;
use std::fmt::{Debug, Display, Formatter};
use ruff_diagnostics::Applicability;
use ruff_macros::CacheKey;
@@ -6,6 +6,7 @@ use rustc_hash::FxHashMap;
use strum::IntoEnumIterator;
use crate::{
display_settings,
registry::{Rule, RuleSet},
rule_selector::{PreviewOptions, Specificity},
RuleSelector,
@@ -95,6 +96,20 @@ impl FixSafetyTable {
}
}
impl Display for FixSafetyTable {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.safety_table",
fields = [
self.forced_safe,
self.forced_unsafe
]
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -2,10 +2,9 @@
//! command-line options. Structure is optimized for internal usage, as opposed
//! to external visibility or parsing.
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use anyhow::Result;
use globset::{Glob, GlobMatcher};
use once_cell::sync::Lazy;
use path_absolutize::path_dedot;
use regex::Regex;
@@ -15,7 +14,7 @@ use crate::codes::RuleCodePrefix;
use ruff_macros::CacheKey;
use crate::line_width::LineLength;
use crate::registry::{Linter, Rule, RuleSet};
use crate::registry::{Linter, Rule};
use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat,
@@ -23,7 +22,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::{ExtensionMapping, FilePatternSet, PerFileIgnore, PythonVersion};
use crate::settings::types::{ExtensionMapping, FilePatternSet, PerFileIgnores, PythonVersion};
use crate::{codes, RuleSelector};
use super::line_width::IndentWidth;
@@ -38,19 +37,139 @@ pub mod flags;
pub mod rule_table;
pub mod types;
/// `display_settings!` is a macro that can display and format struct fields in a readable,
/// namespaced format. It's particularly useful at generating `Display` implementations
/// for types used in settings.
///
/// # Example
/// ```
/// use std::fmt;
/// use ruff_linter::display_settings;
/// #[derive(Default)]
/// struct Settings {
/// option_a: bool,
/// sub_settings: SubSettings,
/// option_b: String,
/// }
///
/// struct SubSettings {
/// name: String
/// }
///
/// impl Default for SubSettings {
/// fn default() -> Self {
/// Self { name: "Default Name".into() }
/// }
///
/// }
///
/// impl fmt::Display for SubSettings {
/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// display_settings! {
/// formatter = f,
/// namespace = "sub_settings",
/// fields = [
/// self.name | quoted
/// ]
/// }
/// Ok(())
/// }
///
/// }
///
/// impl fmt::Display for Settings {
/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// display_settings! {
/// formatter = f,
/// fields = [
/// self.option_a,
/// self.sub_settings | nested,
/// self.option_b | quoted,
/// ]
/// }
/// Ok(())
/// }
///
/// }
///
/// const EXPECTED_OUTPUT: &str = r#"option_a = false
/// sub_settings.name = "Default Name"
/// option_b = ""
/// "#;
///
/// fn main() {
/// let settings = Settings::default();
/// assert_eq!(format!("{settings}"), EXPECTED_OUTPUT);
/// }
/// ```
#[macro_export]
macro_rules! display_settings {
(formatter = $fmt:ident, namespace = $namespace:literal, fields = [$($settings:ident.$field:ident $(| $modifier:tt)?),* $(,)?]) => {
{
const _PREFIX: &str = concat!($namespace, ".");
$(
display_settings!(@field $fmt, _PREFIX, $settings.$field $(| $modifier)?);
)*
}
};
(formatter = $fmt:ident, fields = [$($settings:ident.$field:ident $(| $modifier:tt)?),* $(,)?]) => {
{
const _PREFIX: &str = "";
$(
display_settings!(@field $fmt, _PREFIX, $settings.$field $(| $modifier)?);
)*
}
};
(@field $fmt:ident, $prefix:ident, $settings:ident.$field:ident | debug) => {
writeln!($fmt, "{}{} = {:?}", $prefix, stringify!($field), $settings.$field)?;
};
(@field $fmt:ident, $prefix:ident, $settings:ident.$field:ident | quoted) => {
writeln!($fmt, "{}{} = \"{}\"", $prefix, stringify!($field), $settings.$field)?;
};
(@field $fmt:ident, $prefix:ident, $settings:ident.$field:ident | nested) => {
write!($fmt, "{}", $settings.$field)?;
};
(@field $fmt:ident, $prefix:ident, $settings:ident.$field:ident | optional) => {
{
write!($fmt, "{}{} = ", $prefix, stringify!($field))?;
match &$settings.$field {
Some(value) => writeln!($fmt, "{}", value)?,
None => writeln!($fmt, "none")?
};
}
};
(@field $fmt:ident, $prefix:ident, $settings:ident.$field:ident | array) => {
{
write!($fmt, "{}{} = ", $prefix, stringify!($field))?;
if $settings.$field.is_empty() {
writeln!($fmt, "[]")?;
} else {
writeln!($fmt, "[")?;
for elem in &$settings.$field {
writeln!($fmt, "\t{elem},")?;
}
writeln!($fmt, "]")?;
}
}
};
(@field $fmt:ident, $prefix:ident, $settings:ident.$field:ident) => {
writeln!($fmt, "{}{} = {}", $prefix, stringify!($field), $settings.$field)?;
};
}
#[derive(Debug, CacheKey)]
pub struct LinterSettings {
pub exclude: FilePatternSet,
pub extension: ExtensionMapping,
pub project_root: PathBuf,
pub rules: RuleTable,
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, RuleSet)>,
pub per_file_ignores: PerFileIgnores,
pub fix_safety: FixSafetyTable,
pub target_version: PythonVersion,
pub preview: PreviewMode,
pub explicit_preview_rules: bool,
pub extension: ExtensionMapping,
// Rule-specific settings
pub allowed_confusables: FxHashSet<char>,
@@ -93,6 +212,73 @@ pub struct LinterSettings {
pub pyupgrade: pyupgrade::settings::Settings,
}
impl Display for LinterSettings {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "\n# Linter Settings")?;
display_settings! {
formatter = f,
namespace = "linter",
fields = [
self.exclude,
self.project_root | debug,
self.rules | nested,
self.per_file_ignores,
self.fix_safety | nested,
self.target_version | debug,
self.preview,
self.explicit_preview_rules,
self.extension | nested,
self.allowed_confusables | array,
self.builtins | array,
self.dummy_variable_rgx,
self.external | array,
self.ignore_init_module_imports,
self.logger_objects | array,
self.namespace_packages | debug,
self.src | debug,
self.tab_size,
self.line_length,
self.task_tags | array,
self.typing_modules | array,
]
}
writeln!(f, "\n# Linter Plugins")?;
display_settings! {
formatter = f,
namespace = "linter",
fields = [
self.flake8_annotations | nested,
self.flake8_bandit | nested,
self.flake8_bugbear | nested,
self.flake8_builtins | nested,
self.flake8_comprehensions | nested,
self.flake8_copyright | nested,
self.flake8_errmsg | nested,
self.flake8_gettext | nested,
self.flake8_implicit_str_concat | nested,
self.flake8_import_conventions | nested,
self.flake8_pytest_style | nested,
self.flake8_quotes | nested,
self.flake8_self | nested,
self.flake8_tidy_imports | nested,
self.flake8_type_checking | nested,
self.flake8_unused_arguments | nested,
self.isort | nested,
self.mccabe | nested,
self.pep8_naming | nested,
self.pycodestyle | nested,
self.pyflakes | nested,
self.pylint | nested,
self.pyupgrade | nested,
]
}
Ok(())
}
}
pub const DEFAULT_SELECTORS: &[RuleSelector] = &[
RuleSelector::Linter(Linter::Pyflakes),
// Only include pycodestyle rules that do not overlap with the formatter
@@ -152,7 +338,7 @@ impl LinterSettings {
logger_objects: vec![],
namespace_packages: vec![],
per_file_ignores: vec![],
per_file_ignores: PerFileIgnores::default(),
fix_safety: FixSafetyTable::default(),
src: vec![path_dedot::CWD.clone()],
@@ -204,22 +390,3 @@ impl Default for LinterSettings {
Self::new(path_dedot::CWD.as_path())
}
}
/// Given a list of patterns, create a `GlobSet`.
pub fn resolve_per_file_ignores(
per_file_ignores: Vec<PerFileIgnore>,
) -> Result<Vec<(GlobMatcher, GlobMatcher, RuleSet)>> {
per_file_ignores
.into_iter()
.map(|per_file_ignore| {
// Construct absolute path matcher.
let absolute =
Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher();
// Construct basename matcher.
let basename = Glob::new(&per_file_ignore.basename)?.compile_matcher();
Ok((absolute, basename, per_file_ignore.rules))
})
.collect()
}

View File

@@ -1,5 +1,6 @@
use std::fmt::Debug;
use std::fmt::{Debug, Display, Formatter};
use crate::display_settings;
use ruff_macros::CacheKey;
use crate::registry::{Rule, RuleSet, RuleSetIterator};
@@ -62,6 +63,20 @@ impl RuleTable {
}
}
impl Display for RuleTable {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.rules",
fields = [
self.enabled,
self.should_fix
]
}
Ok(())
}
}
impl FromIterator<Rule> for RuleTable {
fn from_iter<T: IntoIterator<Item = Rule>>(iter: T) -> Self {
let rules = RuleSet::from_iter(iter);

View File

@@ -1,3 +1,4 @@
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::path::{Path, PathBuf};
@@ -5,7 +6,7 @@ use std::str::FromStr;
use std::string::ToString;
use anyhow::{bail, Result};
use globset::{Glob, GlobSet, GlobSetBuilder};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use pep440_rs::{Version as Pep440Version, VersionSpecifiers};
use rustc_hash::FxHashMap;
use serde::{de, Deserialize, Deserializer, Serialize};
@@ -17,9 +18,9 @@ use ruff_diagnostics::Applicability;
use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType;
use crate::fs;
use crate::registry::RuleSet;
use crate::rule_selector::RuleSelector;
use crate::{display_settings, fs};
#[derive(
Clone,
@@ -121,6 +122,15 @@ impl From<bool> for PreviewMode {
}
}
impl Display for PreviewMode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Disabled => write!(f, "disabled"),
Self::Enabled => write!(f, "enabled"),
}
}
}
/// Toggle for unsafe fixes.
/// `Hint` will not apply unsafe fixes but a message will be shown when they are available.
/// `Disabled` will not apply unsafe fixes or show a message.
@@ -133,6 +143,20 @@ pub enum UnsafeFixes {
Enabled,
}
impl Display for UnsafeFixes {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Hint => "hint",
Self::Disabled => "disabled",
Self::Enabled => "enabled",
}
)
}
}
impl From<bool> for UnsafeFixes {
fn from(value: bool) -> Self {
if value {
@@ -178,6 +202,19 @@ impl FilePattern {
}
}
impl Display for FilePattern {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:?}",
match self {
Self::Builtin(pattern) => pattern,
Self::User(pattern, _) => pattern.as_str(),
}
)
}
}
impl FromStr for FilePattern {
type Err = anyhow::Error;
@@ -192,9 +229,14 @@ impl FromStr for FilePattern {
pub struct FilePatternSet {
set: GlobSet,
cache_key: u64,
// This field is only for displaying the internals
// of `set`.
#[allow(clippy::used_underscore_binding)]
_set_internals: Vec<FilePattern>,
}
impl FilePatternSet {
#[allow(clippy::used_underscore_binding)]
pub fn try_from_iter<I>(patterns: I) -> Result<Self, anyhow::Error>
where
I: IntoIterator<Item = FilePattern>,
@@ -202,7 +244,10 @@ impl FilePatternSet {
let mut builder = GlobSetBuilder::new();
let mut hasher = CacheKeyHasher::new();
let mut _set_internals = vec![];
for pattern in patterns {
_set_internals.push(pattern.clone());
pattern.cache_key(&mut hasher);
pattern.add_to(&mut builder)?;
}
@@ -212,10 +257,26 @@ impl FilePatternSet {
Ok(FilePatternSet {
set,
cache_key: hasher.finish(),
_set_internals,
})
}
}
impl Display for FilePatternSet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self._set_internals.is_empty() {
write!(f, "[]")?;
} else {
writeln!(f, "[")?;
for pattern in &self._set_internals {
writeln!(f, "\t{pattern},")?;
}
write!(f, "]")?;
}
Ok(())
}
}
impl Deref for FilePatternSet {
type Target = GlobSet;
@@ -388,9 +449,23 @@ pub struct ExtensionMapping {
}
impl ExtensionMapping {
/// Return the [`Language`] for the given extension.
pub fn get(&self, extension: &str) -> Option<Language> {
self.mapping.get(extension).copied()
/// Return the [`Language`] for the given file.
pub fn get(&self, path: &Path) -> Option<Language> {
let ext = path.extension()?.to_str()?;
self.mapping.get(ext).copied()
}
}
impl Display for ExtensionMapping {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
namespace = "linter.extension",
fields = [
self.mapping | debug
]
}
Ok(())
}
}
@@ -428,6 +503,23 @@ pub enum SerializationFormat {
Sarif,
}
impl Display for SerializationFormat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text => write!(f, "text"),
Self::Json => write!(f, "json"),
Self::JsonLines => write!(f, "json_lines"),
Self::Junit => write!(f, "junit"),
Self::Grouped => write!(f, "grouped"),
Self::Github => write!(f, "github"),
Self::Gitlab => write!(f, "gitlab"),
Self::Pylint => write!(f, "pylint"),
Self::Azure => write!(f, "azure"),
Self::Sarif => write!(f, "sarif"),
}
}
}
impl Default for SerializationFormat {
fn default() -> Self {
Self::Text
@@ -467,3 +559,55 @@ impl Deref for Version {
/// [`fnmatch`](https://docs.python.org/3/library/fnmatch.html) for
/// pattern matching.
pub type IdentifierPattern = glob::Pattern;
#[derive(Debug, CacheKey, Default)]
pub struct PerFileIgnores {
// Ordered as (absolute path matcher, basename matcher, rules)
ignores: Vec<(GlobMatcher, GlobMatcher, RuleSet)>,
}
impl PerFileIgnores {
/// Given a list of patterns, create a `GlobSet`.
pub fn resolve(per_file_ignores: Vec<PerFileIgnore>) -> Result<Self> {
let ignores: Result<Vec<_>> = per_file_ignores
.into_iter()
.map(|per_file_ignore| {
// Construct absolute path matcher.
let absolute =
Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher();
// Construct basename matcher.
let basename = Glob::new(&per_file_ignore.basename)?.compile_matcher();
Ok((absolute, basename, per_file_ignore.rules))
})
.collect();
Ok(Self { ignores: ignores? })
}
}
impl Display for PerFileIgnores {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.is_empty() {
write!(f, "{{}}")?;
} else {
writeln!(f, "{{")?;
for (absolute, basename, rules) in &self.ignores {
writeln!(
f,
"\t{{ absolute = {absolute:#?}, basename = {basename:#?}, rules = {rules} }},"
)?;
}
write!(f, "}}")?;
}
Ok(())
}
}
impl Deref for PerFileIgnores {
type Target = Vec<(GlobMatcher, GlobMatcher, RuleSet)>;
fn deref(&self) -> &Self::Target {
&self.ignores
}
}

View File

@@ -914,6 +914,27 @@ where
}
}
/// A [`Visitor`] to collect all stored [`Expr::Name`] nodes in an AST.
#[derive(Debug, Default)]
pub struct StoredNameFinder<'a> {
/// A map from identifier to defining expression.
pub names: FxHashMap<&'a str, &'a ast::ExprName>,
}
impl<'a, 'b> Visitor<'b> for StoredNameFinder<'a>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'a Expr) {
if let Expr::Name(name) = expr {
if name.ctx.is_store() {
self.names.insert(&name.id, name);
}
}
crate::visitor::walk_expr(self, expr);
}
}
/// A [`StatementVisitor`] that collects all `return` statements in a function or method.
#[derive(Default)]
pub struct ReturnStatementVisitor<'a> {

View File

@@ -79,3 +79,15 @@ pub fn next_sibling<'a>(stmt: &'a Stmt, suite: &'a Suite) -> Option<&'a Stmt> {
}
None
}
/// Given a [`Stmt`] and its containing [`Suite`], return the previous [`Stmt`] in the [`Suite`].
pub fn prev_sibling<'a>(stmt: &'a Stmt, suite: &'a Suite) -> Option<&'a Stmt> {
let mut prev = None;
for sibling in suite {
if sibling == stmt {
return prev;
}
prev = Some(sibling);
}
None
}

View File

@@ -2,6 +2,7 @@ use ruff_formatter::printer::{LineEnding, PrinterOptions, SourceMapGeneration};
use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType;
use std::fmt;
use std::path::Path;
use std::str::FromStr;
@@ -241,6 +242,16 @@ pub enum QuoteStyle {
Preserve,
}
impl fmt::Display for QuoteStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Single => write!(f, "single"),
Self::Double => write!(f, "double"),
Self::Preserve => write!(f, "preserve"),
}
}
}
impl FromStr for QuoteStyle {
type Err = &'static str;
@@ -277,6 +288,15 @@ impl MagicTrailingComma {
}
}
impl fmt::Display for MagicTrailingComma {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Respect => write!(f, "respect"),
Self::Ignore => write!(f, "ignore"),
}
}
}
impl FromStr for MagicTrailingComma {
type Err = &'static str;
@@ -306,6 +326,15 @@ impl PreviewMode {
}
}
impl fmt::Display for PreviewMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Disabled => write!(f, "disabled"),
Self::Enabled => write!(f, "enabled"),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
@@ -323,6 +352,15 @@ impl DocstringCode {
}
}
impl fmt::Display for DocstringCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Disabled => write!(f, "disabled"),
Self::Enabled => write!(f, "enabled"),
}
}
}
#[derive(Copy, Clone, Default, Eq, PartialEq, CacheKey)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
@@ -338,8 +376,8 @@ pub enum DocstringCodeLineWidth {
Dynamic,
}
impl std::fmt::Debug for DocstringCodeLineWidth {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
impl fmt::Debug for DocstringCodeLineWidth {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
DocstringCodeLineWidth::Fixed(v) => v.value().fmt(f),
DocstringCodeLineWidth::Dynamic => "dynamic".fmt(f),
@@ -347,6 +385,15 @@ impl std::fmt::Debug for DocstringCodeLineWidth {
}
}
impl fmt::Display for DocstringCodeLineWidth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Fixed(width) => width.fmt(f),
Self::Dynamic => write!(f, "dynamic"),
}
}
}
/// Responsible for deserializing the `DocstringCodeLineWidth::Dynamic`
/// variant.
fn deserialize_docstring_code_line_width_dynamic<'de, D>(d: D) -> Result<(), D::Error>

View File

@@ -31,8 +31,6 @@
use std::iter::FusedIterator;
use std::{char, cmp::Ordering, str::FromStr};
use unicode_ident::{is_xid_continue, is_xid_start};
use ruff_python_ast::{Int, IpyEscapeKind};
use ruff_text_size::{TextLen, TextRange, TextSize};
@@ -1491,19 +1489,39 @@ const fn is_ascii_identifier_start(c: char) -> bool {
// Checks if the character c is a valid starting character as described
// in https://docs.python.org/3/reference/lexical_analysis.html#identifiers
#[cold]
fn is_unicode_identifier_start(c: char) -> bool {
is_xid_start(c)
unicode_ident::is_xid_start(c)
}
// Checks if the character c is a valid continuation character as described
// in https://docs.python.org/3/reference/lexical_analysis.html#identifiers
fn is_identifier_continuation(c: char) -> bool {
match c {
'a'..='z' | 'A'..='Z' | '_' | '0'..='9' => true,
c => is_xid_continue(c),
if c.is_ascii() {
ASCII_CONTINUE.0[c as usize]
} else {
is_unicode_identifier_continue(c)
}
}
#[cold]
fn is_unicode_identifier_continue(c: char) -> bool {
unicode_ident::is_xid_continue(c)
}
const T: bool = true;
const F: bool = false;
#[repr(C, align(64))]
struct Align64<T>(pub(crate) T);
static ASCII_CONTINUE: Align64<[bool; 128]> = Align64([
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F,
F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, T, T, T, T, T, T, T, T, T, T, F, F, F, F, F, F,
F, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, F, F, F, F, T,
F, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, F, F, F, F, F,
]);
/// Returns `true` for [whitespace](https://docs.python.org/3/reference/lexical_analysis.html#whitespace-between-tokens)
/// characters.
///

View File

@@ -645,6 +645,24 @@ pub fn resolve_assignment<'a>(
pub fn find_assigned_value<'a>(symbol: &str, semantic: &'a SemanticModel<'a>) -> Option<&'a Expr> {
let binding_id = semantic.lookup_symbol(symbol)?;
let binding = semantic.binding(binding_id);
find_binding_value(symbol, binding, semantic)
}
/// Find the assigned [`Expr`] for a given [`Binding`], if any.
///
/// For example given:
/// ```python
/// foo = 42
/// (bar, bla) = 1, "str"
/// ```
///
/// This function will return a `NumberLiteral` with value `Int(42)` when called with `foo` and a
/// `StringLiteral` with value `"str"` when called with `bla`.
pub fn find_binding_value<'a>(
symbol: &str,
binding: &Binding,
semantic: &'a SemanticModel,
) -> Option<&'a Expr> {
match binding.kind {
// Ex) `x := 1`
BindingKind::NamedExprAssignment => {

View File

@@ -129,6 +129,11 @@ impl<'a> Scope<'a> {
self.shadowed_bindings.get(&id).copied()
}
/// Returns an iterator over all bindings that the given binding shadows, including itself.
pub fn shadowed_bindings(&self, id: BindingId) -> impl Iterator<Item = BindingId> + '_ {
std::iter::successors(Some(id), |id| self.shadowed_bindings.get(id).copied())
}
/// Adds a reference to a star import (e.g., `from sys import *`) to this scope.
pub fn add_star_import(&mut self, import: StarImport<'a>) {
self.star_imports.push(import);

View File

@@ -25,12 +25,10 @@ use ruff_linter::rule_selector::{PreviewOptions, Specificity};
use ruff_linter::rules::pycodestyle;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::{
ExtensionMapping, FilePattern, FilePatternSet, PerFileIgnore, PreviewMode, PythonVersion,
SerializationFormat, UnsafeFixes, Version,
};
use ruff_linter::settings::{
resolve_per_file_ignores, LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS,
ExtensionMapping, FilePattern, FilePatternSet, PerFileIgnore, PerFileIgnores, PreviewMode,
PythonVersion, SerializationFormat, UnsafeFixes, Version,
};
use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS};
use ruff_linter::{
fs, warn_user, warn_user_once, warn_user_once_by_id, RuleSelector, RUFF_PKG_VERSION,
};
@@ -117,6 +115,7 @@ pub struct Configuration {
pub output_format: Option<SerializationFormat>,
pub preview: Option<PreviewMode>,
pub required_version: Option<Version>,
pub extension: Option<ExtensionMapping>,
pub show_fixes: Option<bool>,
pub show_source: Option<bool>,
@@ -174,6 +173,7 @@ impl Configuration {
let formatter = FormatterSettings {
exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?,
extension: self.extension.clone().unwrap_or_default(),
preview: format_preview,
target_version: match target_version {
PythonVersion::Py37 => ruff_python_formatter::PythonVersion::Py37,
@@ -241,7 +241,7 @@ impl Configuration {
linter: LinterSettings {
rules: lint.as_rule_table(lint_preview),
exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?,
extension: lint.extension.unwrap_or_default(),
extension: self.extension.unwrap_or_default(),
preview: lint_preview,
target_version,
project_root: project_root.to_path_buf(),
@@ -258,7 +258,7 @@ impl Configuration {
line_length,
tab_size: self.indent_width.unwrap_or_default(),
namespace_packages: self.namespace_packages.unwrap_or_default(),
per_file_ignores: resolve_per_file_ignores(
per_file_ignores: PerFileIgnores::resolve(
lint.per_file_ignores
.unwrap_or_default()
.into_iter()
@@ -496,6 +496,9 @@ impl Configuration {
.map(|src| resolve_src(&src, project_root))
.transpose()?,
target_version: options.target_version,
// `--extension` is a hidden command-line argument that isn't supported in configuration
// files at present.
extension: None,
lint: LintConfiguration::from_options(lint, project_root)?,
format: FormatConfiguration::from_options(
@@ -538,6 +541,7 @@ impl Configuration {
src: self.src.or(config.src),
target_version: self.target_version.or(config.target_version),
preview: self.preview.or(config.preview),
extension: self.extension.or(config.extension),
lint: self.lint.combine(config.lint),
format: self.format.combine(config.format),
@@ -549,7 +553,6 @@ impl Configuration {
pub struct LintConfiguration {
pub exclude: Option<Vec<FilePattern>>,
pub preview: Option<PreviewMode>,
pub extension: Option<ExtensionMapping>,
// Rule selection
pub extend_per_file_ignores: Vec<PerFileIgnore>,
@@ -616,9 +619,6 @@ impl LintConfiguration {
.chain(options.common.extend_unfixable.into_iter().flatten())
.collect();
Ok(LintConfiguration {
// `--extension` is a hidden command-line argument that isn't supported in configuration
// files at present.
extension: None,
exclude: options.exclude.map(|paths| {
paths
.into_iter()
@@ -954,7 +954,6 @@ impl LintConfiguration {
Self {
exclude: self.exclude.or(config.exclude),
preview: self.preview.or(config.preview),
extension: self.extension.or(config.extension),
rule_selections: config
.rule_selections
.into_iter()
@@ -1031,6 +1030,7 @@ impl LintConfiguration {
pub struct FormatConfiguration {
pub exclude: Option<Vec<FilePattern>>,
pub preview: Option<PreviewMode>,
pub extension: Option<ExtensionMapping>,
pub indent_style: Option<IndentStyle>,
pub quote_style: Option<QuoteStyle>,
@@ -1044,6 +1044,9 @@ impl FormatConfiguration {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: FormatOptions, project_root: &Path) -> Result<Self> {
Ok(Self {
// `--extension` is a hidden command-line argument that isn't supported in configuration
// files at present.
extension: None,
exclude: options.exclude.map(|paths| {
paths
.into_iter()
@@ -1077,18 +1080,19 @@ impl FormatConfiguration {
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn combine(self, other: Self) -> Self {
pub fn combine(self, config: Self) -> Self {
Self {
exclude: self.exclude.or(other.exclude),
preview: self.preview.or(other.preview),
indent_style: self.indent_style.or(other.indent_style),
quote_style: self.quote_style.or(other.quote_style),
magic_trailing_comma: self.magic_trailing_comma.or(other.magic_trailing_comma),
line_ending: self.line_ending.or(other.line_ending),
docstring_code_format: self.docstring_code_format.or(other.docstring_code_format),
exclude: self.exclude.or(config.exclude),
preview: self.preview.or(config.preview),
extension: self.extension.or(config.extension),
indent_style: self.indent_style.or(config.indent_style),
quote_style: self.quote_style.or(config.quote_style),
magic_trailing_comma: self.magic_trailing_comma.or(config.magic_trailing_comma),
line_ending: self.line_ending.or(config.line_ending),
docstring_code_format: self.docstring_code_format.or(config.docstring_code_format),
docstring_code_line_width: self
.docstring_code_line_width
.or(other.docstring_code_line_width),
.or(config.docstring_code_line_width),
}
}
}

View File

@@ -1,7 +1,10 @@
use path_absolutize::path_dedot;
use ruff_cache::cache_dir;
use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
use ruff_linter::settings::types::{FilePattern, FilePatternSet, SerializationFormat, UnsafeFixes};
use ruff_linter::display_settings;
use ruff_linter::settings::types::{
ExtensionMapping, FilePattern, FilePatternSet, SerializationFormat, UnsafeFixes,
};
use ruff_linter::settings::LinterSettings;
use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType;
@@ -10,6 +13,7 @@ use ruff_python_formatter::{
QuoteStyle,
};
use ruff_source_file::find_newline;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, CacheKey)]
@@ -53,6 +57,30 @@ impl Default for Settings {
}
}
impl fmt::Display for Settings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n# General Settings")?;
display_settings! {
formatter = f,
fields = [
// We want the quotes and lossy UTF8 conversion for this path, so
// using PathBuf's `Debug` formatter suffices.
self.cache_dir | debug,
self.fix,
self.fix_only,
self.output_format,
self.show_fixes,
self.show_source,
self.unsafe_fixes,
self.file_resolver | nested,
self.linter | nested,
self.formatter | nested
]
}
Ok(())
}
}
#[derive(Debug, CacheKey)]
pub struct FileResolverSettings {
pub exclude: FilePatternSet,
@@ -64,6 +92,26 @@ pub struct FileResolverSettings {
pub project_root: PathBuf,
}
impl fmt::Display for FileResolverSettings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n# File Resolver Settings")?;
display_settings! {
formatter = f,
namespace = "file_resolver",
fields = [
self.exclude,
self.extend_exclude,
self.force_exclude,
self.include,
self.extend_include,
self.respect_gitignore,
self.project_root | debug,
]
}
Ok(())
}
}
pub(crate) static EXCLUDE: &[FilePattern] = &[
FilePattern::Builtin(".bzr"),
FilePattern::Builtin(".direnv"),
@@ -116,6 +164,7 @@ impl FileResolverSettings {
#[derive(CacheKey, Clone, Debug)]
pub struct FormatterSettings {
pub exclude: FilePatternSet,
pub extension: ExtensionMapping,
pub preview: PreviewMode,
pub target_version: ruff_python_formatter::PythonVersion,
@@ -177,6 +226,7 @@ impl Default for FormatterSettings {
Self {
exclude: FilePatternSet::default(),
extension: ExtensionMapping::default(),
target_version: default_options.target_version(),
preview: PreviewMode::Disabled,
line_width: default_options.line_width(),
@@ -191,6 +241,30 @@ impl Default for FormatterSettings {
}
}
impl fmt::Display for FormatterSettings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n# Formatter Settings")?;
display_settings! {
formatter = f,
namespace = "formatter",
fields = [
self.exclude,
self.target_version | debug,
self.preview,
self.line_width,
self.line_ending,
self.indent_style,
self.indent_width,
self.quote_style,
self.magic_trailing_comma,
self.docstring_code_format,
self.docstring_code_line_width,
]
}
Ok(())
}
}
#[derive(
Copy, Clone, Debug, Eq, PartialEq, Default, CacheKey, serde::Serialize, serde::Deserialize,
)]
@@ -212,3 +286,14 @@ pub enum LineEnding {
/// Line endings will be converted to `\n` on Unix and `\r\n` on Windows.
Native,
}
impl fmt::Display for LineEnding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Auto => write!(f, "auto"),
Self::Lf => write!(f, "lf"),
Self::CrLf => write!(f, "crlf"),
Self::Native => write!(f, "native"),
}
}
}

View File

@@ -528,6 +528,8 @@ Options:
Enable preview mode; checks will include unstable rules and fixes. Use `--no-preview` to disable
--config <CONFIG>
Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
--extension <EXTENSION>
List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`
--statistics
Show counts for every rule with at least one violation
--add-noqa
@@ -604,6 +606,8 @@ Options:
Avoid writing any formatted files back; instead, exit with a non-zero status code and the difference between the current file and how the formatted file would look like
--config <CONFIG>
Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
--extension <EXTENSION>
List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`
--target-version <TARGET_VERSION>
The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311, py312]
--preview

View File

@@ -334,7 +334,14 @@ Ruff is available as [`flymake-ruff`](https://melpa.org/#/flymake-ruff) on MELPA
(add-hook 'python-mode-hook #'flymake-ruff-load)
```
Ruff can be used as a formatter in Emacs using the [Apheleia](https://github.com/radian-software/apheleia) formatter library, by setting this configuration:
Ruff is also available as [`emacs-ruff-format`](https://github.com/scop/emacs-ruff-format):
```elisp
(require 'ruff-format)
(add-hook 'python-mode-hook 'ruff-format-on-save-mode)
```
Alternatively, it can be used via the [Apheleia](https://github.com/radian-software/apheleia) formatter library, by setting this configuration:
```emacs-lisp
(add-to-list 'apheleia-mode-alist '(python-mode . ruff))

View File

@@ -99,6 +99,7 @@
}
/* See: https://github.com/astral-sh/ruff/issues/9046 */
[data-md-color-scheme="astral-dark"] div.admonition p a {
color: var(--flare);
[data-md-color-scheme="astral-dark"] div.admonition {
color: var(--md-code-fg-color);
background-color: var(--md-code-bg-color);
}

1
ruff.schema.json generated
View File

@@ -3552,6 +3552,7 @@
"SIM11",
"SIM110",
"SIM112",
"SIM113",
"SIM114",
"SIM115",
"SIM116",