Compare commits
25 Commits
zanie/ecos
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22cf451d51 | ||
|
|
ec1be60dcb | ||
|
|
a327b4da87 | ||
|
|
cdc5e2fb58 | ||
|
|
b5d3caf033 | ||
|
|
8f9753f58e | ||
|
|
67b043482a | ||
|
|
693f957b90 | ||
|
|
a85ed309ea | ||
|
|
2e225d7538 | ||
|
|
4786abac7a | ||
|
|
46d5db56cc | ||
|
|
2729c4cacd | ||
|
|
b2d1fcf7b2 | ||
|
|
78d172aad7 | ||
|
|
13d6c8237a | ||
|
|
51aa73f405 | ||
|
|
0c3123e07e | ||
|
|
dda4ceda71 | ||
|
|
195c000f5a | ||
|
|
a62c735f9e | ||
|
|
94b4bb0f57 | ||
|
|
fe485d791c | ||
|
|
d685107638 | ||
|
|
d85950ce5a |
@@ -13,3 +13,8 @@ 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
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,11 +1,60 @@
|
||||
# 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
8
Cargo.lock
generated
@@ -810,7 +810,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -2051,7 +2051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2188,7 +2188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.1",
|
||||
@@ -2438,7 +2438,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
@@ -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.0
|
||||
rev: v0.1.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -237,9 +237,8 @@ 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 `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
|
||||
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
|
||||
[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
|
||||
|
||||
8
assets/badge/format.json
Normal file
8
assets/badge/format.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -17,8 +17,8 @@ use ruff_linter::settings::DEFAULT_SELECTORS;
|
||||
use ruff_linter::warn_user;
|
||||
use ruff_workspace::options::{
|
||||
Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
|
||||
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintOptions,
|
||||
McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
|
||||
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintCommonOptions,
|
||||
LintOptions, McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
|
||||
};
|
||||
use ruff_workspace::pyproject::Pyproject;
|
||||
|
||||
@@ -99,7 +99,7 @@ pub(crate) fn convert(
|
||||
|
||||
// Parse each supported option.
|
||||
let mut options = Options::default();
|
||||
let mut lint_options = LintOptions::default();
|
||||
let mut lint_options = LintCommonOptions::default();
|
||||
let mut flake8_annotations = Flake8AnnotationsOptions::default();
|
||||
let mut flake8_bugbear = Flake8BugbearOptions::default();
|
||||
let mut flake8_builtins = Flake8BuiltinsOptions::default();
|
||||
@@ -433,8 +433,11 @@ pub(crate) fn convert(
|
||||
}
|
||||
}
|
||||
|
||||
if lint_options != LintOptions::default() {
|
||||
options.lint = Some(lint_options);
|
||||
if lint_options != LintCommonOptions::default() {
|
||||
options.lint = Some(LintOptions {
|
||||
common: lint_options,
|
||||
..LintOptions::default()
|
||||
});
|
||||
}
|
||||
|
||||
// Create the pyproject.toml.
|
||||
@@ -465,7 +468,9 @@ mod tests {
|
||||
use ruff_linter::rules::flake8_quotes;
|
||||
use ruff_linter::rules::pydocstyle::settings::Convention;
|
||||
use ruff_linter::settings::types::PythonVersion;
|
||||
use ruff_workspace::options::{Flake8QuotesOptions, LintOptions, Options, PydocstyleOptions};
|
||||
use ruff_workspace::options::{
|
||||
Flake8QuotesOptions, LintCommonOptions, LintOptions, Options, PydocstyleOptions,
|
||||
};
|
||||
use ruff_workspace::pyproject::Pyproject;
|
||||
|
||||
use crate::converter::DEFAULT_SELECTORS;
|
||||
@@ -475,8 +480,8 @@ mod tests {
|
||||
use super::super::plugin::Plugin;
|
||||
use super::convert;
|
||||
|
||||
fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintOptions {
|
||||
LintOptions {
|
||||
fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintCommonOptions {
|
||||
LintCommonOptions {
|
||||
ignore: Some(vec![]),
|
||||
select: Some(
|
||||
DEFAULT_SELECTORS
|
||||
@@ -486,7 +491,7 @@ mod tests {
|
||||
.sorted_by_key(RuleSelector::prefix_and_code)
|
||||
.collect(),
|
||||
),
|
||||
..LintOptions::default()
|
||||
..LintCommonOptions::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +503,10 @@ mod tests {
|
||||
None,
|
||||
);
|
||||
let expected = Pyproject::new(Options {
|
||||
lint: Some(lint_default_options([])),
|
||||
lint: Some(LintOptions {
|
||||
common: lint_default_options([]),
|
||||
..LintOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -516,7 +524,10 @@ mod tests {
|
||||
);
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: Some(LineLength::try_from(100).unwrap()),
|
||||
lint: Some(lint_default_options([])),
|
||||
lint: Some(LintOptions {
|
||||
common: lint_default_options([]),
|
||||
..LintOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -534,7 +545,10 @@ mod tests {
|
||||
);
|
||||
let expected = Pyproject::new(Options {
|
||||
line_length: Some(LineLength::try_from(100).unwrap()),
|
||||
lint: Some(lint_default_options([])),
|
||||
lint: Some(LintOptions {
|
||||
common: lint_default_options([]),
|
||||
..LintOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -551,7 +565,10 @@ mod tests {
|
||||
Some(vec![]),
|
||||
);
|
||||
let expected = Pyproject::new(Options {
|
||||
lint: Some(lint_default_options([])),
|
||||
lint: Some(LintOptions {
|
||||
common: lint_default_options([]),
|
||||
..LintOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
@@ -569,13 +586,16 @@ mod tests {
|
||||
);
|
||||
let expected = Pyproject::new(Options {
|
||||
lint: Some(LintOptions {
|
||||
flake8_quotes: Some(Flake8QuotesOptions {
|
||||
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
|
||||
multiline_quotes: None,
|
||||
docstring_quotes: None,
|
||||
avoid_escape: None,
|
||||
}),
|
||||
..lint_default_options([])
|
||||
common: LintCommonOptions {
|
||||
flake8_quotes: Some(Flake8QuotesOptions {
|
||||
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
|
||||
multiline_quotes: None,
|
||||
docstring_quotes: None,
|
||||
avoid_escape: None,
|
||||
}),
|
||||
..lint_default_options([])
|
||||
},
|
||||
..LintOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
@@ -597,12 +617,15 @@ mod tests {
|
||||
);
|
||||
let expected = Pyproject::new(Options {
|
||||
lint: Some(LintOptions {
|
||||
pydocstyle: Some(PydocstyleOptions {
|
||||
convention: Some(Convention::Numpy),
|
||||
ignore_decorators: None,
|
||||
property_decorators: None,
|
||||
}),
|
||||
..lint_default_options([Linter::Pydocstyle.into()])
|
||||
common: LintCommonOptions {
|
||||
pydocstyle: Some(PydocstyleOptions {
|
||||
convention: Some(Convention::Numpy),
|
||||
ignore_decorators: None,
|
||||
property_decorators: None,
|
||||
}),
|
||||
..lint_default_options([Linter::Pydocstyle.into()])
|
||||
},
|
||||
..LintOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
@@ -621,13 +644,16 @@ mod tests {
|
||||
);
|
||||
let expected = Pyproject::new(Options {
|
||||
lint: Some(LintOptions {
|
||||
flake8_quotes: Some(Flake8QuotesOptions {
|
||||
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
|
||||
multiline_quotes: None,
|
||||
docstring_quotes: None,
|
||||
avoid_escape: None,
|
||||
}),
|
||||
..lint_default_options([Linter::Flake8Quotes.into()])
|
||||
common: LintCommonOptions {
|
||||
flake8_quotes: Some(Flake8QuotesOptions {
|
||||
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
|
||||
multiline_quotes: None,
|
||||
docstring_quotes: None,
|
||||
avoid_escape: None,
|
||||
}),
|
||||
..lint_default_options([Linter::Flake8Quotes.into()])
|
||||
},
|
||||
..LintOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
@@ -648,7 +674,10 @@ mod tests {
|
||||
);
|
||||
let expected = Pyproject::new(Options {
|
||||
target_version: Some(PythonVersion::Py38),
|
||||
lint: Some(lint_default_options([])),
|
||||
lint: Some(LintOptions {
|
||||
common: lint_default_options([]),
|
||||
..LintOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
1
crates/ruff_cli/resources/test/fixtures/formatted.py
vendored
Normal file
1
crates/ruff_cli/resources/test/fixtures/formatted.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
print("All formatted!")
|
||||
37
crates/ruff_cli/resources/test/fixtures/unformatted.ipynb
vendored
Normal file
37
crates/ruff_cli/resources/test/fixtures/unformatted.ipynb
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
3
crates/ruff_cli/resources/test/fixtures/unformatted.py
vendored
Normal file
3
crates/ruff_cli/resources/test/fixtures/unformatted.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
x = 1
|
||||
y=2
|
||||
z = 3
|
||||
@@ -353,6 +353,10 @@ 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>,
|
||||
@@ -366,6 +370,15 @@ pub struct FormatCommand {
|
||||
respect_gitignore: bool,
|
||||
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
|
||||
no_respect_gitignore: bool,
|
||||
/// List of paths, used to omit files and/or directories from analysis.
|
||||
#[arg(
|
||||
long,
|
||||
value_delimiter = ',',
|
||||
value_name = "FILE_PATTERN",
|
||||
help_heading = "File selection"
|
||||
)]
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
|
||||
/// Enforce exclusions, even for paths passed to Ruff directly on the command-line.
|
||||
/// Use `--no-force-exclude` to disable.
|
||||
#[arg(
|
||||
@@ -385,10 +398,12 @@ pub struct FormatCommand {
|
||||
/// The name of the file when passing it through stdin.
|
||||
#[arg(long, help_heading = "Miscellaneous")]
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
|
||||
/// Enable preview mode; checks will include unstable rules and fixes.
|
||||
/// The minimum Python version that should be supported.
|
||||
#[arg(long, value_enum)]
|
||||
pub target_version: Option<PythonVersion>,
|
||||
/// Enable preview mode; enables unstable formatting.
|
||||
/// Use `--no-preview` to disable.
|
||||
#[arg(long, overrides_with("no_preview"), hide = true)]
|
||||
#[arg(long, overrides_with("no_preview"))]
|
||||
preview: bool,
|
||||
#[clap(long, overrides_with("preview"), hide = true)]
|
||||
no_preview: bool,
|
||||
@@ -511,6 +526,7 @@ impl FormatCommand {
|
||||
(
|
||||
FormatArguments {
|
||||
check: self.check,
|
||||
diff: self.diff,
|
||||
config: self.config,
|
||||
files: self.files,
|
||||
isolated: self.isolated,
|
||||
@@ -522,8 +538,10 @@ 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()
|
||||
},
|
||||
@@ -567,6 +585,7 @@ 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,
|
||||
@@ -658,6 +677,8 @@ impl ConfigurationTransformer for CliOverrides {
|
||||
}
|
||||
if let Some(preview) = &self.preview {
|
||||
config.preview = Some(*preview);
|
||||
config.lint.preview = Some(*preview);
|
||||
config.format.preview = Some(*preview);
|
||||
}
|
||||
if let Some(per_file_ignores) = &self.per_file_ignores {
|
||||
config.lint.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));
|
||||
|
||||
@@ -10,7 +10,7 @@ use ruff_linter::linter::add_noqa_to_path;
|
||||
use ruff_linter::source_kind::SourceKind;
|
||||
use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(crate) fn add_noqa(
|
||||
&paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(ignore::DirEntry::path)
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
pyproject_config,
|
||||
);
|
||||
@@ -45,14 +45,15 @@ pub(crate) fn add_noqa(
|
||||
let modifications: usize = paths
|
||||
.par_iter()
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
.filter_map(|resolved_file| {
|
||||
let SourceType::Python(source_type @ (PySourceType::Python | PySourceType::Stub)) =
|
||||
SourceType::from(path)
|
||||
SourceType::from(resolved_file.path())
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let package = path
|
||||
let path = resolved_file.path();
|
||||
let package = resolved_file
|
||||
.path()
|
||||
.parent()
|
||||
.and_then(|parent| package_roots.get(parent))
|
||||
.and_then(|package| *package);
|
||||
|
||||
@@ -22,7 +22,10 @@ use ruff_linter::{fs, warn_user_once, IOError};
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, PyprojectDiscoveryStrategy};
|
||||
use ruff_workspace::resolver::{
|
||||
match_exclusion, python_files_in_path, PyprojectConfig, PyprojectDiscoveryStrategy,
|
||||
ResolvedFile,
|
||||
};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::cache::{self, Cache};
|
||||
@@ -42,8 +45,7 @@ pub(crate) fn check(
|
||||
// Collect all the Python files to check.
|
||||
let start = Instant::now();
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
||||
let duration = start.elapsed();
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
debug!("Identified files to lint in: {:?}", start.elapsed());
|
||||
|
||||
if paths.is_empty() {
|
||||
warn_user_once!("No Python files found under the given path(s)");
|
||||
@@ -77,7 +79,7 @@ pub(crate) fn check(
|
||||
&paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(ignore::DirEntry::path)
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
pyproject_config,
|
||||
);
|
||||
@@ -98,95 +100,114 @@ pub(crate) fn check(
|
||||
});
|
||||
|
||||
let start = Instant::now();
|
||||
let mut diagnostics: Diagnostics = paths
|
||||
.par_iter()
|
||||
.map(|entry| {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
let path = entry.path();
|
||||
let package = path
|
||||
.parent()
|
||||
.and_then(|parent| package_roots.get(parent))
|
||||
.and_then(|package| *package);
|
||||
let diagnostics_per_file = paths.par_iter().filter_map(|resolved_file| {
|
||||
let result = match resolved_file {
|
||||
Ok(resolved_file) => {
|
||||
let path = resolved_file.path();
|
||||
let package = path
|
||||
.parent()
|
||||
.and_then(|parent| package_roots.get(parent))
|
||||
.and_then(|package| *package);
|
||||
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
|
||||
let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path));
|
||||
let cache = caches.as_ref().and_then(|caches| {
|
||||
if let Some(cache) = caches.get(&cache_root) {
|
||||
Some(cache)
|
||||
} else {
|
||||
debug!("No cache found for {}", cache_root.display());
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
lint_path(
|
||||
path,
|
||||
package,
|
||||
&settings.linter,
|
||||
cache,
|
||||
noqa,
|
||||
fix_mode,
|
||||
unsafe_fixes,
|
||||
if !resolved_file.is_root()
|
||||
&& match_exclusion(
|
||||
resolved_file.path(),
|
||||
resolved_file.file_name(),
|
||||
&settings.linter.exclude,
|
||||
)
|
||||
.map_err(|e| {
|
||||
(Some(path.to_owned()), {
|
||||
let mut error = e.to_string();
|
||||
for cause in e.chain() {
|
||||
write!(&mut error, "\n Cause: {cause}").unwrap();
|
||||
}
|
||||
error
|
||||
})
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Err(e) => Err((
|
||||
if let Error::WithPath { path, .. } = e {
|
||||
Some(path.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
e.io_error()
|
||||
.map_or_else(|| e.to_string(), io::Error::to_string),
|
||||
)),
|
||||
}
|
||||
.unwrap_or_else(|(path, message)| {
|
||||
if let Some(path) = &path {
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if settings.linter.rules.enabled(Rule::IOError) {
|
||||
let dummy =
|
||||
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
|
||||
|
||||
Diagnostics::new(
|
||||
vec![Message::from_diagnostic(
|
||||
Diagnostic::new(IOError { message }, TextRange::default()),
|
||||
dummy,
|
||||
TextSize::default(),
|
||||
)],
|
||||
ImportMap::default(),
|
||||
FxHashMap::default(),
|
||||
)
|
||||
let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path));
|
||||
let cache = caches.as_ref().and_then(|caches| {
|
||||
if let Some(cache) = caches.get(&cache_root) {
|
||||
Some(cache)
|
||||
} else {
|
||||
warn!(
|
||||
"{}{}{} {message}",
|
||||
"Failed to lint ".bold(),
|
||||
fs::relativize_path(path).bold(),
|
||||
":".bold()
|
||||
);
|
||||
Diagnostics::default()
|
||||
debug!("No cache found for {}", cache_root.display());
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
lint_path(
|
||||
path,
|
||||
package,
|
||||
&settings.linter,
|
||||
cache,
|
||||
noqa,
|
||||
fix_mode,
|
||||
unsafe_fixes,
|
||||
)
|
||||
.map_err(|e| {
|
||||
(Some(path.to_path_buf()), {
|
||||
let mut error = e.to_string();
|
||||
for cause in e.chain() {
|
||||
write!(&mut error, "\n Cause: {cause}").unwrap();
|
||||
}
|
||||
error
|
||||
})
|
||||
})
|
||||
}
|
||||
Err(e) => Err((
|
||||
if let Error::WithPath { path, .. } = e {
|
||||
Some(path.clone())
|
||||
} else {
|
||||
warn!("{} {message}", "Encountered error:".bold());
|
||||
None
|
||||
},
|
||||
e.io_error()
|
||||
.map_or_else(|| e.to_string(), io::Error::to_string),
|
||||
)),
|
||||
};
|
||||
|
||||
Some(result.unwrap_or_else(|(path, message)| {
|
||||
if let Some(path) = &path {
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
if settings.linter.rules.enabled(Rule::IOError) {
|
||||
let dummy =
|
||||
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
|
||||
|
||||
Diagnostics::new(
|
||||
vec![Message::from_diagnostic(
|
||||
Diagnostic::new(IOError { message }, TextRange::default()),
|
||||
dummy,
|
||||
TextSize::default(),
|
||||
)],
|
||||
ImportMap::default(),
|
||||
FxHashMap::default(),
|
||||
)
|
||||
} else {
|
||||
warn!(
|
||||
"{}{}{} {message}",
|
||||
"Failed to lint ".bold(),
|
||||
fs::relativize_path(path).bold(),
|
||||
":".bold()
|
||||
);
|
||||
Diagnostics::default()
|
||||
}
|
||||
})
|
||||
})
|
||||
.reduce(Diagnostics::default, |mut acc, item| {
|
||||
acc += item;
|
||||
acc
|
||||
});
|
||||
} else {
|
||||
warn!("{} {message}", "Encountered error:".bold());
|
||||
Diagnostics::default()
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
diagnostics.messages.sort();
|
||||
// Aggregate the diagnostics of all checked files and count the checked files.
|
||||
// This can't be a regular for loop because we use `par_iter`.
|
||||
let (mut all_diagnostics, checked_files) = diagnostics_per_file
|
||||
.fold(
|
||||
|| (Diagnostics::default(), 0u64),
|
||||
|(all_diagnostics, checked_files), file_diagnostics| {
|
||||
(all_diagnostics + file_diagnostics, checked_files + 1)
|
||||
},
|
||||
)
|
||||
.reduce(
|
||||
|| (Diagnostics::default(), 0u64),
|
||||
|a, b| (a.0 + b.0, a.1 + b.1),
|
||||
);
|
||||
|
||||
all_diagnostics.messages.sort();
|
||||
|
||||
// Store the caches.
|
||||
if let Some(caches) = caches {
|
||||
@@ -196,9 +217,9 @@ pub(crate) fn check(
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
debug!("Checked {:?} files in: {:?}", paths.len(), duration);
|
||||
debug!("Checked {:?} files in: {:?}", checked_files, duration);
|
||||
|
||||
Ok(diagnostics)
|
||||
Ok(all_diagnostics)
|
||||
}
|
||||
|
||||
/// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::Result;
|
||||
|
||||
use ruff_linter::packaging;
|
||||
use ruff_linter::settings::flags;
|
||||
use ruff_workspace::resolver::{python_file_at_path, PyprojectConfig};
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
use crate::diagnostics::{lint_stdin, Diagnostics};
|
||||
@@ -22,6 +22,14 @@ pub(crate) fn check_stdin(
|
||||
if !python_file_at_path(filename, pyproject_config, overrides)? {
|
||||
return Ok(Diagnostics::default());
|
||||
}
|
||||
|
||||
let lint_settings = &pyproject_config.settings.linter;
|
||||
if filename
|
||||
.file_name()
|
||||
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
|
||||
{
|
||||
return Ok(Diagnostics::default());
|
||||
}
|
||||
}
|
||||
let package_root = filename.and_then(Path::parent).and_then(|path| {
|
||||
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
|
||||
@@ -9,6 +11,7 @@ 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;
|
||||
|
||||
@@ -20,7 +23,7 @@ use ruff_linter::warn_user_once;
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_python_formatter::{format_module_source, FormatModuleError};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_workspace::resolver::python_files_in_path;
|
||||
use ruff_workspace::resolver::{match_exclusion, python_files_in_path};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
@@ -34,6 +37,20 @@ 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.
|
||||
@@ -48,11 +65,7 @@ pub(crate) fn format(
|
||||
overrides,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
let mode = if cli.check {
|
||||
FormatMode::Check
|
||||
} else {
|
||||
FormatMode::Write
|
||||
};
|
||||
let mode = FormatMode::from_cli(cli);
|
||||
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, overrides)?;
|
||||
|
||||
if paths.is_empty() {
|
||||
@@ -61,26 +74,61 @@ pub(crate) fn format(
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
let (results, errors): (Vec<_>, Vec<_>) = paths
|
||||
let (mut results, mut errors): (Vec<_>, Vec<_>) = paths
|
||||
.into_par_iter()
|
||||
.filter_map(|entry| {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
let path = entry.into_path();
|
||||
|
||||
Ok(resolved_file) => {
|
||||
let path = resolved_file.path();
|
||||
let SourceType::Python(source_type) = SourceType::from(&path) else {
|
||||
// Ignore any non-Python files.
|
||||
return None;
|
||||
};
|
||||
|
||||
let resolved_settings = resolver.resolve(&path, &pyproject_config);
|
||||
let resolved_settings = resolver.resolve(path, &pyproject_config);
|
||||
|
||||
// Ignore files that are excluded from formatting
|
||||
if !resolved_file.is_root()
|
||||
&& match_exclusion(
|
||||
path,
|
||||
resolved_file.file_name(),
|
||||
&resolved_settings.formatter.exclude,
|
||||
)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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,
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
match catch_unwind(|| {
|
||||
format_path(&path, &resolved_settings.formatter, source_type, mode)
|
||||
format_path(
|
||||
path,
|
||||
&resolved_settings.formatter,
|
||||
&unformatted,
|
||||
source_type,
|
||||
mode,
|
||||
)
|
||||
}) {
|
||||
Ok(inner) => inner.map(|result| FormatPathResult { path, result }),
|
||||
Err(error) => Err(FormatCommandError::Panic(Some(path), error)),
|
||||
Ok(inner) => inner.map(|result| FormatPathResult {
|
||||
path: resolved_file.into_path(),
|
||||
unformatted,
|
||||
result,
|
||||
}),
|
||||
Err(error) => Err(FormatCommandError::Panic(
|
||||
Some(resolved_file.into_path()),
|
||||
error,
|
||||
)),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -93,6 +141,27 @@ 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(),
|
||||
@@ -104,13 +173,20 @@ pub(crate) fn format(
|
||||
error!("{error}");
|
||||
}
|
||||
|
||||
let summary = FormatSummary::new(results.as_slice(), mode);
|
||||
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())?;
|
||||
}
|
||||
|
||||
// Report on the formatting changes.
|
||||
if log_level >= LogLevel::Default {
|
||||
#[allow(clippy::print_stdout)]
|
||||
{
|
||||
println!("{summary}");
|
||||
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())?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +198,9 @@ pub(crate) fn format(
|
||||
Ok(ExitStatus::Error)
|
||||
}
|
||||
}
|
||||
FormatMode::Check => {
|
||||
FormatMode::Check | FormatMode::Diff => {
|
||||
if errors.is_empty() {
|
||||
if summary.any_formatted() {
|
||||
if results.any_formatted() {
|
||||
Ok(ExitStatus::Failure)
|
||||
} else {
|
||||
Ok(ExitStatus::Success)
|
||||
@@ -137,61 +213,47 @@ pub(crate) fn format(
|
||||
}
|
||||
|
||||
/// Format the file at the given [`Path`].
|
||||
#[tracing::instrument(skip_all, fields(path = %path.display()))]
|
||||
#[tracing::instrument(level="debug", skip_all, fields(path = %path.display()))]
|
||||
fn format_path(
|
||||
path: &Path,
|
||||
settings: &FormatterSettings,
|
||||
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.
|
||||
match format_source(source_kind, source_type, Some(path), settings)? {
|
||||
FormattedSource::Formatted(formatted) => {
|
||||
if mode.is_write() {
|
||||
let format_result = match format_source(unformatted, source_type, Some(path), settings)? {
|
||||
FormattedSource::Formatted(formatted) => match mode {
|
||||
FormatMode::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
|
||||
}
|
||||
Ok(FormatResult::Formatted)
|
||||
}
|
||||
FormattedSource::Unchanged(_) => Ok(FormatResult::Unchanged),
|
||||
}
|
||||
FormatMode::Check => FormatResult::Formatted,
|
||||
FormatMode::Diff => FormatResult::Diff(formatted),
|
||||
},
|
||||
FormattedSource::Unchanged => FormatResult::Unchanged,
|
||||
};
|
||||
Ok(format_result)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum FormattedSource {
|
||||
/// The source was formatted, and the [`SourceKind`] contains the transformed source code.
|
||||
Formatted(SourceKind),
|
||||
/// The source was unchanged, and the [`SourceKind`] contains the original source code.
|
||||
Unchanged(SourceKind),
|
||||
/// The source was unchanged.
|
||||
Unchanged,
|
||||
}
|
||||
|
||||
impl From<FormattedSource> for FormatResult {
|
||||
fn from(value: FormattedSource) -> Self {
|
||||
match value {
|
||||
FormattedSource::Formatted(_) => FormatResult::Formatted,
|
||||
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,
|
||||
FormattedSource::Unchanged => FormatResult::Unchanged,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,30 +261,28 @@ impl FormattedSource {
|
||||
/// 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(SourceKind::Python(unformatted)))
|
||||
Ok(FormattedSource::Unchanged)
|
||||
} else {
|
||||
Ok(FormattedSource::Formatted(SourceKind::Python(formatted)))
|
||||
}
|
||||
}
|
||||
SourceKind::IpyNotebook(notebook) => {
|
||||
if !notebook.is_python_notebook() {
|
||||
return Ok(FormattedSource::Unchanged(SourceKind::IpyNotebook(
|
||||
notebook,
|
||||
)));
|
||||
return Ok(FormattedSource::Unchanged);
|
||||
}
|
||||
|
||||
let options = settings.to_format_options(source_type, notebook.source_code());
|
||||
@@ -270,9 +330,7 @@ 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(SourceKind::IpyNotebook(
|
||||
notebook,
|
||||
)));
|
||||
return Ok(FormattedSource::Unchanged);
|
||||
};
|
||||
|
||||
// Add the remaining content.
|
||||
@@ -280,21 +338,23 @@ pub(crate) fn format_source(
|
||||
output.push_str(slice);
|
||||
|
||||
// Update the notebook.
|
||||
let mut notebook = notebook.clone();
|
||||
notebook.update(&source_map, output);
|
||||
let mut formatted = notebook.clone();
|
||||
formatted.update(&source_map, output);
|
||||
|
||||
Ok(FormattedSource::Formatted(SourceKind::IpyNotebook(
|
||||
notebook,
|
||||
formatted,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of an individual formatting operation.
|
||||
#[derive(Debug, Clone, Copy, is_macro::Is)]
|
||||
#[derive(Debug, Clone, 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,
|
||||
}
|
||||
@@ -303,38 +363,55 @@ pub(crate) enum FormatResult {
|
||||
#[derive(Debug)]
|
||||
struct FormatPathResult {
|
||||
path: PathBuf,
|
||||
unformatted: SourceKind,
|
||||
result: FormatResult,
|
||||
}
|
||||
|
||||
/// A summary of the formatting results.
|
||||
/// The results of formatting a set of files
|
||||
#[derive(Debug)]
|
||||
struct FormatSummary<'a> {
|
||||
struct FormatResults<'a> {
|
||||
/// The individual formatting results.
|
||||
results: &'a [FormatPathResult],
|
||||
/// The format mode that was used.
|
||||
mode: FormatMode,
|
||||
}
|
||||
|
||||
impl<'a> FormatSummary<'a> {
|
||||
impl<'a> FormatResults<'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| result.result.is_formatted())
|
||||
self.results.iter().any(|result| match result.result {
|
||||
FormatResult::Formatted | FormatResult::Diff { .. } => true,
|
||||
FormatResult::Unchanged => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FormatSummary<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
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<()> {
|
||||
// Compute the number of changed and unchanged files.
|
||||
let mut formatted = 0u32;
|
||||
let mut changed = 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() {
|
||||
@@ -344,39 +421,42 @@ impl Display for FormatSummary<'_> {
|
||||
fs::relativize_path(&result.path).bold()
|
||||
)?;
|
||||
}
|
||||
formatted += 1;
|
||||
changed += 1;
|
||||
}
|
||||
FormatResult::Unchanged => unchanged += 1,
|
||||
FormatResult::Diff(_) => {
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write out a summary of the formatting results.
|
||||
if formatted > 0 && unchanged > 0 {
|
||||
write!(
|
||||
if changed > 0 && unchanged > 0 {
|
||||
writeln!(
|
||||
f,
|
||||
"{} file{} {}, {} file{} left unchanged",
|
||||
formatted,
|
||||
if formatted == 1 { "" } else { "s" },
|
||||
changed,
|
||||
if changed == 1 { "" } else { "s" },
|
||||
match self.mode {
|
||||
FormatMode::Write => "reformatted",
|
||||
FormatMode::Check => "would be reformatted",
|
||||
FormatMode::Check | FormatMode::Diff => "would be reformatted",
|
||||
},
|
||||
unchanged,
|
||||
if unchanged == 1 { "" } else { "s" },
|
||||
)
|
||||
} else if formatted > 0 {
|
||||
write!(
|
||||
} else if changed > 0 {
|
||||
writeln!(
|
||||
f,
|
||||
"{} file{} {}",
|
||||
formatted,
|
||||
if formatted == 1 { "" } else { "s" },
|
||||
changed,
|
||||
if changed == 1 { "" } else { "s" },
|
||||
match self.mode {
|
||||
FormatMode::Write => "reformatted",
|
||||
FormatMode::Check => "would be reformatted",
|
||||
FormatMode::Check | FormatMode::Diff => "would be reformatted",
|
||||
}
|
||||
)
|
||||
} else if unchanged > 0 {
|
||||
write!(
|
||||
writeln!(
|
||||
f,
|
||||
"{} file{} left unchanged",
|
||||
unchanged,
|
||||
|
||||
@@ -3,14 +3,18 @@ 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::python_file_at_path;
|
||||
use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
|
||||
use ruff_workspace::FormatterSettings;
|
||||
|
||||
use crate::args::{CliOverrides, FormatArguments};
|
||||
use crate::commands::format::{format_source, FormatCommandError, FormatMode, FormatResult};
|
||||
use crate::commands::format::{
|
||||
format_source, FormatCommandError, FormatMode, FormatResult, FormattedSource,
|
||||
};
|
||||
use crate::resolve::resolve;
|
||||
use crate::stdin::read_from_stdin;
|
||||
use crate::ExitStatus;
|
||||
@@ -23,16 +27,20 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
overrides,
|
||||
cli.stdin_filename.as_deref(),
|
||||
)?;
|
||||
let mode = if cli.check {
|
||||
FormatMode::Check
|
||||
} else {
|
||||
FormatMode::Write
|
||||
};
|
||||
let mode = FormatMode::from_cli(cli);
|
||||
|
||||
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();
|
||||
@@ -50,7 +58,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||
) {
|
||||
Ok(result) => match mode {
|
||||
FormatMode::Write => Ok(ExitStatus::Success),
|
||||
FormatMode::Check => {
|
||||
FormatMode::Check | FormatMode::Diff => {
|
||||
if result.is_formatted() {
|
||||
Ok(ExitStatus::Failure)
|
||||
} else {
|
||||
@@ -85,15 +93,37 @@ 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)?;
|
||||
|
||||
// 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))?;
|
||||
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))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FormatResult::from(formatted))
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_linter::warn_user_once;
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
|
||||
@@ -25,12 +25,13 @@ pub(crate) fn show_files(
|
||||
}
|
||||
|
||||
// Print the list of files.
|
||||
for entry in paths
|
||||
.iter()
|
||||
for path in paths
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.sorted_by(|a, b| a.path().cmp(b.path()))
|
||||
.map(ResolvedFile::into_path)
|
||||
.sorted_unstable()
|
||||
{
|
||||
writeln!(writer, "{}", entry.path().to_string_lossy())?;
|
||||
writeln!(writer, "{}", path.to_string_lossy())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||
use anyhow::{bail, Result};
|
||||
use itertools::Itertools;
|
||||
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||
|
||||
use crate::args::CliOverrides;
|
||||
|
||||
@@ -19,16 +19,17 @@ pub(crate) fn show_settings(
|
||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
||||
|
||||
// Print the list of files.
|
||||
let Some(entry) = paths
|
||||
.iter()
|
||||
let Some(path) = paths
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.sorted_by(|a, b| a.path().cmp(b.path()))
|
||||
.map(ResolvedFile::into_path)
|
||||
.sorted_unstable()
|
||||
.next()
|
||||
else {
|
||||
bail!("No files found under the given path");
|
||||
};
|
||||
let path = entry.path();
|
||||
let settings = resolver.resolve(path, pyproject_config);
|
||||
|
||||
let settings = resolver.resolve(&path, pyproject_config);
|
||||
|
||||
writeln!(writer, "Resolved settings for: {path:?}")?;
|
||||
if let Some(settings_path) = pyproject_config.path.as_ref() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::ops::AddAssign;
|
||||
use std::ops::{Add, AddAssign};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
@@ -11,7 +11,6 @@ use anyhow::{Context, Result};
|
||||
use colored::Colorize;
|
||||
use filetime::FileTime;
|
||||
use log::{debug, error, warn};
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
@@ -20,6 +19,7 @@ use ruff_linter::logging::DisplayParseError;
|
||||
use ruff_linter::message::Message;
|
||||
use ruff_linter::pyproject_toml::lint_pyproject_toml;
|
||||
use ruff_linter::registry::AsRule;
|
||||
use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_linter::settings::{flags, LinterSettings};
|
||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::{fs, IOError, SyntaxError};
|
||||
@@ -61,7 +61,7 @@ impl FileCacheKey {
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub(crate) struct Diagnostics {
|
||||
pub(crate) messages: Vec<Message>,
|
||||
pub(crate) fixed: FxHashMap<String, FixTable>,
|
||||
pub(crate) fixed: FixMap,
|
||||
pub(crate) imports: ImportMap,
|
||||
pub(crate) notebook_indexes: FxHashMap<String, NotebookIndex>,
|
||||
}
|
||||
@@ -74,7 +74,7 @@ impl Diagnostics {
|
||||
) -> Self {
|
||||
Self {
|
||||
messages,
|
||||
fixed: FxHashMap::default(),
|
||||
fixed: FixMap::default(),
|
||||
imports,
|
||||
notebook_indexes,
|
||||
}
|
||||
@@ -142,22 +142,68 @@ impl Diagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Diagnostics {
|
||||
type Output = Diagnostics;
|
||||
|
||||
fn add(mut self, other: Self) -> Self::Output {
|
||||
self += other;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Diagnostics {
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
self.messages.extend(other.messages);
|
||||
self.imports.extend(other.imports);
|
||||
for (filename, fixed) in other.fixed {
|
||||
self.fixed += other.fixed;
|
||||
self.notebook_indexes.extend(other.notebook_indexes);
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of fixes indexed by file path.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub(crate) struct FixMap(FxHashMap<String, FixTable>);
|
||||
|
||||
impl FixMap {
|
||||
/// Returns `true` if there are no fixes in the map.
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the fixes in the map, along with the file path.
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (&String, &FixTable)> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the fixes in the map.
|
||||
pub(crate) fn values(&self) -> impl Iterator<Item = &FixTable> {
|
||||
self.0.values()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(String, FixTable)> for FixMap {
|
||||
fn from_iter<T: IntoIterator<Item = (String, FixTable)>>(iter: T) -> Self {
|
||||
Self(
|
||||
iter.into_iter()
|
||||
.filter(|(_, fixes)| !fixes.is_empty())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for FixMap {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
for (filename, fixed) in rhs.0 {
|
||||
if fixed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let fixed_in_file = self.fixed.entry(filename).or_default();
|
||||
let fixed_in_file = self.0.entry(filename).or_default();
|
||||
for (rule, count) in fixed {
|
||||
if count > 0 {
|
||||
*fixed_in_file.entry(rule).or_default() += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.notebook_indexes.extend(other.notebook_indexes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +364,7 @@ pub(crate) fn lint_path(
|
||||
|
||||
Ok(Diagnostics {
|
||||
messages,
|
||||
fixed: FxHashMap::from_iter([(fs::relativize_path(path), fixed)]),
|
||||
fixed: FixMap::from_iter([(fs::relativize_path(path), fixed)]),
|
||||
imports,
|
||||
notebook_indexes,
|
||||
})
|
||||
@@ -436,7 +482,7 @@ pub(crate) fn lint_stdin(
|
||||
|
||||
Ok(Diagnostics {
|
||||
messages,
|
||||
fixed: FxHashMap::from_iter([(
|
||||
fixed: FixMap::from_iter([(
|
||||
fs::relativize_path(path.unwrap_or_else(|| Path::new("-"))),
|
||||
fixed,
|
||||
)]),
|
||||
|
||||
@@ -7,11 +7,9 @@ use anyhow::Result;
|
||||
use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use itertools::{iterate, Itertools};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Serialize;
|
||||
|
||||
use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::linter::FixTable;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::message::{
|
||||
AzureEmitter, Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter,
|
||||
@@ -22,7 +20,7 @@ use ruff_linter::registry::{AsRule, Rule};
|
||||
use ruff_linter::settings::flags::{self};
|
||||
use ruff_linter::settings::types::{SerializationFormat, UnsafeFixes};
|
||||
|
||||
use crate::diagnostics::Diagnostics;
|
||||
use crate::diagnostics::{Diagnostics, FixMap};
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default, Debug, Copy, Clone)]
|
||||
@@ -462,7 +460,7 @@ fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableStatistics
|
||||
(!fix_mode.is_apply()) && fixables.is_some_and(FixableStatistics::any_applicable_fixes)
|
||||
}
|
||||
|
||||
fn print_fix_summary(writer: &mut dyn Write, fixed: &FxHashMap<String, FixTable>) -> Result<()> {
|
||||
fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap) -> Result<()> {
|
||||
let total = fixed
|
||||
.values()
|
||||
.map(|table| table.values().sum::<usize>())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![cfg(not(target_family = "wasm"))]
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::str;
|
||||
|
||||
@@ -13,7 +14,7 @@ const BIN_NAME: &str = "ruff";
|
||||
#[test]
|
||||
fn default_options() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["format", "--isolated"])
|
||||
.args(["format", "--isolated", "--stdin-filename", "test.py"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
@@ -50,6 +51,9 @@ fn format_options() -> Result<()> {
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
tab-size = 8
|
||||
line-length = 84
|
||||
|
||||
[format]
|
||||
indent-style = "tab"
|
||||
quote-style = "single"
|
||||
@@ -64,7 +68,7 @@ line-ending = "cr-lf"
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
def foo(arg1, arg2,):
|
||||
print("Shouldn't change quotes")
|
||||
print("Shouldn't change quotes. It exceeds the line width with the tab size 8")
|
||||
|
||||
|
||||
if condition:
|
||||
@@ -76,7 +80,9 @@ if condition:
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
def foo(arg1, arg2):
|
||||
print("Shouldn't change quotes")
|
||||
print(
|
||||
"Shouldn't change quotes. It exceeds the line width with the tab size 8"
|
||||
)
|
||||
|
||||
|
||||
if condition:
|
||||
@@ -88,6 +94,108 @@ if condition:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
extend-exclude = ["out"]
|
||||
|
||||
[format]
|
||||
exclude = ["test.py", "generated.py"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
fs::write(
|
||||
tempdir.path().join("main.py"),
|
||||
r#"
|
||||
from test import say_hy
|
||||
|
||||
if __name__ == "__main__":
|
||||
say_hy("dear Ruff contributor")
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Excluded file but passed to the CLI directly, should be formatted
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
fs::write(
|
||||
tempdir.path().join("generated.py"),
|
||||
r#"NUMBERS = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
|
||||
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
|
||||
]
|
||||
OTHER = "OTHER"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let out_dir = tempdir.path().join("out");
|
||||
fs::create_dir(&out_dir)?;
|
||||
|
||||
fs::write(out_dir.join("a.py"), "a = a")?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--check", "--config"])
|
||||
.arg(ruff_toml.file_name().unwrap())
|
||||
// Explicitly pass test.py, should be formatted regardless of it being excluded by format.exclude
|
||||
.arg(test_path.file_name().unwrap())
|
||||
// Format all other files in the directory, should respect the `exclude` and `format.exclude` options
|
||||
.arg("."), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
Would reformat: main.py
|
||||
Would reformat: test.py
|
||||
2 files would be reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude_stdin() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
extend-select = ["B", "Q"]
|
||||
|
||||
[format]
|
||||
exclude = ["generated.py"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.args(["format", "--config", &ruff_toml.file_name().unwrap().to_string_lossy(), "--stdin-filename", "generated.py", "-"])
|
||||
.pass_stdin(r#"
|
||||
from test import say_hy
|
||||
|
||||
if __name__ == '__main__':
|
||||
say_hy("dear Ruff contributor")
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_option_inheritance() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
@@ -182,3 +290,125 @@ 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.
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -1252,8 +1252,8 @@ fn diff_does_not_show_display_only_fixes_with_unsafe_fixes_enabled() {
|
||||
])
|
||||
.pass_stdin("def add_to_list(item, some_list=[]): ..."),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
@@ -1276,8 +1276,8 @@ fn diff_only_unsafe_fixes_available() {
|
||||
])
|
||||
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
|
||||
@r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -31,14 +31,15 @@ inline-quotes = "single"
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.args(["--stdin-filename", "test.py"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:5: Q000 [*] Double quotes found but single quotes preferred
|
||||
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
|
||||
-:1:19: Q000 [*] Double quotes found but single quotes preferred
|
||||
test.py:1:5: Q000 [*] Double quotes found but single quotes preferred
|
||||
test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading
|
||||
test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
|
||||
Found 3 errors.
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
|
||||
@@ -155,3 +156,117 @@ inline-quotes = "single"
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
extend-select = ["B", "Q"]
|
||||
extend-exclude = ["out"]
|
||||
|
||||
[lint]
|
||||
exclude = ["test.py", "generated.py"]
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
fs::write(
|
||||
tempdir.path().join("main.py"),
|
||||
r#"
|
||||
from test import say_hy
|
||||
|
||||
if __name__ == "__main__":
|
||||
say_hy("dear Ruff contributor")
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Excluded file but passed to the CLI directly, should be linted
|
||||
let test_path = tempdir.path().join("test.py");
|
||||
fs::write(
|
||||
&test_path,
|
||||
r#"
|
||||
def say_hy(name: str):
|
||||
print(f"Hy {name}")"#,
|
||||
)?;
|
||||
|
||||
fs::write(
|
||||
tempdir.path().join("generated.py"),
|
||||
r#"NUMBERS = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
|
||||
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
|
||||
]
|
||||
OTHER = "OTHER"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let out_dir = tempdir.path().join("out");
|
||||
fs::create_dir(&out_dir)?;
|
||||
|
||||
fs::write(out_dir.join("a.py"), r#"a = "a""#)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("check")
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
|
||||
// Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
|
||||
.arg(test_path.file_name().unwrap())
|
||||
// Lint all other files in the directory, should respect the `exclude` and `lint.exclude` options
|
||||
.arg("."), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
main.py:4:16: Q000 [*] Double quotes found but single quotes preferred
|
||||
main.py:5:12: Q000 [*] Double quotes found but single quotes preferred
|
||||
test.py:3:15: Q000 [*] Double quotes found but single quotes preferred
|
||||
Found 3 errors.
|
||||
[*] 3 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude_stdin() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
extend-select = ["B", "Q"]
|
||||
|
||||
[lint]
|
||||
exclude = ["generated.py"]
|
||||
|
||||
[lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.current_dir(tempdir.path())
|
||||
.arg("check")
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
|
||||
.args(["--stdin-filename", "generated.py"])
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
from test import say_hy
|
||||
|
||||
if __name__ == "__main__":
|
||||
say_hy("dear Ruff contributor")
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use std::{fmt, fs, io, iter};
|
||||
|
||||
use anyhow::{bail, format_err, Context, Error};
|
||||
use clap::{CommandFactory, FromArgMatches};
|
||||
use ignore::DirEntry;
|
||||
use imara_diff::intern::InternedInput;
|
||||
use imara_diff::sink::Counter;
|
||||
use imara_diff::{diff, Algorithm};
|
||||
@@ -36,14 +35,14 @@ use ruff_linter::settings::types::{FilePattern, FilePatternSet};
|
||||
use ruff_python_formatter::{
|
||||
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
|
||||
};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, Resolver};
|
||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
|
||||
|
||||
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn ruff_check_paths(
|
||||
dirs: &[PathBuf],
|
||||
) -> anyhow::Result<(
|
||||
Vec<Result<DirEntry, ignore::Error>>,
|
||||
Vec<Result<ResolvedFile, ignore::Error>>,
|
||||
Resolver,
|
||||
PyprojectConfig,
|
||||
)> {
|
||||
@@ -467,9 +466,9 @@ fn format_dev_project(
|
||||
let iter = { paths.into_par_iter() };
|
||||
#[cfg(feature = "singlethreaded")]
|
||||
let iter = { paths.into_iter() };
|
||||
iter.map(|dir_entry| {
|
||||
iter.map(|path| {
|
||||
let result = format_dir_entry(
|
||||
dir_entry,
|
||||
path,
|
||||
stability_check,
|
||||
write,
|
||||
&black_options,
|
||||
@@ -527,24 +526,20 @@ fn format_dev_project(
|
||||
|
||||
/// Error handling in between walkdir and `format_dev_file`
|
||||
fn format_dir_entry(
|
||||
dir_entry: Result<DirEntry, ignore::Error>,
|
||||
resolved_file: Result<ResolvedFile, ignore::Error>,
|
||||
stability_check: bool,
|
||||
write: bool,
|
||||
options: &BlackOptions,
|
||||
resolver: &Resolver,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
|
||||
let dir_entry = match dir_entry.context("Iterating the files in the repository failed") {
|
||||
Ok(dir_entry) => dir_entry,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let file = dir_entry.path().to_path_buf();
|
||||
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
|
||||
// For some reason it does not filter in the beginning
|
||||
if dir_entry.file_name() == "pyproject.toml" {
|
||||
return Ok((Ok(Statistics::default()), file));
|
||||
if resolved_file.file_name() == "pyproject.toml" {
|
||||
return Ok((Ok(Statistics::default()), resolved_file.into_path()));
|
||||
}
|
||||
|
||||
let path = dir_entry.path().to_path_buf();
|
||||
let path = resolved_file.into_path();
|
||||
let mut options = options.to_py_format_options(&path);
|
||||
|
||||
let settings = resolver.resolve(&path, pyproject_config);
|
||||
|
||||
@@ -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::OptionsMetadata;
|
||||
use ruff_workspace::options_base::{OptionEntry, OptionsMetadata};
|
||||
|
||||
use crate::ROOT_DIR;
|
||||
|
||||
@@ -55,7 +55,11 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
process_documentation(explanation.trim(), &mut output);
|
||||
process_documentation(
|
||||
explanation.trim(),
|
||||
&mut output,
|
||||
&rule.noqa_code().to_string(),
|
||||
);
|
||||
|
||||
let filename = PathBuf::from(ROOT_DIR)
|
||||
.join("docs")
|
||||
@@ -74,7 +78,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_documentation(documentation: &str, out: &mut String) {
|
||||
fn process_documentation(documentation: &str, out: &mut String, rule_name: &str) {
|
||||
let mut in_options = false;
|
||||
let mut after = String::new();
|
||||
|
||||
@@ -100,7 +104,17 @@ fn process_documentation(documentation: &str, out: &mut String) {
|
||||
if let Some(rest) = line.strip_prefix("- `") {
|
||||
let option = rest.trim_end().trim_end_matches('`');
|
||||
|
||||
assert!(Options::metadata().has(option), "unknown option {option}");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
let anchor = option.replace('.', "-");
|
||||
out.push_str(&format!("- [`{option}`][{option}]\n"));
|
||||
@@ -138,6 +152,7 @@ Something [`else`][other].
|
||||
|
||||
[other]: http://example.com.",
|
||||
&mut output,
|
||||
"example",
|
||||
);
|
||||
assert_eq!(
|
||||
output,
|
||||
|
||||
@@ -101,6 +101,24 @@ 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));
|
||||
|
||||
@@ -95,7 +95,7 @@ impl std::fmt::Display for IndentStyle {
|
||||
///
|
||||
/// Determines the visual width of a tab character (`\t`) and the number of
|
||||
/// spaces per indent when using [`IndentStyle::Space`].
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, CacheKey)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct IndentWidth(NonZeroU8);
|
||||
@@ -575,6 +575,10 @@ where
|
||||
context: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rule(&self) -> &R {
|
||||
&self.rule
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, R, O, C> FormatRefWithRule<'_, T, R, C>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
19
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S310.py
vendored
Normal file
19
crates/ruff_linter/resources/test/fixtures/flake8_bandit/S310.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
@@ -10,6 +10,10 @@ def double_quotes_backslash_uppercase():
|
||||
R"""Sum\\mary."""
|
||||
|
||||
|
||||
def shouldnt_add_raw_here():
|
||||
"Ruff \U000026a1"
|
||||
|
||||
|
||||
def make_unique_pod_id(pod_id: str) -> str | None:
|
||||
r"""
|
||||
Generate a unique Pod name.
|
||||
|
||||
17
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_19.py
vendored
Normal file
17
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_19.py
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Test that type parameters are considered used."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from .foo import Record as Record1
|
||||
from .bar import Record as Record2
|
||||
|
||||
type RecordCallback[R: Record1] = Callable[[R], None]
|
||||
|
||||
|
||||
def process_record[R: Record2](record: R) -> None:
|
||||
...
|
||||
5
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_20.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_20.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Test lazy evaluation of type alias values."""
|
||||
|
||||
type RecordCallback[R: Record] = Callable[[R], None]
|
||||
|
||||
from collections.abc import Callable
|
||||
10
crates/ruff_linter/resources/test/fixtures/pylint/global_at_module_level.py
vendored
Normal file
10
crates/ruff_linter/resources/test/fixtures/pylint/global_at_module_level.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
global price # W0604
|
||||
|
||||
price = 25
|
||||
|
||||
if True:
|
||||
global X # W0604
|
||||
|
||||
def no_error():
|
||||
global price
|
||||
price = 30
|
||||
@@ -21,6 +21,9 @@ 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())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::{Expr, TypeParam};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_semantic::{ScopeId, Snapshot};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub(crate) struct Deferred<'a> {
|
||||
pub(crate) scopes: Vec<ScopeId>,
|
||||
pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>,
|
||||
pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) type_param_definitions: Vec<(&'a TypeParam, Snapshot)>,
|
||||
pub(crate) type_param_definitions: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) functions: Vec<Snapshot>,
|
||||
pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) for_loops: Vec<Snapshot>,
|
||||
|
||||
@@ -582,9 +582,9 @@ where
|
||||
if let Some(type_params) = type_params {
|
||||
self.visit_type_params(type_params);
|
||||
}
|
||||
// The value in a `type` alias has annotation semantics, in that it's never
|
||||
// evaluated at runtime.
|
||||
self.visit_annotation(value);
|
||||
self.deferred
|
||||
.type_param_definitions
|
||||
.push((value, self.semantic.snapshot()));
|
||||
self.semantic.pop_scope();
|
||||
self.visit_expr(name);
|
||||
}
|
||||
@@ -1389,9 +1389,14 @@ where
|
||||
}
|
||||
}
|
||||
// Step 2: Traversal
|
||||
self.deferred
|
||||
.type_param_definitions
|
||||
.push((type_param, self.semantic.snapshot()));
|
||||
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
|
||||
bound: Some(bound), ..
|
||||
}) = type_param
|
||||
{
|
||||
self.deferred
|
||||
.type_param_definitions
|
||||
.push((bound, self.semantic.snapshot()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1766,12 +1771,9 @@ impl<'a> Checker<'a> {
|
||||
for (type_param, snapshot) in type_params {
|
||||
self.semantic.restore(snapshot);
|
||||
|
||||
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
|
||||
bound: Some(bound), ..
|
||||
}) = type_param
|
||||
{
|
||||
self.visit_annotation(bound);
|
||||
}
|
||||
self.semantic.flags |=
|
||||
SemanticModelFlags::TYPE_PARAM_DEFINITION | SemanticModelFlags::TYPE_DEFINITION;
|
||||
self.visit_expr(type_param);
|
||||
}
|
||||
}
|
||||
self.semantic.restore(snapshot);
|
||||
|
||||
@@ -264,6 +264,7 @@ 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),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
|
||||
@@ -253,3 +253,9 @@ impl From<NonZeroU8> for TabSize {
|
||||
Self(tab_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TabSize> for NonZeroU8 {
|
||||
fn from(value: TabSize) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
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.");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ 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"))]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
//! 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;
|
||||
@@ -850,10 +849,23 @@ 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
|
||||
["urllib", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener" | "Request"] |
|
||||
["urllib", "request", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener"] |
|
||||
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" | "URLopener" | "FancyURLopener"] => Some(SuspiciousURLOpenUsage.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()),
|
||||
// NonCryptographicRandom
|
||||
["random", "random" | "randrange" | "randint" | "choice" | "choices" | "uniform" | "triangular"] => Some(SuspiciousNonCryptographicRandomUsage.into()),
|
||||
// UnverifiedContext
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
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
|
||||
|
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// "text.txt".strip(".txt") # "ex"
|
||||
/// "text.txt".strip(".txt") # "e"
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,7 @@ use log::error;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::{self as ast, whitespace, Constant, ElifElseClause, Expr, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -12,6 +12,7 @@ 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};
|
||||
|
||||
@@ -107,6 +108,33 @@ 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(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use memchr::memchr_iter;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -46,18 +46,21 @@ use crate::docstrings::Docstring;
|
||||
#[violation]
|
||||
pub struct EscapeSequenceInDocstring;
|
||||
|
||||
impl Violation for EscapeSequenceInDocstring {
|
||||
impl AlwaysFixableViolation for EscapeSequenceInDocstring {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(r#"Use `r"""` if any backslashes in a docstring"#)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
format!(r#"Add `r` prefix"#)
|
||||
}
|
||||
}
|
||||
|
||||
/// D301
|
||||
pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) {
|
||||
// Docstring is already raw.
|
||||
let contents = docstring.contents;
|
||||
if contents.starts_with('r') || contents.starts_with("ur") {
|
||||
if docstring.leading_quote().contains(['r', 'R']) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,11 +70,15 @@ pub(crate) fn backslashes(checker: &mut Checker, docstring: &Docstring) {
|
||||
if memchr_iter(b'\\', bytes).any(|position| {
|
||||
let escaped_char = bytes.get(position.saturating_add(1));
|
||||
// Allow continuations (backslashes followed by newlines) and Unicode escapes.
|
||||
!matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'N'))
|
||||
!matches!(escaped_char, Some(b'\r' | b'\n' | b'u' | b'U' | b'N'))
|
||||
}) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
EscapeSequenceInDocstring,
|
||||
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
|
||||
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
"r".to_owned() + docstring.contents,
|
||||
docstring.range(),
|
||||
));
|
||||
)));
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,12 +78,14 @@ pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
|
||||
|
||||
let body = docstring.body().as_str();
|
||||
if !body.ends_with('\'') {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
format!("{prefixes}'''{body}'''"),
|
||||
docstring.range(),
|
||||
)));
|
||||
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(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
@@ -94,12 +96,14 @@ pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
|
||||
|
||||
let body = docstring.body().as_str();
|
||||
if !body.ends_with('"') {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
format!("{prefixes}\"\"\"{body}\"\"\""),
|
||||
docstring.range(),
|
||||
)));
|
||||
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(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -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,17 +10,7 @@ D.py:307:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
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():
|
||||
@@ -29,17 +19,7 @@ D.py:312:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
D.py:317:5: D300 Use triple double quotes `"""`
|
||||
|
|
||||
315 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
|
||||
316 | def single_quotes_raw():
|
||||
@@ -48,17 +28,7 @@ D.py:317:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
D.py:322:5: D300 Use triple double quotes `"""`
|
||||
|
|
||||
320 | @expect('D300: Use """triple double quotes""" (found \'-quotes)')
|
||||
321 | def single_quotes_raw_uppercase():
|
||||
@@ -67,17 +37,7 @@ D.py:322:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
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():
|
||||
@@ -86,17 +46,7 @@ D.py:328:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
D.py:645:5: D300 Use triple double quotes `"""`
|
||||
|
|
||||
644 | def single_line_docstring_with_an_escaped_backslash():
|
||||
645 | "\
|
||||
@@ -108,19 +58,7 @@ D.py:645:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
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
|
||||
@@ -130,17 +68,7 @@ D.py:649:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
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
|
||||
@@ -148,17 +76,7 @@ D.py:654:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
D.py:658:5: D300 Use triple double quotes `"""`
|
||||
|
|
||||
657 | class CommentAfterDocstring:
|
||||
658 | "After this docstring there's a comment." # priorities=1
|
||||
@@ -168,17 +86,7 @@ D.py:658:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
= 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 `"""`
|
||||
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 \
|
||||
@@ -188,13 +96,4 @@ 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"""
|
||||
|
||||
|
||||
|
||||
@@ -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,11 +17,4 @@ 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."""
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
|
||||
---
|
||||
D.py:328:5: D301 Use `r"""` if any backslashes in a docstring
|
||||
|
|
||||
326 | @expect('D301: Use r""" if any backslashes in a docstring')
|
||||
327 | def single_quotes_raw_uppercase_backslash():
|
||||
328 | R'Sum\mary.'
|
||||
| ^^^^^^^^^^^^ D301
|
||||
|
|
||||
|
||||
D.py:333:5: D301 Use `r"""` if any backslashes in a docstring
|
||||
D.py:333:5: D301 [*] Use `r"""` if any backslashes in a docstring
|
||||
|
|
||||
331 | @expect('D301: Use r""" if any backslashes in a docstring')
|
||||
332 | def double_quotes_backslash():
|
||||
333 | """Sum\\mary."""
|
||||
| ^^^^^^^^^^^^^^^^ D301
|
||||
|
|
||||
= help: Add `r` prefix
|
||||
|
||||
D.py:338:5: D301 Use `r"""` if any backslashes in a docstring
|
||||
|
|
||||
336 | @expect('D301: Use r""" if any backslashes in a docstring')
|
||||
337 | def double_quotes_backslash_uppercase():
|
||||
338 | R"""Sum\\mary."""
|
||||
| ^^^^^^^^^^^^^^^^^ D301
|
||||
|
|
||||
ℹ Suggested fix
|
||||
330 330 |
|
||||
331 331 | @expect('D301: Use r""" if any backslashes in a docstring')
|
||||
332 332 | def double_quotes_backslash():
|
||||
333 |- """Sum\\mary."""
|
||||
333 |+ r"""Sum\\mary."""
|
||||
334 334 |
|
||||
335 335 |
|
||||
336 336 | @expect('D301: Use r""" if any backslashes in a docstring')
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
|
||||
---
|
||||
D301.py:2:5: D301 Use `r"""` if any backslashes in a docstring
|
||||
D301.py:2:5: D301 [*] Use `r"""` if any backslashes in a docstring
|
||||
|
|
||||
1 | def double_quotes_backslash():
|
||||
2 | """Sum\\mary."""
|
||||
| ^^^^^^^^^^^^^^^^ D301
|
||||
|
|
||||
= help: Add `r` prefix
|
||||
|
||||
D301.py:10:5: D301 Use `r"""` if any backslashes in a docstring
|
||||
|
|
||||
9 | def double_quotes_backslash_uppercase():
|
||||
10 | R"""Sum\\mary."""
|
||||
| ^^^^^^^^^^^^^^^^^ D301
|
||||
|
|
||||
ℹ Suggested fix
|
||||
1 1 | def double_quotes_backslash():
|
||||
2 |- """Sum\\mary."""
|
||||
2 |+ r"""Sum\\mary."""
|
||||
3 3 |
|
||||
4 4 |
|
||||
5 5 | def double_quotes_backslash_raw():
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
---
|
||||
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 """
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
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"""
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
|
||||
---
|
||||
D300.py:6:5: D300 Use triple double quotes `"""`
|
||||
|
|
||||
5 | def ends_in_quote():
|
||||
6 | 'Sum\\mary."'
|
||||
| ^^^^^^^^^^^^^ D300
|
||||
|
|
||||
= help: Convert to triple double quotes
|
||||
|
||||
D300.py:10:5: D300 [*] Use triple double quotes `"""`
|
||||
|
|
||||
9 | def contains_quote():
|
||||
10 | 'Sum"\\mary.'
|
||||
| ^^^^^^^^^^^^^ D300
|
||||
|
|
||||
= help: Convert to triple double quotes
|
||||
|
||||
ℹ Fix
|
||||
7 7 |
|
||||
8 8 |
|
||||
9 9 | def contains_quote():
|
||||
10 |- 'Sum"\\mary.'
|
||||
10 |+ """Sum"\\mary."""
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ mod tests {
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_16.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_17.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_18.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_19.py"))]
|
||||
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
|
||||
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
|
||||
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
|
||||
@@ -135,6 +136,7 @@ mod tests {
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_17.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_18.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_19.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_20.py"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F821_20.py:3:24: F821 Undefined name `Record`
|
||||
|
|
||||
1 | """Test lazy evaluation of type alias values."""
|
||||
2 |
|
||||
3 | type RecordCallback[R: Record] = Callable[[R], None]
|
||||
| ^^^^^^ F821
|
||||
4 |
|
||||
5 | from collections.abc import Callable
|
||||
|
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ 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(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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::*;
|
||||
@@ -78,6 +79,7 @@ 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;
|
||||
|
||||
@@ -15,7 +15,6 @@ pub enum ConstantType {
|
||||
Float,
|
||||
Int,
|
||||
Str,
|
||||
Tuple,
|
||||
}
|
||||
|
||||
impl TryFrom<&Constant> for ConstantType {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
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():
|
||||
|
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::rules::{
|
||||
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
|
||||
pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade,
|
||||
};
|
||||
use crate::settings::types::{PerFileIgnore, PythonVersion};
|
||||
use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion};
|
||||
use crate::{codes, RuleSelector};
|
||||
|
||||
use super::line_width::{LineLength, TabSize};
|
||||
@@ -38,6 +38,7 @@ pub mod types;
|
||||
|
||||
#[derive(Debug, CacheKey)]
|
||||
pub struct LinterSettings {
|
||||
pub exclude: FilePatternSet,
|
||||
pub project_root: PathBuf,
|
||||
|
||||
pub rules: RuleTable,
|
||||
@@ -131,6 +132,7 @@ impl LinterSettings {
|
||||
|
||||
pub fn new(project_root: &Path) -> Self {
|
||||
Self {
|
||||
exclude: FilePatternSet::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
project_root: project_root.to_path_buf(),
|
||||
rules: DEFAULT_SELECTORS
|
||||
|
||||
@@ -18,6 +18,7 @@ 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
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use proc_macro2::TokenTree;
|
||||
use proc_macro2::{TokenStream, TokenTree};
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::meta::ParseNestedMeta;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::token::Comma;
|
||||
use syn::{
|
||||
AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput, ExprLit, Field,
|
||||
Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Token, Type, TypePath,
|
||||
Fields, Lit, LitStr, Meta, Path, PathArguments, PathSegment, Type, TypePath,
|
||||
};
|
||||
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
|
||||
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
||||
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
|
||||
let DeriveInput {
|
||||
ident,
|
||||
data,
|
||||
@@ -190,9 +189,30 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
|
||||
default,
|
||||
value_type,
|
||||
example,
|
||||
} = attr.parse_args::<FieldAttributes>()?;
|
||||
} = parse_field_attributes(attr)?;
|
||||
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{
|
||||
@@ -200,6 +220,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::To
|
||||
default: &#default,
|
||||
value_type: &#value_type,
|
||||
example: &#example,
|
||||
deprecated: #deprecated
|
||||
})
|
||||
}
|
||||
))
|
||||
@@ -212,39 +233,109 @@ struct FieldAttributes {
|
||||
example: String,
|
||||
}
|
||||
|
||||
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>()?;
|
||||
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()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
default,
|
||||
value_type,
|
||||
example: dedent(&example).trim_matches('\n').to_string(),
|
||||
})
|
||||
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} = \"...\"`"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
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")),
|
||||
}
|
||||
#[derive(Default, Debug)]
|
||||
struct DeprecatedAttribute {
|
||||
since: Option<String>,
|
||||
note: Option<String>,
|
||||
}
|
||||
|
||||
@@ -333,7 +333,6 @@ 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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::node::AnyNodeRef;
|
||||
use crate::AnyNodeRef;
|
||||
use crate::{self as ast, Expr};
|
||||
|
||||
/// Unowned pendant to [`ast::Expr`] that stores a reference instead of a owned value.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::Path;
|
||||
|
||||
pub use expression::*;
|
||||
pub use int::*;
|
||||
pub use node::{AnyNode, AnyNodeRef, AstNode, NodeKind};
|
||||
pub use nodes::*;
|
||||
|
||||
pub mod all;
|
||||
@@ -14,7 +15,7 @@ pub mod helpers;
|
||||
pub mod identifier;
|
||||
pub mod imports;
|
||||
mod int;
|
||||
pub mod node;
|
||||
mod node;
|
||||
mod nodes;
|
||||
pub mod parenthesize;
|
||||
pub mod relocate;
|
||||
|
||||
@@ -4817,7 +4817,7 @@ pub enum AnyNodeRef<'a> {
|
||||
ElifElseClause(&'a ast::ElifElseClause),
|
||||
}
|
||||
|
||||
impl AnyNodeRef<'_> {
|
||||
impl<'a> AnyNodeRef<'a> {
|
||||
pub fn as_ptr(&self) -> NonNull<()> {
|
||||
match self {
|
||||
AnyNodeRef::ModModule(node) => NonNull::from(*node).cast(),
|
||||
@@ -5456,9 +5456,9 @@ impl AnyNodeRef<'_> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn visit_preorder<'a, V>(&'a self, visitor: &mut V)
|
||||
pub fn visit_preorder<'b, V>(&'b self, visitor: &mut V)
|
||||
where
|
||||
V: PreorderVisitor<'a> + ?Sized,
|
||||
V: PreorderVisitor<'b> + ?Sized,
|
||||
{
|
||||
match self {
|
||||
AnyNodeRef::ModModule(node) => node.visit_preorder(visitor),
|
||||
@@ -5544,6 +5544,66 @@ impl AnyNodeRef<'_> {
|
||||
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> {
|
||||
|
||||
@@ -1,38 +1,47 @@
|
||||
use ruff_python_trivia::{BackwardsTokenizer, CommentRanges, SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::node::AnyNodeRef;
|
||||
use crate::AnyNodeRef;
|
||||
use crate::ExpressionRef;
|
||||
|
||||
/// Returns the [`TextRange`] of a given expression including parentheses, if the expression is
|
||||
/// 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()
|
||||
/// 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))
|
||||
} else {
|
||||
parent.end()
|
||||
SimpleTokenizer::starts_at(expr.end(), source)
|
||||
};
|
||||
|
||||
let right_tokenizer =
|
||||
SimpleTokenizer::new(source, TextRange::new(expr.end(), exclusive_parent_end))
|
||||
.skip_trivia()
|
||||
.take_while(|token| token.kind == SimpleTokenKind::RParen);
|
||||
let right_tokenizer = right_tokenizer
|
||||
.skip_trivia()
|
||||
.take_while(|token| token.kind == SimpleTokenKind::RParen);
|
||||
|
||||
let left_tokenizer = BackwardsTokenizer::up_to(expr.start(), source, comment_ranges)
|
||||
.skip_trivia()
|
||||
@@ -43,6 +52,16 @@ pub fn parenthesized_range(
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -102,3 +102,6 @@ aaaaaaaaaaaaaaaaaaaaa = [
|
||||
c # negative decimal
|
||||
]
|
||||
|
||||
# Parenthesized targets and iterators.
|
||||
[x for (x) in y]
|
||||
[x for x in (y)]
|
||||
|
||||
@@ -65,7 +65,7 @@ d3 = "d"[
|
||||
|
||||
# Spacing around the colon(s)
|
||||
def a():
|
||||
...
|
||||
pass
|
||||
|
||||
e00 = "e"[:]
|
||||
e01 = "e"[:1]
|
||||
|
||||
@@ -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,3 +161,14 @@ 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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ aaaaaaaa = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbb
|
||||
for converter in connection.ops.get_db_converters(
|
||||
expression
|
||||
) + expression.get_db_converters(connection):
|
||||
...
|
||||
pass
|
||||
|
||||
|
||||
aaa = (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
###
|
||||
# Blank lines around functions
|
||||
###
|
||||
import sys
|
||||
|
||||
x = 1
|
||||
|
||||
@@ -159,3 +160,97 @@ 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
|
||||
@@ -0,0 +1,113 @@
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGo
|
||||
pass
|
||||
|
||||
else:
|
||||
...
|
||||
pass
|
||||
|
||||
while (
|
||||
some_condition(unformatted, args) and anotherCondition or aThirdCondition
|
||||
|
||||
@@ -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
|
||||
... # body
|
||||
pass # 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,45 +60,41 @@ with (a, *b):
|
||||
|
||||
with (
|
||||
# leading comment
|
||||
a) as b: ...
|
||||
a) as b: pass
|
||||
|
||||
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
|
||||
)
|
||||
): ...
|
||||
|
||||
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
|
||||
|
||||
with (a # trailing same line comment
|
||||
# trailing own line comment
|
||||
) as b: ...
|
||||
) as b: pass
|
||||
|
||||
with (
|
||||
(a
|
||||
@@ -106,7 +102,7 @@ with (
|
||||
)
|
||||
as # trailing as same line comment
|
||||
b # trailing b same line comment
|
||||
): ...
|
||||
): pass
|
||||
|
||||
with (
|
||||
# comment
|
||||
@@ -157,7 +153,7 @@ with (
|
||||
CtxManager2() as example2,
|
||||
CtxManager2() as example2,
|
||||
):
|
||||
...
|
||||
pass
|
||||
|
||||
with [
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
@@ -165,7 +161,7 @@ with [
|
||||
"cccccccccccccccccccccccccccccccccccccccccc",
|
||||
dddddddddddddddddddddddddddddddd,
|
||||
] as example1, aaaaaaaaaaaaaaaaaaaaaaaaaa * bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb * cccccccccccccccccccccccccccc + ddddddddddddddddd as example2, CtxManager222222222222222() as example2:
|
||||
...
|
||||
pass
|
||||
|
||||
# Comments on open parentheses
|
||||
with ( # comment
|
||||
@@ -173,7 +169,7 @@ with ( # comment
|
||||
CtxManager2() as example2,
|
||||
CtxManager3() as example3,
|
||||
):
|
||||
...
|
||||
pass
|
||||
|
||||
with ( # outer comment
|
||||
( # inner comment
|
||||
@@ -182,25 +178,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
|
||||
@@ -208,7 +204,7 @@ with ( # outer comment
|
||||
),
|
||||
CtxManager2(),
|
||||
) as example:
|
||||
...
|
||||
pass
|
||||
|
||||
# Breaking of with items.
|
||||
with (test # bar
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -180,7 +180,7 @@ mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
|
||||
use ruff_formatter::SourceCode;
|
||||
use ruff_python_ast::node::AnyNode;
|
||||
use ruff_python_ast::AnyNode;
|
||||
use ruff_python_ast::{StmtBreak, StmtContinue};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
@@ -52,7 +52,7 @@ impl<'a> From<AnyNodeRef<'a>> for NodeRefEqualityKey<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::comments::node_key::NodeRefEqualityKey;
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::StmtContinue;
|
||||
use ruff_text_size::TextRange;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
@@ -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) = last_child_in_body(preceding) {
|
||||
if let Some(last_child) = preceding.last_child_in_body() {
|
||||
let innermost_child =
|
||||
std::iter::successors(Some(last_child), |parent| last_child_in_body(*parent))
|
||||
std::iter::successors(Some(last_child), AnyNodeRef::last_child_in_body)
|
||||
.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) = last_child_in_body(preceding) else {
|
||||
let Some(last_child) = preceding.last_child_in_body() 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_body(last_child_in_parent) {
|
||||
if let Some(nested_child) = last_child_in_parent.last_child_in_body() {
|
||||
// The comment belongs to the inner block.
|
||||
parent = Some(last_child_in_parent);
|
||||
last_child_in_parent = nested_child;
|
||||
@@ -1878,8 +1878,7 @@ fn handle_lambda_comment<'a>(
|
||||
CommentPlacement::Default(comment)
|
||||
}
|
||||
|
||||
/// Attach trailing end-of-line comments on the operator as dangling comments on the enclosing
|
||||
/// node.
|
||||
/// Move comment between a unary op and its operand before the unary op by marking them as trailing.
|
||||
///
|
||||
/// For example, given:
|
||||
/// ```python
|
||||
@@ -1896,26 +1895,27 @@ fn handle_unary_op_comment<'a>(
|
||||
unary_op: &'a ast::ExprUnaryOp,
|
||||
locator: &Locator,
|
||||
) -> CommentPlacement<'a> {
|
||||
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(
|
||||
let mut 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);
|
||||
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)
|
||||
}
|
||||
|
||||
CommentPlacement::dangling(comment.enclosing_node(), comment)
|
||||
}
|
||||
|
||||
/// Attach an end-of-line comment immediately following an open bracket as a dangling comment on
|
||||
@@ -2176,65 +2176,6 @@ 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
Reference in New Issue
Block a user