Compare commits

..

1 Commits

Author SHA1 Message Date
Zanie
105fb1c682 Refactor ecosystem checks into module 2023-10-23 11:55:14 -05:00
202 changed files with 2243 additions and 3738 deletions

View File

@@ -13,8 +13,3 @@ MD041: false
# MD013/line-length
MD013: false
# MD024/no-duplicate-heading
MD024:
# Allow when nested under different parents e.g. CHANGELOG.md
allow_different_nesting: true

View File

@@ -1,60 +1,11 @@
# Changelog
## 0.1.1
### Rule changes
- Add unsafe fix for `escape-sequence-in-docstring` (`D301`) (#7970)
### Configuration
- Respect `#(deprecated)` attribute in configuration options (#8035)
- Add `[format|lint].exclude` options (#8000)
- Respect `tab-size` setting in formatter (#8006)
- Add `lint.preview` (#8002)
## Preview features
- \[`pylint`\] Implement `literal-membership` (`PLR6201`) (#7973)
- \[`pylint`\] Implement `too-many-boolean-expressions` (`PLR0916`) (#7975)
- \[`pylint`\] Implement `misplaced-bare-raise` (`E0704`) (#7961)
- \[`pylint`\] Implement `global-at-module-level` (`W0604`) (#8058)
- \[`pylint`\] Implement `unspecified-encoding` (`PLW1514`) (#7939)
- Add fix for `triple-single-quotes` (`D300`) (#7967)
### Formatter
- New code style badge for `ruff format` (#7878)
- Fix comments outside expression parentheses (#7873)
- Add `--target-version` to `ruff format` (#8055)
- Skip over parentheses when detecting `in` keyword (#8054)
- Add `--diff` option to `ruff format` (#7937)
- Insert newline after nested function or class statements (#7946)
- Use `pass` over ellipsis in non-function/class contexts (#8049)
### Bug fixes
- Lazily evaluate all PEP 695 type alias values (#8033)
- Avoid failed assertion when showing fixes from stdin (#8029)
- Avoid flagging HTTP and HTTPS literals in urllib-open (#8046)
- Avoid flagging `bad-dunder-method-name` for `_` (#8015)
- Remove Python 2-only methods from `URLOpen` audit (#8047)
- Use set bracket replacement for `iteration-over-set` to preserve whitespace and comments (#8001)
### Documentation
- Update tutorial to match revised Ruff defaults (#8066)
- Update rule `B005` docs (#8028)
- Update GitHub actions example in docs to use `--output-format` (#8014)
- Document `lint.preview` and `format.preview` (#8032)
- Clarify that new rules should be added to `RuleGroup::Preview`. (#7989)
## 0.1.0
This is the first release which uses the `CHANGELOG` file. See [GitHub Releases](https://github.com/astral-sh/ruff/releases) for prior changelog entries.
Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/).
## 0.1.0
### Breaking changes
- Unsafe fixes are no longer displayed or applied without opt-in ([#7769](https://github.com/astral-sh/ruff/pull/7769))

8
Cargo.lock generated
View File

@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.1"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
@@ -2051,7 +2051,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.1"
version = "0.1.0"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2188,7 +2188,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.1"
version = "0.1.0"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.1",
@@ -2438,7 +2438,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.1"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.1
rev: v0.1.0
hooks:
- id: ruff
```
@@ -237,8 +237,9 @@ linting command.
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
Rust as a first-party feature.
By default, Ruff enables Flake8's `F` rules, along with a subset of the `E` rules, omitting any
stylistic rules that overlap with the use of a formatter, like
By default, Ruff enables Flake8's `E` and `F` rules. Ruff supports all rules from the `F` category,
and a [subset](https://docs.astral.sh/ruff/rules/#error-e) of the `E` category, omitting those
stylistic rules made obsolete by the use of a formatter, like
[Black](https://github.com/psf/black).
If you're just getting started with Ruff, **the default rule set is a great place to start**: it

View File

@@ -1,8 +0,0 @@
{
"label": "code style",
"message": "Ruff",
"logoSvg": "<svg width=\"510\" height=\"622\" viewBox=\"0 0 510 622\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M206.701 0C200.964 0 196.314 4.64131 196.314 10.3667V41.4667C196.314 47.192 191.663 51.8333 185.927 51.8333H156.843C151.107 51.8333 146.456 56.4746 146.456 62.2V145.133C146.456 150.859 141.806 155.5 136.069 155.5H106.986C101.249 155.5 96.5988 160.141 96.5988 165.867V222.883C96.5988 228.609 91.9484 233.25 86.2118 233.25H57.1283C51.3917 233.25 46.7413 237.891 46.7413 243.617V300.633C46.7413 306.359 42.0909 311 36.3544 311H10.387C4.6504 311 0 315.641 0 321.367V352.467C0 358.192 4.6504 362.833 10.387 362.833H145.418C151.154 362.833 155.804 367.475 155.804 373.2V430.217C155.804 435.942 151.154 440.583 145.418 440.583H116.334C110.597 440.583 105.947 445.225 105.947 450.95V507.967C105.947 513.692 101.297 518.333 95.5601 518.333H66.4766C60.74 518.333 56.0896 522.975 56.0896 528.7V611.633C56.0896 617.359 60.74 622 66.4766 622H149.572C155.309 622 159.959 617.359 159.959 611.633V570.167H201.507C207.244 570.167 211.894 565.525 211.894 559.8V528.7C211.894 522.975 216.544 518.333 222.281 518.333H251.365C257.101 518.333 261.752 513.692 261.752 507.967V476.867C261.752 471.141 266.402 466.5 272.138 466.5H301.222C306.959 466.5 311.609 461.859 311.609 456.133V425.033C311.609 419.308 316.259 414.667 321.996 414.667H351.079C356.816 414.667 361.466 410.025 361.466 404.3V373.2C361.466 367.475 366.117 362.833 371.853 362.833H400.937C406.673 362.833 411.324 358.192 411.324 352.467V321.367C411.324 315.641 415.974 311 421.711 311H450.794C456.531 311 461.181 306.359 461.181 300.633V217.7C461.181 211.975 456.531 207.333 450.794 207.333H420.672C414.936 207.333 410.285 202.692 410.285 196.967V165.867C410.285 160.141 414.936 155.5 420.672 155.5H449.756C455.492 155.5 460.143 150.859 460.143 145.133V114.033C460.143 108.308 464.793 103.667 470.53 103.667H499.613C505.35 103.667 510 99.0253 510 93.3V10.3667C510 4.64132 505.35 0 499.613 0H206.701ZM168.269 440.583C162.532 440.583 157.882 445.225 157.882 450.95V507.967C157.882 513.692 153.231 518.333 147.495 518.333H118.411C112.675 518.333 108.024 522.975 108.024 528.7V559.8C108.024 565.525 112.675 570.167 118.411 570.167H159.959V528.7C159.959 522.975 164.61 518.333 170.346 518.333H199.43C205.166 518.333 209.817 513.692 209.817 507.967V476.867C209.817 471.141 214.467 466.5 220.204 466.5H249.287C255.024 466.5 259.674 461.859 259.674 456.133V425.033C259.674 419.308 264.325 414.667 270.061 414.667H299.145C304.881 414.667 309.532 410.025 309.532 404.3V373.2C309.532 367.475 314.182 362.833 319.919 362.833H349.002C354.739 362.833 359.389 358.192 359.389 352.467V321.367C359.389 315.641 364.039 311 369.776 311H398.859C404.596 311 409.246 306.359 409.246 300.633V269.533C409.246 263.808 404.596 259.167 398.859 259.167H318.88C313.143 259.167 308.493 254.525 308.493 248.8V217.7C308.493 211.975 313.143 207.333 318.88 207.333H347.963C353.7 207.333 358.35 202.692 358.35 196.967V165.867C358.35 160.141 363.001 155.5 368.737 155.5H397.821C403.557 155.5 408.208 150.859 408.208 145.133V114.033C408.208 108.308 412.858 103.667 418.595 103.667H447.678C453.415 103.667 458.065 99.0253 458.065 93.3V62.2C458.065 56.4746 453.415 51.8333 447.678 51.8333H208.778C203.041 51.8333 198.391 56.4746 198.391 62.2V145.133C198.391 150.859 193.741 155.5 188.004 155.5H158.921C153.184 155.5 148.534 160.141 148.534 165.867V222.883C148.534 228.609 143.883 233.25 138.147 233.25H109.063C103.327 233.25 98.6762 237.891 98.6762 243.617V300.633C98.6762 306.359 103.327 311 109.063 311H197.352C203.089 311 207.739 315.641 207.739 321.367V430.217C207.739 435.942 203.089 440.583 197.352 440.583H168.269Z\" fill=\"#D7FF64\"/></svg>",
"logoWidth": 10,
"labelColor": "grey",
"color": "#261230"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.1.1"
version = "0.1.0"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.1"
version = "0.1.0"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1 +0,0 @@
print("All formatted!")

View File

@@ -1,37 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "98e1dd71-14a2-454d-9be0-061dde560b07",
"metadata": {},
"outputs": [],
"source": [
"import numpy\n",
"maths = (numpy.arange(100)**2).sum()\n",
"stats= numpy.asarray([1,2,3,4]).median()"
]
}
],
"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.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,3 +0,0 @@
x = 1
y=2
z = 3

View File

@@ -353,10 +353,6 @@ pub struct FormatCommand {
/// files would have been modified, and zero otherwise.
#[arg(long)]
pub check: bool,
/// 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.
#[arg(long)]
pub diff: bool,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for configuration.
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
@@ -370,15 +366,6 @@ pub struct FormatCommand {
respect_gitignore: bool,
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
no_respect_gitignore: bool,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(
long,
value_delimiter = ',',
value_name = "FILE_PATTERN",
help_heading = "File selection"
)]
pub exclude: Option<Vec<FilePattern>>,
/// Enforce exclusions, even for paths passed to Ruff directly on the command-line.
/// Use `--no-force-exclude` to disable.
#[arg(
@@ -398,12 +385,10 @@ pub struct FormatCommand {
/// The name of the file when passing it through stdin.
#[arg(long, help_heading = "Miscellaneous")]
pub stdin_filename: Option<PathBuf>,
/// The minimum Python version that should be supported.
#[arg(long, value_enum)]
pub target_version: Option<PythonVersion>,
/// Enable preview mode; enables unstable formatting.
/// Enable preview mode; checks will include unstable rules and fixes.
/// Use `--no-preview` to disable.
#[arg(long, overrides_with("no_preview"))]
#[arg(long, overrides_with("no_preview"), hide = true)]
preview: bool,
#[clap(long, overrides_with("preview"), hide = true)]
no_preview: bool,
@@ -526,7 +511,6 @@ impl FormatCommand {
(
FormatArguments {
check: self.check,
diff: self.diff,
config: self.config,
files: self.files,
isolated: self.isolated,
@@ -538,10 +522,8 @@ impl FormatCommand {
self.respect_gitignore,
self.no_respect_gitignore,
),
exclude: self.exclude,
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
target_version: self.target_version,
// Unsupported on the formatter CLI, but required on `Overrides`.
..CliOverrides::default()
},
@@ -585,7 +567,6 @@ pub struct CheckArguments {
#[allow(clippy::struct_excessive_bools)]
pub struct FormatArguments {
pub check: bool,
pub diff: bool,
pub config: Option<PathBuf>,
pub files: Vec<PathBuf>,
pub isolated: bool,
@@ -677,8 +658,6 @@ impl ConfigurationTransformer for CliOverrides {
}
if let Some(preview) = &self.preview {
config.preview = Some(*preview);
config.lint.preview = Some(*preview);
config.format.preview = Some(*preview);
}
if let Some(per_file_ignores) = &self.per_file_ignores {
config.lint.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io;
use std::io::{stderr, stdout, Write};
use std::path::{Path, PathBuf};
use std::time::Instant;
@@ -11,7 +9,6 @@ use itertools::Itertools;
use log::error;
use rayon::iter::Either::{Left, Right};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use similar::TextDiff;
use thiserror::Error;
use tracing::debug;
@@ -23,7 +20,7 @@ use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, FormatModuleError};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{match_exclusion, python_files_in_path};
use ruff_workspace::resolver::python_files_in_path;
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
@@ -37,20 +34,6 @@ pub(crate) enum FormatMode {
Write,
/// Check if the file is formatted, but do not write the formatted contents back.
Check,
/// Check if the file is formatted, show a diff if not.
Diff,
}
impl FormatMode {
pub(crate) fn from_cli(cli: &FormatArguments) -> Self {
if cli.diff {
FormatMode::Diff
} else if cli.check {
FormatMode::Check
} else {
FormatMode::Write
}
}
}
/// Format a set of files, and return the exit status.
@@ -65,7 +48,11 @@ pub(crate) fn format(
overrides,
cli.stdin_filename.as_deref(),
)?;
let mode = FormatMode::from_cli(cli);
let mode = if cli.check {
FormatMode::Check
} else {
FormatMode::Write
};
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, overrides)?;
if paths.is_empty() {
@@ -74,61 +61,26 @@ pub(crate) fn format(
}
let start = Instant::now();
let (mut results, mut errors): (Vec<_>, Vec<_>) = paths
let (results, errors): (Vec<_>, Vec<_>) = paths
.into_par_iter()
.filter_map(|entry| {
match entry {
Ok(resolved_file) => {
let path = resolved_file.path();
Ok(entry) => {
let path = entry.into_path();
let SourceType::Python(source_type) = SourceType::from(&path) else {
// Ignore any non-Python files.
return None;
};
let resolved_settings = resolver.resolve(path, &pyproject_config);
// Ignore files that are excluded from formatting
if !resolved_file.is_root()
&& match_exclusion(
path,
resolved_file.file_name(),
&resolved_settings.formatter.exclude,
)
{
return None;
}
// Extract the sources from the file.
let unformatted = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return None,
Err(err) => {
return Some(Err(FormatCommandError::Read(
Some(path.to_path_buf()),
err,
)));
}
};
let resolved_settings = resolver.resolve(&path, &pyproject_config);
Some(
match catch_unwind(|| {
format_path(
path,
&resolved_settings.formatter,
&unformatted,
source_type,
mode,
)
format_path(&path, &resolved_settings.formatter, source_type, mode)
}) {
Ok(inner) => inner.map(|result| FormatPathResult {
path: resolved_file.into_path(),
unformatted,
result,
}),
Err(error) => Err(FormatCommandError::Panic(
Some(resolved_file.into_path()),
error,
)),
Ok(inner) => inner.map(|result| FormatPathResult { path, result }),
Err(error) => Err(FormatCommandError::Panic(Some(path), error)),
},
)
}
@@ -141,27 +93,6 @@ pub(crate) fn format(
});
let duration = start.elapsed();
// Make output deterministic, at least as long as we have a path
results.sort_unstable_by(|x, y| x.path.cmp(&y.path));
errors.sort_by(|x, y| {
fn get_key(error: &FormatCommandError) -> Option<&PathBuf> {
match &error {
FormatCommandError::Ignore(ignore) => {
if let ignore::Error::WithPath { path, .. } = ignore {
Some(path)
} else {
None
}
}
FormatCommandError::Panic(path, _)
| FormatCommandError::Read(path, _)
| FormatCommandError::Format(path, _)
| FormatCommandError::Write(path, _) => path.as_ref(),
}
}
get_key(x).cmp(&get_key(y))
});
debug!(
"Formatted {} files in {:.2?}",
results.len() + errors.len(),
@@ -173,20 +104,13 @@ pub(crate) fn format(
error!("{error}");
}
results.sort_unstable_by(|a, b| a.path.cmp(&b.path));
let results = FormatResults::new(results.as_slice(), mode);
if mode.is_diff() {
results.write_diff(&mut stdout().lock())?;
}
let summary = FormatSummary::new(results.as_slice(), mode);
// Report on the formatting changes.
if log_level >= LogLevel::Default {
if mode.is_diff() {
// Allow piping the diff to e.g. a file by writing the summary to stderr
results.write_summary(&mut stderr().lock())?;
} else {
results.write_summary(&mut stdout().lock())?;
#[allow(clippy::print_stdout)]
{
println!("{summary}");
}
}
@@ -198,9 +122,9 @@ pub(crate) fn format(
Ok(ExitStatus::Error)
}
}
FormatMode::Check | FormatMode::Diff => {
FormatMode::Check => {
if errors.is_empty() {
if results.any_formatted() {
if summary.any_formatted() {
Ok(ExitStatus::Failure)
} else {
Ok(ExitStatus::Success)
@@ -213,47 +137,61 @@ pub(crate) fn format(
}
/// Format the file at the given [`Path`].
#[tracing::instrument(level="debug", skip_all, fields(path = %path.display()))]
#[tracing::instrument(skip_all, fields(path = %path.display()))]
fn format_path(
path: &Path,
settings: &FormatterSettings,
unformatted: &SourceKind,
source_type: PySourceType,
mode: FormatMode,
) -> Result<FormatResult, FormatCommandError> {
// Extract the sources from the file.
let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => return Ok(FormatResult::Unchanged),
Err(err) => {
return Err(FormatCommandError::Read(Some(path.to_path_buf()), err));
}
};
// Format the source.
let format_result = match format_source(unformatted, source_type, Some(path), settings)? {
FormattedSource::Formatted(formatted) => match mode {
FormatMode::Write => {
match format_source(source_kind, source_type, Some(path), settings)? {
FormattedSource::Formatted(formatted) => {
if mode.is_write() {
let mut writer = File::create(path).map_err(|err| {
FormatCommandError::Write(Some(path.to_path_buf()), err.into())
})?;
formatted
.write(&mut writer)
.map_err(|err| FormatCommandError::Write(Some(path.to_path_buf()), err))?;
FormatResult::Formatted
}
FormatMode::Check => FormatResult::Formatted,
FormatMode::Diff => FormatResult::Diff(formatted),
},
FormattedSource::Unchanged => FormatResult::Unchanged,
};
Ok(format_result)
Ok(FormatResult::Formatted)
}
FormattedSource::Unchanged(_) => Ok(FormatResult::Unchanged),
}
}
#[derive(Debug)]
pub(crate) enum FormattedSource {
/// The source was formatted, and the [`SourceKind`] contains the transformed source code.
Formatted(SourceKind),
/// The source was unchanged.
Unchanged,
/// The source was unchanged, and the [`SourceKind`] contains the original source code.
Unchanged(SourceKind),
}
impl From<FormattedSource> for FormatResult {
fn from(value: FormattedSource) -> Self {
match value {
FormattedSource::Formatted(_) => FormatResult::Formatted,
FormattedSource::Unchanged => FormatResult::Unchanged,
FormattedSource::Unchanged(_) => FormatResult::Unchanged,
}
}
}
impl FormattedSource {
pub(crate) fn source_kind(&self) -> &SourceKind {
match self {
FormattedSource::Formatted(source_kind) => source_kind,
FormattedSource::Unchanged(source_kind) => source_kind,
}
}
}
@@ -261,28 +199,30 @@ impl From<FormattedSource> for FormatResult {
/// Format a [`SourceKind`], returning the transformed [`SourceKind`], or `None` if the source was
/// unchanged.
pub(crate) fn format_source(
source_kind: &SourceKind,
source_kind: SourceKind,
source_type: PySourceType,
path: Option<&Path>,
settings: &FormatterSettings,
) -> Result<FormattedSource, FormatCommandError> {
match source_kind {
SourceKind::Python(unformatted) => {
let options = settings.to_format_options(source_type, unformatted);
let options = settings.to_format_options(source_type, &unformatted);
let formatted = format_module_source(unformatted, options)
let formatted = format_module_source(&unformatted, options)
.map_err(|err| FormatCommandError::Format(path.map(Path::to_path_buf), err))?;
let formatted = formatted.into_code();
if formatted.len() == unformatted.len() && formatted == *unformatted {
Ok(FormattedSource::Unchanged)
Ok(FormattedSource::Unchanged(SourceKind::Python(unformatted)))
} else {
Ok(FormattedSource::Formatted(SourceKind::Python(formatted)))
}
}
SourceKind::IpyNotebook(notebook) => {
if !notebook.is_python_notebook() {
return Ok(FormattedSource::Unchanged);
return Ok(FormattedSource::Unchanged(SourceKind::IpyNotebook(
notebook,
)));
}
let options = settings.to_format_options(source_type, notebook.source_code());
@@ -330,7 +270,9 @@ pub(crate) fn format_source(
// If the file was unchanged, return `None`.
let (Some(mut output), Some(last)) = (output, last) else {
return Ok(FormattedSource::Unchanged);
return Ok(FormattedSource::Unchanged(SourceKind::IpyNotebook(
notebook,
)));
};
// Add the remaining content.
@@ -338,23 +280,21 @@ pub(crate) fn format_source(
output.push_str(slice);
// Update the notebook.
let mut formatted = notebook.clone();
formatted.update(&source_map, output);
let mut notebook = notebook.clone();
notebook.update(&source_map, output);
Ok(FormattedSource::Formatted(SourceKind::IpyNotebook(
formatted,
notebook,
)))
}
}
}
/// The result of an individual formatting operation.
#[derive(Debug, Clone, is_macro::Is)]
#[derive(Debug, Clone, Copy, is_macro::Is)]
pub(crate) enum FormatResult {
/// The file was formatted.
Formatted,
/// The file was formatted, [`SourceKind`] contains the formatted code
Diff(SourceKind),
/// The file was unchanged, as the formatted contents matched the existing contents.
Unchanged,
}
@@ -363,55 +303,38 @@ pub(crate) enum FormatResult {
#[derive(Debug)]
struct FormatPathResult {
path: PathBuf,
unformatted: SourceKind,
result: FormatResult,
}
/// The results of formatting a set of files
/// A summary of the formatting results.
#[derive(Debug)]
struct FormatResults<'a> {
struct FormatSummary<'a> {
/// The individual formatting results.
results: &'a [FormatPathResult],
/// The format mode that was used.
mode: FormatMode,
}
impl<'a> FormatResults<'a> {
impl<'a> FormatSummary<'a> {
fn new(results: &'a [FormatPathResult], mode: FormatMode) -> Self {
Self { results, mode }
}
/// Returns `true` if any of the files require formatting.
fn any_formatted(&self) -> bool {
self.results.iter().any(|result| match result.result {
FormatResult::Formatted | FormatResult::Diff { .. } => true,
FormatResult::Unchanged => false,
})
self.results
.iter()
.any(|result| result.result.is_formatted())
}
}
fn write_diff(&self, f: &mut impl Write) -> io::Result<()> {
for result in self.results {
if let FormatResult::Diff(formatted) = &result.result {
let text_diff =
TextDiff::from_lines(result.unformatted.source_code(), formatted.source_code());
let mut unified_diff = text_diff.unified_diff();
unified_diff.header(
&fs::relativize_path(&result.path),
&fs::relativize_path(&result.path),
);
unified_diff.to_writer(&mut *f)?;
}
}
Ok(())
}
fn write_summary(&self, f: &mut impl Write) -> io::Result<()> {
impl Display for FormatSummary<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
// Compute the number of changed and unchanged files.
let mut changed = 0u32;
let mut formatted = 0u32;
let mut unchanged = 0u32;
for result in self.results {
match &result.result {
match result.result {
FormatResult::Formatted => {
// If we're running in check mode, report on any files that would be formatted.
if self.mode.is_check() {
@@ -421,42 +344,39 @@ impl<'a> FormatResults<'a> {
fs::relativize_path(&result.path).bold()
)?;
}
changed += 1;
formatted += 1;
}
FormatResult::Unchanged => unchanged += 1,
FormatResult::Diff(_) => {
changed += 1;
}
}
}
// Write out a summary of the formatting results.
if changed > 0 && unchanged > 0 {
writeln!(
if formatted > 0 && unchanged > 0 {
write!(
f,
"{} file{} {}, {} file{} left unchanged",
changed,
if changed == 1 { "" } else { "s" },
formatted,
if formatted == 1 { "" } else { "s" },
match self.mode {
FormatMode::Write => "reformatted",
FormatMode::Check | FormatMode::Diff => "would be reformatted",
FormatMode::Check => "would be reformatted",
},
unchanged,
if unchanged == 1 { "" } else { "s" },
)
} else if changed > 0 {
writeln!(
} else if formatted > 0 {
write!(
f,
"{} file{} {}",
changed,
if changed == 1 { "" } else { "s" },
formatted,
if formatted == 1 { "" } else { "s" },
match self.mode {
FormatMode::Write => "reformatted",
FormatMode::Check | FormatMode::Diff => "would be reformatted",
FormatMode::Check => "would be reformatted",
}
)
} else if unchanged > 0 {
writeln!(
write!(
f,
"{} file{} left unchanged",
unchanged,

View File

@@ -3,18 +3,14 @@ use std::path::Path;
use anyhow::Result;
use log::error;
use ruff_linter::fs;
use similar::TextDiff;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
use ruff_workspace::resolver::python_file_at_path;
use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments};
use crate::commands::format::{
format_source, FormatCommandError, FormatMode, FormatResult, FormattedSource,
};
use crate::commands::format::{format_source, FormatCommandError, FormatMode, FormatResult};
use crate::resolve::resolve;
use crate::stdin::read_from_stdin;
use crate::ExitStatus;
@@ -27,20 +23,16 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
overrides,
cli.stdin_filename.as_deref(),
)?;
let mode = FormatMode::from_cli(cli);
let mode = if cli.check {
FormatMode::Check
} else {
FormatMode::Write
};
if let Some(filename) = cli.stdin_filename.as_deref() {
if !python_file_at_path(filename, &pyproject_config, overrides)? {
return Ok(ExitStatus::Success);
}
let format_settings = &pyproject_config.settings.formatter;
if filename
.file_name()
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{
return Ok(ExitStatus::Success);
}
}
let path = cli.stdin_filename.as_deref();
@@ -58,7 +50,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
) {
Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => {
FormatMode::Check => {
if result.is_formatted() {
Ok(ExitStatus::Failure)
} else {
@@ -93,37 +85,15 @@ fn format_source_code(
};
// Format the source.
let formatted = format_source(&source_kind, source_type, path, settings)?;
let formatted = format_source(source_kind, source_type, path, settings)?;
match &formatted {
FormattedSource::Formatted(formatted) => match mode {
FormatMode::Write => {
let mut writer = stdout().lock();
formatted
.write(&mut writer)
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
}
FormatMode::Check => {}
FormatMode::Diff => {
let mut writer = stdout().lock();
let text_diff =
TextDiff::from_lines(source_kind.source_code(), formatted.source_code());
let mut unified_diff = text_diff.unified_diff();
if let Some(path) = path {
unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path));
}
unified_diff.to_writer(&mut writer).unwrap();
}
},
FormattedSource::Unchanged => {
// Write to stdout regardless of whether the source was formatted
if mode.is_write() {
let mut writer = stdout().lock();
source_kind
.write(&mut writer)
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
}
}
// Write to stdout regardless of whether the source was formatted.
if mode.is_write() {
let mut writer = stdout().lock();
formatted
.source_kind()
.write(&mut writer)
.map_err(|err| FormatCommandError::Write(path.map(Path::to_path_buf), err))?;
}
Ok(FormatResult::from(formatted))

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
#![cfg(not(target_family = "wasm"))]
use std::fs;
use std::path::Path;
use std::process::Command;
use std::str;
@@ -14,7 +13,7 @@ const BIN_NAME: &str = "ruff";
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py"])
.args(["format", "--isolated"])
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
@@ -51,9 +50,6 @@ fn format_options() -> Result<()> {
fs::write(
&ruff_toml,
r#"
tab-size = 8
line-length = 84
[format]
indent-style = "tab"
quote-style = "single"
@@ -68,7 +64,7 @@ line-ending = "cr-lf"
.arg("-")
.pass_stdin(r#"
def foo(arg1, arg2,):
print("Shouldn't change quotes. It exceeds the line width with the tab size 8")
print("Shouldn't change quotes")
if condition:
@@ -80,9 +76,7 @@ if condition:
exit_code: 0
----- stdout -----
def foo(arg1, arg2):
print(
"Shouldn't change quotes. It exceeds the line width with the tab size 8"
)
print("Shouldn't change quotes")
if condition:
@@ -94,108 +88,6 @@ if condition:
Ok(())
}
#[test]
fn exclude() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-exclude = ["out"]
[format]
exclude = ["test.py", "generated.py"]
"#,
)?;
fs::write(
tempdir.path().join("main.py"),
r#"
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#,
)?;
// Excluded file but passed to the CLI directly, should be formatted
let test_path = tempdir.path().join("test.py");
fs::write(
&test_path,
r#"
def say_hy(name: str):
print(f"Hy {name}")"#,
)?;
fs::write(
tempdir.path().join("generated.py"),
r#"NUMBERS = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
]
OTHER = "OTHER"
"#,
)?;
let out_dir = tempdir.path().join("out");
fs::create_dir(&out_dir)?;
fs::write(out_dir.join("a.py"), "a = a")?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--check", "--config"])
.arg(ruff_toml.file_name().unwrap())
// Explicitly pass test.py, should be formatted regardless of it being excluded by format.exclude
.arg(test_path.file_name().unwrap())
// Format all other files in the directory, should respect the `exclude` and `format.exclude` options
.arg("."), @r###"
success: false
exit_code: 1
----- stdout -----
Would reformat: main.py
Would reformat: test.py
2 files would be reformatted
----- stderr -----
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
Ok(())
}
#[test]
fn exclude_stdin() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
extend-select = ["B", "Q"]
[format]
exclude = ["generated.py"]
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--config", &ruff_toml.file_name().unwrap().to_string_lossy(), "--stdin-filename", "generated.py", "-"])
.pass_stdin(r#"
from test import say_hy
if __name__ == '__main__':
say_hy("dear Ruff contributor")
"#), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
Ok(())
}
#[test]
fn format_option_inheritance() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -290,125 +182,3 @@ format = "json"
});
Ok(())
}
#[test]
fn test_diff() {
let args = ["format", "--isolated", "--diff"];
let fixtures = Path::new("resources").join("test").join("fixtures");
let paths = [
fixtures.join("unformatted.py"),
fixtures.join("formatted.py"),
fixtures.join("unformatted.ipynb"),
];
insta::with_settings!({filters => vec![
// Replace windows paths
(r"\\", "/"),
]}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).args(args).args(paths),
@r###"
success: false
exit_code: 1
----- stdout -----
--- resources/test/fixtures/unformatted.ipynb
+++ resources/test/fixtures/unformatted.ipynb
@@ -1,3 +1,4 @@
import numpy
-maths = (numpy.arange(100)**2).sum()
-stats= numpy.asarray([1,2,3,4]).median()
+
+maths = (numpy.arange(100) ** 2).sum()
+stats = numpy.asarray([1, 2, 3, 4]).median()
--- resources/test/fixtures/unformatted.py
+++ resources/test/fixtures/unformatted.py
@@ -1,3 +1,3 @@
x = 1
-y=2
+y = 2
z = 3
----- stderr -----
warning: `ruff format` is not yet stable, and subject to change in future versions.
2 files would be reformatted, 1 file left unchanged
"###);
});
}
#[test]
fn test_diff_no_change() {
let args = ["format", "--isolated", "--diff"];
let fixtures = Path::new("resources").join("test").join("fixtures");
let paths = [fixtures.join("unformatted.py")];
insta::with_settings!({filters => vec![
// Replace windows paths
(r"\\", "/"),
]}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).args(args).args(paths),
@r###"
success: false
exit_code: 1
----- stdout -----
--- resources/test/fixtures/unformatted.py
+++ resources/test/fixtures/unformatted.py
@@ -1,3 +1,3 @@
x = 1
-y=2
+y = 2
z = 3
----- stderr -----
warning: `ruff format` is not yet stable, and subject to change in future versions.
1 file would be reformatted
"###
);
});
}
#[test]
fn test_diff_stdin_unformatted() {
let args = [
"format",
"--isolated",
"--diff",
"-",
"--stdin-filename",
"unformatted.py",
];
let fixtures = Path::new("resources").join("test").join("fixtures");
let unformatted = fs::read(fixtures.join("unformatted.py")).unwrap();
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).args(args).pass_stdin(unformatted),
@r###"
success: false
exit_code: 1
----- stdout -----
--- unformatted.py
+++ unformatted.py
@@ -1,3 +1,3 @@
x = 1
-y=2
+y = 2
z = 3
----- stderr -----
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
}
#[test]
fn test_diff_stdin_formatted() {
let args = ["format", "--isolated", "--diff", "-"];
let fixtures = Path::new("resources").join("test").join("fixtures");
let unformatted = fs::read(fixtures.join("formatted.py")).unwrap();
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME)).args(args).pass_stdin(unformatted),
@r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `ruff format` is not yet stable, and subject to change in future versions.
"###);
}

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ use strum::IntoEnumIterator;
use ruff_diagnostics::FixAvailability;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use ruff_workspace::options::Options;
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
use ruff_workspace::options_base::OptionsMetadata;
use crate::ROOT_DIR;
@@ -55,11 +55,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
output.push('\n');
}
process_documentation(
explanation.trim(),
&mut output,
&rule.noqa_code().to_string(),
);
process_documentation(explanation.trim(), &mut output);
let filename = PathBuf::from(ROOT_DIR)
.join("docs")
@@ -78,7 +74,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
Ok(())
}
fn process_documentation(documentation: &str, out: &mut String, rule_name: &str) {
fn process_documentation(documentation: &str, out: &mut String) {
let mut in_options = false;
let mut after = String::new();
@@ -104,17 +100,7 @@ fn process_documentation(documentation: &str, out: &mut String, rule_name: &str)
if let Some(rest) = line.strip_prefix("- `") {
let option = rest.trim_end().trim_end_matches('`');
match Options::metadata().find(option) {
Some(OptionEntry::Field(field)) => {
if field.deprecated.is_some() {
eprintln!("Rule {rule_name} references deprecated option {option}.");
}
}
Some(_) => {}
None => {
panic!("Unknown option {option} referenced by rule {rule_name}");
}
}
assert!(Options::metadata().has(option), "unknown option {option}");
let anchor = option.replace('.', "-");
out.push_str(&format!("- [`{option}`][{option}]\n"));
@@ -152,7 +138,6 @@ Something [`else`][other].
[other]: http://example.com.",
&mut output,
"example",
);
assert_eq!(
output,

View File

@@ -101,24 +101,6 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parent_set:
output.push_str(&format!("{header_level} [`{name}`](#{name})\n"));
}
output.push('\n');
if let Some(deprecated) = &field.deprecated {
output.push_str("!!! warning \"Deprecated\"\n");
output.push_str(" This option has been deprecated");
if let Some(since) = deprecated.since {
write!(output, " in {since}").unwrap();
}
output.push('.');
if let Some(message) = deprecated.message {
writeln!(output, " {message}").unwrap();
}
output.push('\n');
}
output.push_str(field.doc);
output.push_str("\n\n");
output.push_str(&format!("**Default value**: `{}`\n", field.default));

View File

@@ -95,7 +95,7 @@ impl std::fmt::Display for IndentStyle {
///
/// Determines the visual width of a tab character (`\t`) and the number of
/// spaces per indent when using [`IndentStyle::Space`].
#[derive(Clone, Copy, Debug, Eq, PartialEq, CacheKey)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct IndentWidth(NonZeroU8);
@@ -575,10 +575,6 @@ where
context: PhantomData,
}
}
pub fn rule(&self) -> &R {
&self.rule
}
}
impl<T, R, O, C> FormatRefWithRule<'_, T, R, C>

View File

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

View File

@@ -1,19 +0,0 @@
import urllib.request
urllib.request.urlopen(url='http://www.google.com')
urllib.request.urlopen(url='http://www.google.com', **kwargs)
urllib.request.urlopen('http://www.google.com')
urllib.request.urlopen('file:///foo/bar/baz')
urllib.request.urlopen(url)
urllib.request.Request(url='http://www.google.com', **kwargs)
urllib.request.Request(url='http://www.google.com')
urllib.request.Request('http://www.google.com')
urllib.request.Request('file:///foo/bar/baz')
urllib.request.Request(url)
urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
urllib.request.URLopener().open(fullurl='http://www.google.com')
urllib.request.URLopener().open('http://www.google.com')
urllib.request.URLopener().open('file:///foo/bar/baz')
urllib.request.URLopener().open(url)

View File

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

View File

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

View File

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

View File

@@ -1,10 +0,0 @@
global price # W0604
price = 25
if True:
global X # W0604
def no_error():
global price
price = 30

View File

@@ -21,9 +21,6 @@ use crate::settings::types::PythonVersion;
pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
match stmt {
Stmt::Global(ast::StmtGlobal { names, range: _ }) => {
if checker.enabled(Rule::GlobalAtModuleLevel) {
pylint::rules::global_at_module_level(checker, stmt);
}
if checker.enabled(Rule::AmbiguousVariableName) {
checker.diagnostics.extend(names.iter().filter_map(|name| {
pycodestyle::rules::ambiguous_variable_name(name, name.range())

View File

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

View File

@@ -582,9 +582,9 @@ where
if let Some(type_params) = type_params {
self.visit_type_params(type_params);
}
self.deferred
.type_param_definitions
.push((value, self.semantic.snapshot()));
// The value in a `type` alias has annotation semantics, in that it's never
// evaluated at runtime.
self.visit_annotation(value);
self.semantic.pop_scope();
self.visit_expr(name);
}
@@ -1389,14 +1389,9 @@ where
}
}
// Step 2: Traversal
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
bound: Some(bound), ..
}) = type_param
{
self.deferred
.type_param_definitions
.push((bound, self.semantic.snapshot()));
}
self.deferred
.type_param_definitions
.push((type_param, self.semantic.snapshot()));
}
}
@@ -1771,9 +1766,12 @@ impl<'a> Checker<'a> {
for (type_param, snapshot) in type_params {
self.semantic.restore(snapshot);
self.semantic.flags |=
SemanticModelFlags::TYPE_PARAM_DEFINITION | SemanticModelFlags::TYPE_DEFINITION;
self.visit_expr(type_param);
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
bound: Some(bound), ..
}) = type_param
{
self.visit_annotation(bound);
}
}
}
self.semantic.restore(snapshot);

View File

@@ -264,7 +264,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W0131") => (RuleGroup::Stable, rules::pylint::rules::NamedExprWithoutContext),
(Pylint, "W0406") => (RuleGroup::Stable, rules::pylint::rules::ImportSelf),
(Pylint, "W0602") => (RuleGroup::Stable, rules::pylint::rules::GlobalVariableNotAssigned),
(Pylint, "W0604") => (RuleGroup::Preview, rules::pylint::rules::GlobalAtModuleLevel),
(Pylint, "W0603") => (RuleGroup::Stable, rules::pylint::rules::GlobalStatement),
(Pylint, "W0711") => (RuleGroup::Stable, rules::pylint::rules::BinaryOpException),
(Pylint, "W1508") => (RuleGroup::Stable, rules::pylint::rules::InvalidEnvvarDefault),

View File

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

View File

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

View File

@@ -253,7 +253,7 @@ impl FileExemption {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());
let path_display = relativize_path(path);
warn!("Unexpected `# ruff: noqa` directive at {path_display}:{line}. File-level suppression comments must appear on their own line. For line-level suppression, omit the `ruff:` prefix.");
warn!("Unexpected `# ruff: noqa` directive at {path_display}:{line}. File-level suppression comments must appear on their own line.");
continue;
}

View File

@@ -42,7 +42,6 @@ mod tests {
#[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))]
#[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))]
#[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))]
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"))]
#[test_case(Rule::TryExceptPass, Path::new("S110.py"))]

View File

@@ -1,9 +1,10 @@
//! Check for calls to suspicious functions, or calls into suspicious modules.
//!
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html>
use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -849,23 +850,10 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()),
// MarkSafe
["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
// URLOpen (`urlopen`, `urlretrieve`, `Request`)
["urllib", "request", "urlopen" | "urlretrieve" | "Request"] |
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "Request"] => {
// If the `url` argument is a string literal, allow `http` and `https` schemes.
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
if let Some(Expr::Constant(ast::ExprConstant { value: ast::Constant::Str(url), .. })) = &call.arguments.find_argument("url", 0) {
let url = url.trim_start();
if url.starts_with("http://") || url.starts_with("https://") {
return None;
}
}
}
Some(SuspiciousURLOpenUsage.into())
},
// URLOpen (`URLopener`, `FancyURLopener`)
["urllib", "request", "URLopener" | "FancyURLopener"] |
["six", "moves", "urllib", "request", "URLopener" | "FancyURLopener"] => Some(SuspiciousURLOpenUsage.into()),
// URLOpen
["urllib", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener" | "Request"] |
["urllib", "request", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener"] |
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener"] => Some(SuspiciousURLOpenUsage.into()),
// NonCryptographicRandom
["random", "random" | "randrange" | "randint" | "choice" | "choices" | "uniform" | "triangular"] => Some(SuspiciousNonCryptographicRandomUsage.into()),
// UnverifiedContext

View File

@@ -1,107 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
---
S310.py:4:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
3 | urllib.request.urlopen(url='http://www.google.com')
4 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
5 | urllib.request.urlopen('http://www.google.com')
6 | urllib.request.urlopen('file:///foo/bar/baz')
|
S310.py:6:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
4 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
5 | urllib.request.urlopen('http://www.google.com')
6 | urllib.request.urlopen('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
7 | urllib.request.urlopen(url)
|
S310.py:7:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
5 | urllib.request.urlopen('http://www.google.com')
6 | urllib.request.urlopen('file:///foo/bar/baz')
7 | urllib.request.urlopen(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
8 |
9 | urllib.request.Request(url='http://www.google.com', **kwargs)
|
S310.py:9:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
7 | urllib.request.urlopen(url)
8 |
9 | urllib.request.Request(url='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
10 | urllib.request.Request(url='http://www.google.com')
11 | urllib.request.Request('http://www.google.com')
|
S310.py:12:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
10 | urllib.request.Request(url='http://www.google.com')
11 | urllib.request.Request('http://www.google.com')
12 | urllib.request.Request('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
13 | urllib.request.Request(url)
|
S310.py:13:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
11 | urllib.request.Request('http://www.google.com')
12 | urllib.request.Request('file:///foo/bar/baz')
13 | urllib.request.Request(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
14 |
15 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
S310.py:15:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
13 | urllib.request.Request(url)
14 |
15 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
16 | urllib.request.URLopener().open(fullurl='http://www.google.com')
17 | urllib.request.URLopener().open('http://www.google.com')
|
S310.py:16:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
15 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
16 | urllib.request.URLopener().open(fullurl='http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
17 | urllib.request.URLopener().open('http://www.google.com')
18 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
S310.py:17:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
15 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
16 | urllib.request.URLopener().open(fullurl='http://www.google.com')
17 | urllib.request.URLopener().open('http://www.google.com')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
18 | urllib.request.URLopener().open('file:///foo/bar/baz')
19 | urllib.request.URLopener().open(url)
|
S310.py:18:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
16 | urllib.request.URLopener().open(fullurl='http://www.google.com')
17 | urllib.request.URLopener().open('http://www.google.com')
18 | urllib.request.URLopener().open('file:///foo/bar/baz')
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
19 | urllib.request.URLopener().open(url)
|
S310.py:19:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
17 | urllib.request.URLopener().open('http://www.google.com')
18 | urllib.request.URLopener().open('file:///foo/bar/baz')
19 | urllib.request.URLopener().open(url)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ mod tests {
use test_case::test_case;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_messages, settings};
@@ -108,33 +107,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::TripleSingleQuotes, Path::new("D.py"))]
#[test_case(Rule::TripleSingleQuotes, Path::new("D300.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
// Tests for rules with preview features
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("pydocstyle").join(path).as_path(),
&settings::LinterSettings {
pydocstyle: Settings {
convention: None,
ignore_decorators: BTreeSet::from_iter(["functools.wraps".to_string()]),
property_decorators: BTreeSet::from_iter([
"gi.repository.GObject.Property".to_string()
]),
},
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn bom() -> Result<()> {
let diagnostics = test_path(

View File

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

View File

@@ -78,14 +78,12 @@ pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
let mut diagnostic =
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
if checker.settings.preview.is_enabled() {
let body = docstring.body().as_str();
if !body.ends_with('\'') {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{prefixes}'''{body}'''"),
docstring.range(),
)));
}
let body = docstring.body().as_str();
if !body.ends_with('\'') {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{prefixes}'''{body}'''"),
docstring.range(),
)));
}
checker.diagnostics.push(diagnostic);
@@ -96,14 +94,12 @@ pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
let mut diagnostic =
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
if checker.settings.preview.is_enabled() {
let body = docstring.body().as_str();
if !body.ends_with('"') {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{prefixes}\"\"\"{body}\"\"\""),
docstring.range(),
)));
}
let body = docstring.body().as_str();
if !body.ends_with('"') {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{prefixes}\"\"\"{body}\"\"\""),
docstring.range(),
)));
}
checker.diagnostics.push(diagnostic);

View File

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

View File

@@ -9,7 +9,7 @@ D300.py:6:5: D300 Use triple double quotes `"""`
|
= help: Convert to triple double quotes
D300.py:10:5: D300 Use triple double quotes `"""`
D300.py:10:5: D300 [*] Use triple double quotes `"""`
|
9 | def contains_quote():
10 | 'Sum"\\mary.'
@@ -17,4 +17,11 @@ D300.py:10:5: D300 Use triple double quotes `"""`
|
= help: Convert to triple double quotes
Fix
7 7 |
8 8 |
9 9 | def contains_quote():
10 |- 'Sum"\\mary.'
10 |+ """Sum"\\mary."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -138,7 +138,6 @@ mod tests {
#[test_case(Rule::NoSelfUse, Path::new("no_self_use.py"))]
#[test_case(Rule::MisplacedBareRaise, Path::new("misplaced_bare_raise.py"))]
#[test_case(Rule::LiteralMembership, Path::new("literal_membership.py"))]
#[test_case(Rule::GlobalAtModuleLevel, Path::new("global_at_module_level.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,34 +0,0 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Stmt;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the `global` keyword at the module level.
///
/// ## Why is this bad?
/// The `global` keyword is used within functions to indicate that a name
/// refers to a global variable, rather than a local variable.
///
/// At the module level, all names are global by default, so the `global`
/// keyword is redundant.
#[violation]
pub struct GlobalAtModuleLevel;
impl Violation for GlobalAtModuleLevel {
#[derive_message_formats]
fn message(&self) -> String {
format!("`global` at module level is redundant")
}
}
/// PLW0604
pub(crate) fn global_at_module_level(checker: &mut Checker, stmt: &Stmt) {
if checker.semantic().current_scope().kind.is_module() {
checker
.diagnostics
.push(Diagnostic::new(GlobalAtModuleLevel, stmt.range()));
}
}

View File

@@ -14,7 +14,6 @@ pub(crate) use comparison_with_itself::*;
pub(crate) use continue_in_finally::*;
pub(crate) use duplicate_bases::*;
pub(crate) use eq_without_hash::*;
pub(crate) use global_at_module_level::*;
pub(crate) use global_statement::*;
pub(crate) use global_variable_not_assigned::*;
pub(crate) use import_self::*;
@@ -79,7 +78,6 @@ mod comparison_with_itself;
mod continue_in_finally;
mod duplicate_bases;
mod eq_without_hash;
mod global_at_module_level;
mod global_statement;
mod global_variable_not_assigned;
mod import_self;

View File

@@ -15,6 +15,7 @@ pub enum ConstantType {
Float,
Int,
Str,
Tuple,
}
impl TryFrom<&Constant> for ConstantType {

View File

@@ -1,21 +0,0 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
global_at_module_level.py:1:1: PLW0604 `global` at module level is redundant
|
1 | global price # W0604
| ^^^^^^^^^^^^ PLW0604
2 |
3 | price = 25
|
global_at_module_level.py:6:5: PLW0604 `global` at module level is redundant
|
5 | if True:
6 | global X # W0604
| ^^^^^^^^ PLW0604
7 |
8 | def no_error():
|

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
Ok(quote! {
impl crate::configuration::CombinePluginOptions for #ident {
fn combine(self, other: Self) -> Self {
#[allow(deprecated)]
Self {
#(
#output

View File

@@ -1,15 +1,16 @@
use proc_macro2::{TokenStream, TokenTree};
use proc_macro2::TokenTree;
use quote::{quote, quote_spanned};
use syn::meta::ParseNestedMeta;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::{
AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field,
Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Type, TypePath,
Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Token, Type, TypePath,
};
use ruff_python_trivia::textwrap::dedent;
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput {
ident,
data,
@@ -189,30 +190,9 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
default,
value_type,
example,
} = parse_field_attributes(attr)?;
} = attr.parse_args::<FieldAttributes>()?;
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
let deprecated = if let Some(deprecated) = field
.attrs
.iter()
.find(|attr| attr.path().is_ident("deprecated"))
{
fn quote_option(option: Option<String>) -> TokenStream {
match option {
None => quote!(None),
Some(value) => quote!(Some(#value)),
}
}
let deprecated = parse_deprecated_attribute(deprecated)?;
let note = quote_option(deprecated.note);
let since = quote_option(deprecated.since);
quote!(Some(crate::options_base::Deprecated { since: #since, message: #note }))
} else {
quote!(None)
};
Ok(quote_spanned!(
ident.span() => {
visit.record_field(#kebab_name, crate::options_base::OptionField{
@@ -220,7 +200,6 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
default: &#default,
value_type: &#value_type,
example: &#example,
deprecated: #deprecated
})
}
))
@@ -233,109 +212,39 @@ struct FieldAttributes {
example: String,
}
fn parse_field_attributes(attribute: &Attribute) -> syn::Result<FieldAttributes> {
let mut default = None;
let mut value_type = None;
let mut example = None;
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
default = Some(get_string_literal(&meta, "default", "option")?.value());
} else if meta.path.is_ident("value_type") {
value_type = Some(get_string_literal(&meta, "value_type", "option")?.value());
} else if meta.path.is_ident("example") {
let example_text = get_string_literal(&meta, "value_type", "option")?.value();
example = Some(dedent(&example_text).trim_matches('\n').to_string());
} else {
return Err(syn::Error::new(
meta.path.span(),
format!(
"Deprecated meta {:?} is not supported by ruff's option macro.",
meta.path.get_ident()
),
));
impl Parse for FieldAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let default = _parse_key_value(input, "default")?;
input.parse::<Comma>()?;
let value_type = _parse_key_value(input, "value_type")?;
input.parse::<Comma>()?;
let example = _parse_key_value(input, "example")?;
if !input.is_empty() {
input.parse::<Comma>()?;
}
Ok(())
})?;
let Some(default) = default else {
return Err(syn::Error::new(attribute.span(), "Mandatory `default` field is missing in `#[option]` attribute. Specify the default using `#[option(default=\"..\")]`."));
};
let Some(value_type) = value_type else {
return Err(syn::Error::new(attribute.span(), "Mandatory `value_type` field is missing in `#[option]` attribute. Specify the value type using `#[option(value_type=\"..\")]`."));
};
let Some(example) = example else {
return Err(syn::Error::new(attribute.span(), "Mandatory `example` field is missing in `#[option]` attribute. Add an example using `#[option(example=\"..\")]`."));
};
Ok(FieldAttributes {
default,
value_type,
example,
})
}
fn parse_deprecated_attribute(attribute: &Attribute) -> syn::Result<DeprecatedAttribute> {
let mut deprecated = DeprecatedAttribute::default();
attribute.parse_nested_meta(|meta| {
if meta.path.is_ident("note") {
deprecated.note = Some(get_string_literal(&meta, "note", "deprecated")?.value());
} else if meta.path.is_ident("since") {
deprecated.since = Some(get_string_literal(&meta, "since", "deprecated")?.value());
} else {
return Err(syn::Error::new(
meta.path.span(),
format!(
"Deprecated meta {:?} is not supported by ruff's option macro.",
meta.path.get_ident()
),
));
}
Ok(())
})?;
Ok(deprecated)
}
fn get_string_literal(
meta: &ParseNestedMeta,
meta_name: &str,
attribute_name: &str,
) -> syn::Result<syn::LitStr> {
let expr: syn::Expr = meta.value()?.parse()?;
let mut value = &expr;
while let syn::Expr::Group(e) = value {
value = &e.expr;
}
if let syn::Expr::Lit(ExprLit {
lit: Lit::Str(lit), ..
}) = value
{
let suffix = lit.suffix();
if !suffix.is_empty() {
return Err(syn::Error::new(
lit.span(),
format!("unexpected suffix `{suffix}` on string literal"),
));
}
Ok(lit.clone())
} else {
Err(syn::Error::new(
expr.span(),
format!("expected {attribute_name} attribute to be a string: `{meta_name} = \"...\"`"),
))
Ok(Self {
default,
value_type,
example: dedent(&example).trim_matches('\n').to_string(),
})
}
}
#[derive(Default, Debug)]
struct DeprecatedAttribute {
since: Option<String>,
note: Option<String>,
fn _parse_key_value(input: ParseStream, name: &str) -> syn::Result<String> {
let ident: proc_macro2::Ident = input.parse()?;
if ident != name {
return Err(syn::Error::new(
ident.span(),
format!("Expected `{name}` name"),
));
}
input.parse::<Token![=]>()?;
let value: Lit = input.parse()?;
match &value {
Lit::Str(v) => Ok(v.value()),
_ => Err(syn::Error::new(value.span(), "Expected literal string")),
}
}

View File

@@ -333,6 +333,7 @@ pub enum ComparableConstant<'a> {
Str { value: &'a str, unicode: bool },
Bytes(&'a [u8]),
Int(&'a ast::Int),
Tuple(Vec<ComparableConstant<'a>>),
Float(u64),
Complex { real: u64, imag: u64 },
Ellipsis,

View File

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

View File

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

View File

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

View File

@@ -4817,7 +4817,7 @@ pub enum AnyNodeRef<'a> {
ElifElseClause(&'a ast::ElifElseClause),
}
impl<'a> AnyNodeRef<'a> {
impl AnyNodeRef<'_> {
pub fn as_ptr(&self) -> NonNull<()> {
match self {
AnyNodeRef::ModModule(node) => NonNull::from(*node).cast(),
@@ -5456,9 +5456,9 @@ impl<'a> AnyNodeRef<'a> {
)
}
pub fn visit_preorder<'b, V>(&'b self, visitor: &mut V)
pub fn visit_preorder<'a, V>(&'a self, visitor: &mut V)
where
V: PreorderVisitor<'b> + ?Sized,
V: PreorderVisitor<'a> + ?Sized,
{
match self {
AnyNodeRef::ModModule(node) => node.visit_preorder(visitor),
@@ -5544,66 +5544,6 @@ impl<'a> AnyNodeRef<'a> {
AnyNodeRef::ElifElseClause(node) => node.visit_preorder(visitor),
}
}
/// The last child of the last branch, if the node has multiple branches.
pub fn last_child_in_body(&self) -> Option<AnyNodeRef<'a>> {
let body = match self {
AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. })
| AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. })
| AnyNodeRef::StmtWith(ast::StmtWith { body, .. })
| AnyNodeRef::MatchCase(MatchCase { body, .. })
| AnyNodeRef::ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler {
body,
..
})
| AnyNodeRef::ElifElseClause(ast::ElifElseClause { body, .. }) => body,
AnyNodeRef::StmtIf(ast::StmtIf {
body,
elif_else_clauses,
..
}) => elif_else_clauses.last().map_or(body, |clause| &clause.body),
AnyNodeRef::StmtFor(ast::StmtFor { body, orelse, .. })
| AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => {
if orelse.is_empty() {
body
} else {
orelse
}
}
AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => {
return cases.last().map(AnyNodeRef::from);
}
AnyNodeRef::StmtTry(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
if finalbody.is_empty() {
if orelse.is_empty() {
if handlers.is_empty() {
body
} else {
return handlers.last().map(AnyNodeRef::from);
}
} else {
orelse
}
} else {
finalbody
}
}
// Not a node that contains an indented child node.
_ => return None,
};
body.last().map(AnyNodeRef::from)
}
}
impl<'a> From<&'a ast::ModModule> for AnyNodeRef<'a> {

View File

@@ -1,47 +1,38 @@
use ruff_python_trivia::{BackwardsTokenizer, CommentRanges, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextLen, TextRange};
use crate::AnyNodeRef;
use crate::node::AnyNodeRef;
use crate::ExpressionRef;
/// Returns an iterator over the ranges of the optional parentheses surrounding an expression.
///
/// E.g. for `((f()))` with `f()` as expression, the iterator returns the ranges (1, 6) and (0, 7).
///
/// Note that without a parent the range can be inaccurate, e.g. `f(a)` we falsely return a set of
/// parentheses around `a` even if the parentheses actually belong to `f`. That is why you should
/// generally prefer [`parenthesized_range`].
pub fn parentheses_iterator<'a>(
expr: ExpressionRef<'a>,
parent: Option<AnyNodeRef>,
comment_ranges: &'a CommentRanges,
source: &'a str,
) -> impl Iterator<Item = TextRange> + 'a {
let right_tokenizer = if let Some(parent) = parent {
// If the parent is a node that brings its own parentheses, exclude the closing parenthesis
// from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which
// the open and close parentheses are part of the `Arguments` node.
//
// There are a few other nodes that may have their own parentheses, but are fine to exclude:
// - `Parameters`: The parameters to a function definition. Any expressions would represent
// default arguments, and so must be preceded by _at least_ the parameter name. As such,
// we won't mistake any parentheses for the opening and closing parentheses on the
// `Parameters` node itself.
// - `Tuple`: The elements of a tuple. The only risk is a single-element tuple (e.g., `(x,)`),
// which must have a trailing comma anyway.
let exclusive_parent_end = if parent.is_arguments() {
parent.end() - ")".text_len()
} else {
parent.end()
};
SimpleTokenizer::new(source, TextRange::new(expr.end(), exclusive_parent_end))
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
/// parenthesized; or `None`, if the expression is not parenthesized.
pub fn parenthesized_range(
expr: ExpressionRef,
parent: AnyNodeRef,
comment_ranges: &CommentRanges,
source: &str,
) -> Option<TextRange> {
// If the parent is a node that brings its own parentheses, exclude the closing parenthesis
// from our search range. Otherwise, we risk matching on calls, like `func(x)`, for which
// the open and close parentheses are part of the `Arguments` node.
//
// There are a few other nodes that may have their own parentheses, but are fine to exclude:
// - `Parameters`: The parameters to a function definition. Any expressions would represent
// default arguments, and so must be preceded by _at least_ the parameter name. As such,
// we won't mistake any parentheses for the opening and closing parentheses on the
// `Parameters` node itself.
// - `Tuple`: The elements of a tuple. The only risk is a single-element tuple (e.g., `(x,)`),
// which must have a trailing comma anyway.
let exclusive_parent_end = if parent.is_arguments() {
parent.end() - ")".text_len()
} else {
SimpleTokenizer::starts_at(expr.end(), source)
parent.end()
};
let right_tokenizer = right_tokenizer
.skip_trivia()
.take_while(|token| token.kind == SimpleTokenKind::RParen);
let right_tokenizer =
SimpleTokenizer::new(source, TextRange::new(expr.end(), exclusive_parent_end))
.skip_trivia()
.take_while(|token| token.kind == SimpleTokenKind::RParen);
let left_tokenizer = BackwardsTokenizer::up_to(expr.start(), source, comment_ranges)
.skip_trivia()
@@ -52,16 +43,6 @@ pub fn parentheses_iterator<'a>(
// the `right_tokenizer` is exhausted.
right_tokenizer
.zip(left_tokenizer)
.last()
.map(|(right, left)| TextRange::new(left.start(), right.end()))
}
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
/// parenthesized; or `None`, if the expression is not parenthesized.
pub fn parenthesized_range(
expr: ExpressionRef,
parent: AnyNodeRef,
comment_ranges: &CommentRanges,
source: &str,
) -> Option<TextRange> {
parentheses_iterator(expr, Some(parent), comment_ranges, source).last()
}

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ if [
dddddddddddddddddddd,
eeeeeeeeee,
] & aaaaaaaaaaaaaaaaaaaaaaaaaa:
pass
...
if [
aaaaaaaaaaaaa,
@@ -84,7 +84,7 @@ if [
dddddddddddddddddddd,
eeeeeeeeee,
] & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
pass
...
# Right only can break
if aaaaaaaaaaaaaaaaaaaaaaaaaa & [
@@ -94,7 +94,7 @@ if aaaaaaaaaaaaaaaaaaaaaaaaaa & [
dddddddddddddddddddd,
eeeeeeeeee,
]:
pass
...
if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & [
aaaaaaaaaaaaa,
@@ -103,7 +103,7 @@ if aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
dddddddddddddddddddd,
eeeeeeeeee,
]:
pass
...
# Left or right can break
@@ -114,7 +114,7 @@ if [2222, 333] & [
dddddddddddddddddddd,
eeeeeeeeee,
]:
pass
...
if [
aaaaaaaaaaaaa,
@@ -123,7 +123,7 @@ if [
dddddddddddddddddddd,
eeeeeeeeee,
] & [2222, 333]:
pass
...
if [
aaaaaaaaaaaaa,
@@ -132,7 +132,7 @@ if [
dddddddddddddddddddd,
eeeeeeeeee,
] & [fffffffffffffffff, gggggggggggggggggggg, hhhhhhhhhhhhhhhhhhhhh, iiiiiiiiiiiiiiii, jjjjjjjjjjjjj]:
pass
...
if (
# comment
@@ -152,7 +152,7 @@ if (
]:
pass
pass
...
# Nesting
if (aaaa + b) & [
@@ -162,7 +162,7 @@ if (aaaa + b) & [
iiiiiiiiiiiiiiii,
jjjjjjjjjjjjj,
]:
pass
...
if [
fffffffffffffffff,
@@ -171,7 +171,7 @@ if [
iiiiiiiiiiiiiiii,
jjjjjjjjjjjjj,
] & (a + b):
pass
...
if [
@@ -185,7 +185,7 @@ if [
a
+ b
):
pass
...
if (
[
@@ -199,7 +199,7 @@ if (
# comment
a + b
):
pass
...
# Unstable formatting in https://github.com/realtyem/synapse-unraid/blob/unraid_develop/synapse/handlers/presence.py

View File

@@ -16,7 +16,7 @@ if (
and self._returncode
and self._proc.poll()
):
pass
...
if (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
@@ -26,14 +26,14 @@ if (
and aaaaaaaaaaaaaaaaaaaaaaaaaa
and aaaaaaaaaaaaaaaaaaaaaaaaaaaa
):
pass
...
if (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaas
and aaaaaaaaaaaaaaaaa
):
pass
...
if [2222, 333] and [
@@ -43,7 +43,7 @@ if [2222, 333] and [
dddddddddddddddddddd,
eeeeeeeeee,
]:
pass
...
if [
aaaaaaaaaaaaa,

View File

@@ -102,6 +102,3 @@ aaaaaaaaaaaaaaaaaaaaa = [
c # negative decimal
]
# Parenthesized targets and iterators.
[x for (x) in y]
[x for x in (y)]

View File

@@ -65,7 +65,7 @@ d3 = "d"[
# Spacing around the colon(s)
def a():
pass
...
e00 = "e"[:]
e01 = "e"[:1]

View File

@@ -139,18 +139,18 @@ if not \
# Regression: https://github.com/astral-sh/ruff/issues/5338
if a and not aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa & aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
pass
...
if (
not
# comment
a):
pass
...
if (
not # comment
a):
pass
...
# Regression test for: https://github.com/astral-sh/ruff/issues/7423
if True:
@@ -161,14 +161,3 @@ if True:
+ "WARNING: Removing listed files. Do you really want to continue. yes/n)? "
):
pass
# https://github.com/astral-sh/ruff/issues/7448
x = (
# a
not # b
# c
( # d
# e
True
)
)

View File

@@ -51,7 +51,7 @@ aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbb
for converter in connection.ops.get_db_converters(
expression
) + expression.get_db_converters(connection):
pass
...
aaa = (

View File

@@ -1,7 +1,6 @@
###
# Blank lines around functions
###
import sys
x = 1
@@ -160,97 +159,3 @@ def f():
# comment
x = 1
def f():
if True:
def double(s):
return s + s
print("below function")
if True:
class A:
x = 1
print("below class")
if True:
def double(s):
return s + s
#
print("below comment function")
if True:
class A:
x = 1
#
print("below comment class")
if True:
def double(s):
return s + s
#
print("below comment function 2")
if True:
def double(s):
return s + s
#
def outer():
def inner():
pass
print("below nested functions")
if True:
def double(s):
return s + s
print("below function")
if True:
class A:
x = 1
print("below class")
def outer():
def inner():
pass
print("below nested functions")
class Path:
if sys.version_info >= (3, 11):
def joinpath(self): ...
# The .open method comes from pathlib.pyi and should be kept in sync.
@overload
def open(self): ...
def fakehttp():
class FakeHTTPConnection:
if mock_close:
def close(self):
pass
FakeHTTPConnection.fakedata = fakedata
if True:
if False:
def x():
def y():
pass
#comment
print()
# NOTE: Please keep this the last block in this file. This tests that we don't insert
# empty line(s) at the end of the file due to nested function
if True:
def nested_trailing_function():
pass

View File

@@ -1,113 +0,0 @@
list_with_parenthesized_elements1 = [
# comment leading outer
(
# comment leading inner
1 + 2 # comment trailing inner
) # comment trailing outer
]
list_with_parenthesized_elements2 = [
# leading outer
(1 + 2)
]
list_with_parenthesized_elements3 = [
# leading outer
(1 + 2) # trailing outer
]
list_with_parenthesized_elements4 = [
# leading outer
(1 + 2), # trailing outer
]
list_with_parenthesized_elements5 = [
(1), # trailing outer
(2), # trailing outer
]
nested_parentheses1 = (
(
(
1
) # i
) # j
) # k
nested_parentheses2 = [
(
(
(
1
) # i
# i2
) # j
# j2
) # k
# k2
]
nested_parentheses3 = (
( # a
( # b
1
) # i
) # j
) # k
nested_parentheses4 = [
# a
( # b
# c
( # d
# e
( #f
1
) # i
# i2
) # j
# j2
) # k
# k2
]
x = (
# unary comment
not
# in-between comment
(
# leading inner
"a"
),
not # in-between comment
(
# leading inner
"b"
),
not
( # in-between comment
# leading inner
"c"
),
# 1
not # 2
( # 3
# 4
"d"
)
)
if (
# unary comment
not
# in-between comment
(
# leading inner
1
)
):
pass
# Make sure we keep a inside the parentheses
# https://github.com/astral-sh/ruff/issues/7892
x = (
# a
( # b
1
)
)

View File

@@ -15,23 +15,23 @@ for aVeryLongNameThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAnd
pass
else:
pass
...
for (
x,
y,
) in z: # comment
pass
...
# remove brackets around x,y but keep them around z,w
for (x, y) in (z, w):
pass
...
# type comment
for x in (): # type: int
pass
...
# Tuple parentheses for iterable.
for x in 1, 2, 3:

View File

@@ -19,12 +19,12 @@ else: # 12 trailing else condition
if x == y:
if y == z:
pass
...
if a == b:
pass
...
else: # trailing comment
pass
...
# trailing else comment
@@ -34,11 +34,11 @@ elif aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +
2222222222222222222222,
3333333333
]:
pass
...
else:
pass
...
# Regression test: Don't drop the trailing comment by associating it with the elif
# instead of the else.

View File

@@ -206,9 +206,9 @@ match pattern_singleton:
case (
True # trailing
):
pass
...
case False:
pass
...
match foo:
@@ -406,39 +406,39 @@ match pattern_match_class:
case Point2D(
# own line
):
pass
...
case (
Point2D
# own line
()
):
pass
...
case Point2D( # end of line line
):
pass
...
case Point2D( # end of line
0, 0
):
pass
...
case Point2D(0, 0):
pass
...
case Point2D(
( # end of line
# own line
0
), 0):
pass
...
case Point3D(x=0, y=0, z=000000000000000000000000000000000000000000000000000000000000000000000000000000000):
pass
...
case Bar(0, a=None, b="hello"):
pass
...
case FooBar(# leading
# leading
@@ -449,7 +449,7 @@ match pattern_match_class:
# trailing
# trailing
):
pass
...
case A(
b # b
@@ -481,26 +481,26 @@ match pattern_match_or:
# own line 4
c # trailing 5
):
pass
...
case (
(a)
| # trailing
( b )
):
pass
...
case (a|b|c):
pass
...
case foo | bar | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh:
pass
...
case ( # end of line
a | b
# own line
):
pass
...
# Single-element tuples.

View File

@@ -1,72 +1,72 @@
try:
pass
...
except:
pass
...
try:
pass
...
except (KeyError): # should remove brackets and be a single line
pass
...
try: # try
pass
...
# end of body
# before except
except (Exception, ValueError) as exc: # except line
pass
...
# before except 2
except KeyError as key: # except line 2
pass
...
# in body 2
# before else
else:
pass
...
# before finally
finally:
pass
...
# with line breaks
try: # try
pass
...
# end of body
# before except
except (Exception, ValueError) as exc: # except line
pass
...
# before except 2
except KeyError as key: # except line 2
pass
...
# in body 2
# before else
else:
pass
...
# before finally
finally:
pass
...
# with line breaks
try:
pass
...
except:
pass
...
try:
pass
...
except (Exception, Exception, Exception, Exception, Exception, Exception, Exception) as exc: # splits exception over multiple lines
pass
...
try:
pass
...
except:
a = 10 # trailing comment1
b = 11 # trailing comment2
@@ -74,21 +74,21 @@ except:
# try/except*, mostly the same as try
try: # try
pass
...
# end of body
# before except
except* (Exception, ValueError) as exc: # except line
pass
...
# before except 2
except* KeyError as key: # except line 2
pass
...
# in body 2
# before else
else:
pass
...
# before finally
finally:
pass
...
# try and try star are statements with body
# Minimized from https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91

View File

@@ -15,7 +15,7 @@ while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGo
pass
else:
pass
...
while (
some_condition(unformatted, args) and anotherCondition or aThirdCondition

View File

@@ -1,16 +1,16 @@
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
pass
...
# trailing
with a, a: # after colon
pass
...
# trailing
with (
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
):
pass
...
# trailing
@@ -19,7 +19,7 @@ with (
, # comma
b # c
): # colon
pass
...
with (
@@ -30,7 +30,7 @@ with (
, # comma
c # c
): # colon
pass # body
... # body
# body trailing own
with (
@@ -42,14 +42,14 @@ with (
with (a,): # magic trailing comma
pass
...
with (a): # should remove brackets
pass
...
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
pass
...
# currently unparsable by black: https://github.com/psf/black/issues/3678
@@ -60,41 +60,45 @@ with (a, *b):
with (
# leading comment
a) as b: pass
a) as b: ...
with (
# leading comment
a as b
): pass
): ...
with (
a as b
# trailing comment
): pass
): ...
with (
a as (
# leading comment
b
)
): pass
): ...
with (
a as (
b
# trailing comment
)
): pass
): ...
with (a # trailing same line comment
# trailing own line comment
) as b: ...
with (
a # trailing same line comment
# trailing own line comment
as b
): pass
): ...
with (a # trailing same line comment
# trailing own line comment
) as b: pass
) as b: ...
with (
(a
@@ -102,7 +106,7 @@ with (
)
as # trailing as same line comment
b # trailing b same line comment
): pass
): ...
with (
# comment
@@ -153,7 +157,7 @@ with (
CtxManager2() as example2,
CtxManager2() as example2,
):
pass
...
with [
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
@@ -161,7 +165,7 @@ with [
"cccccccccccccccccccccccccccccccccccccccccc",
dddddddddddddddddddddddddddddddd,
] as example1, aaaaaaaaaaaaaaaaaaaaaaaaaa * bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccccccc + ddddddddddddddddd as example2, CtxManager222222222222222() as example2:
pass
...
# Comments on open parentheses
with ( # comment
@@ -169,7 +173,7 @@ with ( # comment
CtxManager2() as example2,
CtxManager3() as example3,
):
pass
...
with ( # outer comment
( # inner comment
@@ -178,25 +182,25 @@ with ( # outer comment
CtxManager2() as example2,
CtxManager3() as example3,
):
pass
...
with ( # outer comment
CtxManager()
) as example:
pass
...
with ( # outer comment
CtxManager()
) as example, ( # inner comment
CtxManager2()
) as example2:
pass
...
with ( # outer comment
CtxManager1(),
CtxManager2(),
) as example:
pass
...
with ( # outer comment
( # inner comment
@@ -204,7 +208,7 @@ with ( # outer comment
),
CtxManager2(),
) as example:
pass
...
# Breaking of with items.
with (test # bar

View File

@@ -17,7 +17,7 @@ class Test:
c = 30
while a == 10:
print(a)
...
# trailing comment with one line before
@@ -26,7 +26,7 @@ while a == 10:
d = 40
while b == 20:
print(b)
...
# no empty line before
e = 50 # one empty line before

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
use std::cmp::Ordering;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, Comprehension, Expr, MatchCase, ModModule, Parameters};
use ruff_python_trivia::{
find_only_token_in_range, indentation_at_offset, BackwardsTokenizer, CommentRanges,
@@ -347,9 +347,9 @@ fn handle_end_of_line_comment_around_body<'a>(
// ```
// The first earlier branch filters out ambiguities e.g. around try-except-finally.
if let Some(preceding) = comment.preceding_node() {
if let Some(last_child) = preceding.last_child_in_body() {
if let Some(last_child) = last_child_in_body(preceding) {
let innermost_child =
std::iter::successors(Some(last_child), AnyNodeRef::last_child_in_body)
std::iter::successors(Some(last_child), |parent| last_child_in_body(*parent))
.last()
.unwrap_or(last_child);
return CommentPlacement::trailing(innermost_child, comment);
@@ -670,7 +670,7 @@ fn handle_own_line_comment_after_branch<'a>(
preceding: AnyNodeRef<'a>,
locator: &Locator,
) -> CommentPlacement<'a> {
let Some(last_child) = preceding.last_child_in_body() else {
let Some(last_child) = last_child_in_body(preceding) else {
return CommentPlacement::Default(comment);
};
@@ -734,7 +734,7 @@ fn handle_own_line_comment_after_branch<'a>(
return CommentPlacement::trailing(last_child_in_parent, comment);
}
Ordering::Greater => {
if let Some(nested_child) = last_child_in_parent.last_child_in_body() {
if let Some(nested_child) = last_child_in_body(last_child_in_parent) {
// The comment belongs to the inner block.
parent = Some(last_child_in_parent);
last_child_in_parent = nested_child;
@@ -1878,7 +1878,8 @@ fn handle_lambda_comment<'a>(
CommentPlacement::Default(comment)
}
/// Move comment between a unary op and its operand before the unary op by marking them as trailing.
/// Attach trailing end-of-line comments on the operator as dangling comments on the enclosing
/// node.
///
/// For example, given:
/// ```python
@@ -1895,27 +1896,26 @@ fn handle_unary_op_comment<'a>(
unary_op: &'a ast::ExprUnaryOp,
locator: &Locator,
) -> CommentPlacement<'a> {
let mut tokenizer = SimpleTokenizer::new(
locator.contents(),
TextRange::new(unary_op.start(), unary_op.operand.start()),
)
.skip_trivia();
let op_token = tokenizer.next();
debug_assert!(op_token.is_some_and(|token| matches!(
token.kind,
SimpleTokenKind::Tilde
| SimpleTokenKind::Not
| SimpleTokenKind::Plus
| SimpleTokenKind::Minus
)));
let up_to = tokenizer
.find(|token| token.kind == SimpleTokenKind::LParen)
.map_or(unary_op.operand.start(), |lparen| lparen.start());
if comment.end() < up_to {
CommentPlacement::leading(unary_op, comment)
} else {
CommentPlacement::Default(comment)
if comment.line_position().is_own_line() {
return CommentPlacement::Default(comment);
}
if comment.start() > unary_op.operand.start() {
return CommentPlacement::Default(comment);
}
let tokenizer = SimpleTokenizer::new(
locator.contents(),
TextRange::new(comment.start(), unary_op.operand.start()),
);
if tokenizer
.skip_trivia()
.any(|token| token.kind == SimpleTokenKind::LParen)
{
return CommentPlacement::Default(comment);
}
CommentPlacement::dangling(comment.enclosing_node(), comment)
}
/// Attach an end-of-line comment immediately following an open bracket as a dangling comment on
@@ -2176,6 +2176,65 @@ where
right.is_some_and(|right| left.ptr_eq(right.into()))
}
/// The last child of the last branch, if the node has multiple branches.
fn last_child_in_body(node: AnyNodeRef) -> Option<AnyNodeRef> {
let body = match node {
AnyNodeRef::StmtFunctionDef(ast::StmtFunctionDef { body, .. })
| AnyNodeRef::StmtClassDef(ast::StmtClassDef { body, .. })
| AnyNodeRef::StmtWith(ast::StmtWith { body, .. })
| AnyNodeRef::MatchCase(MatchCase { body, .. })
| AnyNodeRef::ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler {
body, ..
})
| AnyNodeRef::ElifElseClause(ast::ElifElseClause { body, .. }) => body,
AnyNodeRef::StmtIf(ast::StmtIf {
body,
elif_else_clauses,
..
}) => elif_else_clauses.last().map_or(body, |clause| &clause.body),
AnyNodeRef::StmtFor(ast::StmtFor { body, orelse, .. })
| AnyNodeRef::StmtWhile(ast::StmtWhile { body, orelse, .. }) => {
if orelse.is_empty() {
body
} else {
orelse
}
}
AnyNodeRef::StmtMatch(ast::StmtMatch { cases, .. }) => {
return cases.last().map(AnyNodeRef::from);
}
AnyNodeRef::StmtTry(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
if finalbody.is_empty() {
if orelse.is_empty() {
if handlers.is_empty() {
body
} else {
return handlers.last().map(AnyNodeRef::from);
}
} else {
orelse
}
} else {
finalbody
}
}
// Not a node that contains an indented child node.
_ => return None,
};
body.last().map(AnyNodeRef::from)
}
/// Returns `true` if `statement` is the first statement in an alternate `body` (e.g. the else of an if statement)
fn is_first_statement_in_alternate_body(statement: AnyNodeRef, has_body: AnyNodeRef) -> bool {
match has_body {

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