Compare commits
21 Commits
v0.1.7
...
zb/debug-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17e77fe515 | ||
|
|
946b308197 | ||
|
|
d22ce5372d | ||
|
|
acab5f3cf2 | ||
|
|
06c9f625b6 | ||
|
|
bbb0a0c360 | ||
|
|
9361e22fe9 | ||
|
|
f484df5470 | ||
|
|
af88ffc57e | ||
|
|
b918647927 | ||
|
|
ef7778d794 | ||
|
|
bd443ebe91 | ||
|
|
ee6548d7dd | ||
|
|
b4a050c21d | ||
|
|
958702ded0 | ||
|
|
268d95e911 | ||
|
|
3def18fc21 | ||
|
|
c48ba690eb | ||
|
|
fd49fb935f | ||
|
|
fe54ef08aa | ||
|
|
b7ffd73edd |
24
.github/workflows/ci.yaml
vendored
24
.github/workflows/ci.yaml
vendored
@@ -48,8 +48,8 @@ jobs:
|
||||
- "!crates/ruff_dev/**"
|
||||
- "!crates/ruff_shrinking/**"
|
||||
- scripts/*
|
||||
- .github/workflows/ci.yaml
|
||||
- python/**
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
formatter:
|
||||
- Cargo.toml
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
- .github/workflows/ci.yaml
|
||||
|
||||
code:
|
||||
- "*/**"
|
||||
- "**/*"
|
||||
- "!**/*.md"
|
||||
- "!docs/**"
|
||||
- "!assets/**"
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
name: "cargo clippy"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
cargo-test-linux:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo test (linux)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
cargo-test-windows:
|
||||
runs-on: windows-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo test (windows)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
cargo-test-wasm:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo test (wasm)"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
cargo-fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
name: "cargo fuzz"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
name: "test scripts"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
name: "cargo udeps"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install nightly Rust toolchain"
|
||||
@@ -444,7 +444,7 @@ jobs:
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: extractions/setup-just@v1
|
||||
env:
|
||||
@@ -483,7 +483,7 @@ jobs:
|
||||
benchmarks:
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@v4
|
||||
@@ -502,7 +502,7 @@ jobs:
|
||||
run: cargo codspeed build --features codspeed -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@v1
|
||||
uses: CodSpeedHQ/action@v2
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
14
.github/workflows/release.yaml
vendored
14
.github/workflows/release.yaml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-x86_64-apple-darwin.tar.gz
|
||||
ARCHIVE_FILE=ruff-${{ inputs.tag }}-x86_64-apple-darwin.tar.gz
|
||||
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-aarch64-apple-darwin.tar.gz
|
||||
ARCHIVE_FILE=ruff-${{ inputs.tag }}-aarch64-apple-darwin.tar.gz
|
||||
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
- name: "Archive binary"
|
||||
shell: bash
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.zip
|
||||
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.zip
|
||||
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
|
||||
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
|
||||
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
|
||||
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
|
||||
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
|
||||
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
@@ -343,7 +343,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
|
||||
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
|
||||
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
@@ -399,7 +399,7 @@ jobs:
|
||||
path: dist
|
||||
- name: "Archive binary"
|
||||
run: |
|
||||
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
|
||||
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
|
||||
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
|
||||
@@ -9,7 +9,7 @@ homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
|
||||
license = "MIT"
|
||||
license = "MIT2"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.69" }
|
||||
|
||||
@@ -147,3 +147,38 @@ def func(x: int):
|
||||
while x > 0:
|
||||
break
|
||||
return 1
|
||||
|
||||
|
||||
import abc
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class Foo(abc.ABC):
|
||||
@abstractmethod
|
||||
def method(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def method(self):
|
||||
"""Docstring."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def method(self):
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def method():
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def method(cls):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def method(self):
|
||||
if self.x > 0:
|
||||
return 1
|
||||
else:
|
||||
return 1.5
|
||||
|
||||
@@ -10,7 +10,6 @@ Foo.objects.create(**{**bar}) # PIE804
|
||||
|
||||
foo(**{})
|
||||
|
||||
|
||||
foo(**{**data, "foo": "buzz"})
|
||||
foo(**buzz)
|
||||
foo(**{"bar-foo": True})
|
||||
@@ -20,3 +19,5 @@ foo(**{buzz: True})
|
||||
foo(**{"": True})
|
||||
foo(**{f"buzz__{bar}": True})
|
||||
abc(**{"for": 3})
|
||||
|
||||
foo(**{},)
|
||||
|
||||
@@ -82,3 +82,14 @@ raise IndexError();
|
||||
|
||||
# RSE102
|
||||
raise Foo()
|
||||
|
||||
# OK
|
||||
raise ctypes.WinError()
|
||||
|
||||
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# OK
|
||||
raise func()
|
||||
|
||||
2
crates/ruff_linter/resources/test/fixtures/isort/force_sort_within_sections_future.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/isort/force_sort_within_sections_future.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import __future__
|
||||
from __future__ import annotations
|
||||
2
crates/ruff_linter/resources/test/fixtures/isort/future_from.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/isort/future_from.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import __future__
|
||||
from __future__ import annotations
|
||||
@@ -110,3 +110,10 @@ print('Hello %(arg)s' % bar['bop'])
|
||||
"%s" % (
|
||||
x, # comment
|
||||
)
|
||||
|
||||
|
||||
path = "%s-%s-%s.pem" % (
|
||||
safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
|
||||
cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
|
||||
hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
|
||||
)
|
||||
|
||||
@@ -543,7 +543,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
flake8_bugbear::rules::no_explicit_stacklevel(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryDictKwargs) {
|
||||
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
|
||||
flake8_pie::rules::unnecessary_dict_kwargs(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryRangeStart) {
|
||||
flake8_pie::rules::unnecessary_range_start(checker, call);
|
||||
|
||||
@@ -251,7 +251,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pylint::rules::too_many_arguments(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::TooManyPositional) {
|
||||
pylint::rules::too_many_positional(checker, parameters, stmt);
|
||||
pylint::rules::too_many_positional(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::TooManyReturnStatements) {
|
||||
if let Some(diagnostic) = pylint::rules::too_many_return_statements(
|
||||
|
||||
@@ -537,6 +537,19 @@ fn check_dynamically_typed<F>(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty_body(body: &[Stmt]) -> bool {
|
||||
body.iter().all(|stmt| match stmt {
|
||||
Stmt::Pass(_) => true,
|
||||
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
|
||||
matches!(
|
||||
value.as_ref(),
|
||||
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate flake8-annotation checks for a given `Definition`.
|
||||
pub(crate) fn definition(
|
||||
checker: &Checker,
|
||||
@@ -725,16 +738,22 @@ pub(crate) fn definition(
|
||||
) {
|
||||
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
|
||||
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
|
||||
let return_type = auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
|
||||
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
|
||||
&& is_empty_body(body)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MissingReturnTypeClassMethod {
|
||||
name: name.to_string(),
|
||||
@@ -752,16 +771,22 @@ pub(crate) fn definition(
|
||||
}
|
||||
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
|
||||
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
|
||||
let return_type = auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
|
||||
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
|
||||
&& is_empty_body(body)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MissingReturnTypeStaticMethod {
|
||||
name: name.to_string(),
|
||||
@@ -818,18 +843,25 @@ pub(crate) fn definition(
|
||||
match visibility {
|
||||
visibility::Visibility::Public => {
|
||||
if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) {
|
||||
let return_type = auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
(checker.generator().expr(&return_type), edits)
|
||||
});
|
||||
let return_type =
|
||||
if visibility::is_abstract(decorator_list, checker.semantic())
|
||||
&& is_empty_body(body)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
(checker.generator().expr(&return_type), edits)
|
||||
})
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MissingReturnTypeUndocumentedPublicFunction {
|
||||
name: name.to_string(),
|
||||
@@ -853,18 +885,25 @@ pub(crate) fn definition(
|
||||
}
|
||||
visibility::Visibility::Private => {
|
||||
if checker.enabled(Rule::MissingReturnTypePrivateFunction) {
|
||||
let return_type = auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
(checker.generator().expr(&return_type), edits)
|
||||
});
|
||||
let return_type =
|
||||
if visibility::is_abstract(decorator_list, checker.semantic())
|
||||
&& is_empty_body(body)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
auto_return_type(function)
|
||||
.and_then(|return_type| {
|
||||
return_type.into_expression(
|
||||
checker.importer(),
|
||||
function.parameters.start(),
|
||||
checker.semantic(),
|
||||
checker.settings.target_version,
|
||||
)
|
||||
})
|
||||
.map(|(return_type, edits)| {
|
||||
(checker.generator().expr(&return_type), edits)
|
||||
})
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
MissingReturnTypePrivateFunction {
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -427,4 +427,72 @@ auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public
|
||||
148 148 | break
|
||||
149 149 | return 1
|
||||
|
||||
auto_return_type.py:158:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
156 | class Foo(abc.ABC):
|
||||
157 | @abstractmethod
|
||||
158 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
159 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:162:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
161 | @abc.abstractmethod
|
||||
162 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
163 | """Docstring."""
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:166:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
165 | @abc.abstractmethod
|
||||
166 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
167 | ...
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:171:9: ANN205 Missing return type annotation for staticmethod `method`
|
||||
|
|
||||
169 | @staticmethod
|
||||
170 | @abstractmethod
|
||||
171 | def method():
|
||||
| ^^^^^^ ANN205
|
||||
172 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:176:9: ANN206 Missing return type annotation for classmethod `method`
|
||||
|
|
||||
174 | @classmethod
|
||||
175 | @abstractmethod
|
||||
176 | def method(cls):
|
||||
| ^^^^^^ ANN206
|
||||
177 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public function `method`
|
||||
|
|
||||
179 | @abstractmethod
|
||||
180 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
181 | if self.x > 0:
|
||||
182 | return 1
|
||||
|
|
||||
= help: Add return type annotation: `float`
|
||||
|
||||
ℹ Unsafe fix
|
||||
177 177 | pass
|
||||
178 178 |
|
||||
179 179 | @abstractmethod
|
||||
180 |- def method(self):
|
||||
180 |+ def method(self) -> float:
|
||||
181 181 | if self.x > 0:
|
||||
182 182 | return 1
|
||||
183 183 | else:
|
||||
|
||||
|
||||
|
||||
@@ -482,4 +482,72 @@ auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public
|
||||
148 149 | break
|
||||
149 150 | return 1
|
||||
|
||||
auto_return_type.py:158:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
156 | class Foo(abc.ABC):
|
||||
157 | @abstractmethod
|
||||
158 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
159 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:162:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
161 | @abc.abstractmethod
|
||||
162 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
163 | """Docstring."""
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:166:9: ANN201 Missing return type annotation for public function `method`
|
||||
|
|
||||
165 | @abc.abstractmethod
|
||||
166 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
167 | ...
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:171:9: ANN205 Missing return type annotation for staticmethod `method`
|
||||
|
|
||||
169 | @staticmethod
|
||||
170 | @abstractmethod
|
||||
171 | def method():
|
||||
| ^^^^^^ ANN205
|
||||
172 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:176:9: ANN206 Missing return type annotation for classmethod `method`
|
||||
|
|
||||
174 | @classmethod
|
||||
175 | @abstractmethod
|
||||
176 | def method(cls):
|
||||
| ^^^^^^ ANN206
|
||||
177 | pass
|
||||
|
|
||||
= help: Add return type annotation
|
||||
|
||||
auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public function `method`
|
||||
|
|
||||
179 | @abstractmethod
|
||||
180 | def method(self):
|
||||
| ^^^^^^ ANN201
|
||||
181 | if self.x > 0:
|
||||
182 | return 1
|
||||
|
|
||||
= help: Add return type annotation: `float`
|
||||
|
||||
ℹ Unsafe fix
|
||||
177 177 | pass
|
||||
178 178 |
|
||||
179 179 | @abstractmethod
|
||||
180 |- def method(self):
|
||||
180 |+ def method(self) -> float:
|
||||
181 181 | if self.x > 0:
|
||||
182 182 | return 1
|
||||
183 183 | else:
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_python_ast::{self as ast, Expr, Keyword};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
@@ -8,6 +8,7 @@ use ruff_text_size::Ranged;
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::{remove_argument, Parentheses};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unnecessary `dict` kwargs.
|
||||
@@ -52,8 +53,8 @@ impl AlwaysFixableViolation for UnnecessaryDictKwargs {
|
||||
}
|
||||
|
||||
/// PIE804
|
||||
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[Keyword]) {
|
||||
for kw in kwargs {
|
||||
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
for kw in &call.arguments.keywords {
|
||||
// keyword is a spread operator (indicated by None)
|
||||
if kw.arg.is_some() {
|
||||
continue;
|
||||
@@ -65,7 +66,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
|
||||
|
||||
// Ex) `foo(**{**bar})`
|
||||
if matches!(keys.as_slice(), [None]) {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, call.range());
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
format!("**{}", checker.locator().slice(values[0].range())),
|
||||
@@ -86,10 +87,18 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range());
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, call.range());
|
||||
|
||||
if values.is_empty() {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(kw.start(), kw.end())));
|
||||
diagnostic.try_set_fix(|| {
|
||||
remove_argument(
|
||||
kw,
|
||||
&call.arguments,
|
||||
Parentheses::Preserve,
|
||||
checker.locator().contents(),
|
||||
)
|
||||
.map(Fix::safe_edit)
|
||||
});
|
||||
} else {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
kwargs
|
||||
|
||||
@@ -106,6 +106,8 @@ PIE804.py:11:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
10 |
|
||||
11 | foo(**{})
|
||||
| ^^^^^^^^^ PIE804
|
||||
12 |
|
||||
13 | foo(**{**data, "foo": "buzz"})
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
@@ -116,7 +118,23 @@ PIE804.py:11:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
11 |-foo(**{})
|
||||
11 |+foo()
|
||||
12 12 |
|
||||
13 13 |
|
||||
14 14 | foo(**{**data, "foo": "buzz"})
|
||||
13 13 | foo(**{**data, "foo": "buzz"})
|
||||
14 14 | foo(**buzz)
|
||||
|
||||
PIE804.py:23:1: PIE804 [*] Unnecessary `dict` kwargs
|
||||
|
|
||||
21 | abc(**{"for": 3})
|
||||
22 |
|
||||
23 | foo(**{},)
|
||||
| ^^^^^^^^^^ PIE804
|
||||
|
|
||||
= help: Remove unnecessary kwargs
|
||||
|
||||
ℹ Safe fix
|
||||
20 20 | foo(**{f"buzz__{bar}": True})
|
||||
21 21 | abc(**{"for": 3})
|
||||
22 22 |
|
||||
23 |-foo(**{},)
|
||||
23 |+foo()
|
||||
|
||||
|
||||
|
||||
@@ -78,9 +78,7 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
|
||||
|
||||
// `ctypes.WinError()` is a function, not a class. It's part of the standard library, so
|
||||
// we might as well get it right.
|
||||
if exception_type
|
||||
.as_ref()
|
||||
.is_some_and(ExceptionType::is_builtin)
|
||||
if exception_type.is_none()
|
||||
&& checker
|
||||
.semantic()
|
||||
.resolve_call_path(func)
|
||||
|
||||
@@ -266,6 +266,8 @@ RSE102.py:84:10: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
83 | # RSE102
|
||||
84 | raise Foo()
|
||||
| ^^ RSE102
|
||||
85 |
|
||||
86 | # OK
|
||||
|
|
||||
= help: Remove unnecessary parentheses
|
||||
|
||||
@@ -275,5 +277,8 @@ RSE102.py:84:10: RSE102 [*] Unnecessary parentheses on raised exception
|
||||
83 83 | # RSE102
|
||||
84 |-raise Foo()
|
||||
84 |+raise Foo
|
||||
85 85 |
|
||||
86 86 | # OK
|
||||
87 87 | raise ctypes.WinError()
|
||||
|
||||
|
||||
|
||||
@@ -66,6 +66,10 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) {
|
||||
return;
|
||||
};
|
||||
|
||||
if checker.semantic().in_annotation() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attr.starts_with("__") && !attr.ends_with("__"))
|
||||
|| (attr.starts_with('_') && !attr.starts_with("__"))
|
||||
{
|
||||
|
||||
@@ -180,7 +180,7 @@ fn format_import_block(
|
||||
continue;
|
||||
};
|
||||
|
||||
let imports = order_imports(import_block, settings);
|
||||
let imports = order_imports(import_block, import_section, settings);
|
||||
|
||||
// Add a blank line between every section.
|
||||
if is_first_block {
|
||||
@@ -291,6 +291,7 @@ mod tests {
|
||||
#[test_case(Path::new("force_sort_within_sections.py"))]
|
||||
#[test_case(Path::new("force_to_top.py"))]
|
||||
#[test_case(Path::new("force_wrap_aliases.py"))]
|
||||
#[test_case(Path::new("future_from.py"))]
|
||||
#[test_case(Path::new("if_elif_else.py"))]
|
||||
#[test_case(Path::new("import_from_after_import.py"))]
|
||||
#[test_case(Path::new("inline_comments.py"))]
|
||||
@@ -701,6 +702,7 @@ mod tests {
|
||||
|
||||
#[test_case(Path::new("force_sort_within_sections.py"))]
|
||||
#[test_case(Path::new("force_sort_within_sections_with_as_names.py"))]
|
||||
#[test_case(Path::new("force_sort_within_sections_future.py"))]
|
||||
fn force_sort_within_sections(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("force_sort_within_sections_{}", path.to_string_lossy());
|
||||
let mut diagnostics = test_path(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::rules::isort::sorting::ImportStyle;
|
||||
use crate::rules::isort::{ImportSection, ImportType};
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::settings::Settings;
|
||||
@@ -8,6 +9,7 @@ use super::types::{AliasData, CommentSet, ImportBlock, ImportFromStatement};
|
||||
|
||||
pub(crate) fn order_imports<'a>(
|
||||
block: ImportBlock<'a>,
|
||||
section: &ImportSection,
|
||||
settings: &Settings,
|
||||
) -> Vec<EitherImport<'a>> {
|
||||
let straight_imports = block.import.into_iter();
|
||||
@@ -52,7 +54,35 @@ pub(crate) fn order_imports<'a>(
|
||||
},
|
||||
);
|
||||
|
||||
let ordered_imports = if settings.force_sort_within_sections {
|
||||
let ordered_imports = if matches!(section, ImportSection::Known(ImportType::Future)) {
|
||||
from_imports
|
||||
.sorted_by_cached_key(|(import_from, _, _, aliases)| {
|
||||
ModuleKey::from_module(
|
||||
import_from.module,
|
||||
None,
|
||||
import_from.level,
|
||||
aliases.first().map(|(alias, _)| (alias.name, alias.asname)),
|
||||
ImportStyle::From,
|
||||
settings,
|
||||
)
|
||||
})
|
||||
.map(ImportFrom)
|
||||
.chain(
|
||||
straight_imports
|
||||
.sorted_by_cached_key(|(alias, _)| {
|
||||
ModuleKey::from_module(
|
||||
Some(alias.name),
|
||||
alias.asname,
|
||||
None,
|
||||
None,
|
||||
ImportStyle::Straight,
|
||||
settings,
|
||||
)
|
||||
})
|
||||
.map(Import),
|
||||
)
|
||||
.collect()
|
||||
} else if settings.force_sort_within_sections {
|
||||
straight_imports
|
||||
.map(Import)
|
||||
.chain(from_imports.map(ImportFrom))
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
force_sort_within_sections_future.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
|
|
||||
1 | / import __future__
|
||||
2 | | from __future__ import annotations
|
||||
|
|
||||
= help: Organize imports
|
||||
|
||||
ℹ Safe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | import __future__
|
||||
2 |-from __future__ import annotations
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/isort/mod.rs
|
||||
---
|
||||
future_from.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
|
|
||||
1 | / import __future__
|
||||
2 | | from __future__ import annotations
|
||||
|
|
||||
= help: Organize imports
|
||||
|
||||
ℹ Safe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | import __future__
|
||||
2 |-from __future__ import annotations
|
||||
|
||||
|
||||
@@ -21,6 +21,14 @@ use crate::rules::pylint::helpers::CmpOpExt;
|
||||
/// foo == foo
|
||||
/// ```
|
||||
///
|
||||
/// In some cases, self-comparisons are used to determine whether a float is
|
||||
/// NaN. Instead, prefer `math.isnan`:
|
||||
/// ```python
|
||||
/// import math
|
||||
///
|
||||
/// math.isnan(foo)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons)
|
||||
#[violation]
|
||||
|
||||
@@ -53,7 +53,7 @@ impl AlwaysFixableViolation for NoClassmethodDecorator {
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def bar(cls):
|
||||
/// def bar(arg1, arg2):
|
||||
/// ...
|
||||
///
|
||||
/// bar = staticmethod(bar)
|
||||
@@ -63,7 +63,7 @@ impl AlwaysFixableViolation for NoClassmethodDecorator {
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// @staticmethod
|
||||
/// def bar(cls):
|
||||
/// def bar(arg1, arg2):
|
||||
/// ...
|
||||
/// ```
|
||||
#[violation]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{identifier::Identifier, Parameters, Stmt};
|
||||
use ruff_python_ast::{self as ast, identifier::Identifier};
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -55,11 +56,12 @@ impl Violation for TooManyPositional {
|
||||
}
|
||||
|
||||
/// PLR0917
|
||||
pub(crate) fn too_many_positional(checker: &mut Checker, parameters: &Parameters, stmt: &Stmt) {
|
||||
let num_positional_args = parameters
|
||||
pub(crate) fn too_many_positional(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
|
||||
let num_positional_args = function_def
|
||||
.parameters
|
||||
.args
|
||||
.iter()
|
||||
.chain(¶meters.posonlyargs)
|
||||
.chain(&function_def.parameters.posonlyargs)
|
||||
.filter(|arg| {
|
||||
!checker
|
||||
.settings
|
||||
@@ -67,13 +69,22 @@ pub(crate) fn too_many_positional(checker: &mut Checker, parameters: &Parameters
|
||||
.is_match(&arg.parameter.name)
|
||||
})
|
||||
.count();
|
||||
|
||||
if num_positional_args > checker.settings.pylint.max_positional_args {
|
||||
// Allow excessive arguments in `@override` or `@overload` methods, since they're required
|
||||
// to adhere to the parent signature.
|
||||
if visibility::is_override(&function_def.decorator_list, checker.semantic())
|
||||
|| visibility::is_overload(&function_def.decorator_list, checker.semantic())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TooManyPositional {
|
||||
c_pos: num_positional_args,
|
||||
max_pos: checker.settings.pylint.max_positional_args,
|
||||
},
|
||||
stmt.identifier(),
|
||||
function_def.identifier(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr, StmtFor};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::pylint::helpers::SequenceIndexVisitor;
|
||||
@@ -51,7 +52,7 @@ pub(crate) fn unnecessary_dict_index_lookup(checker: &mut Checker, stmt_for: &St
|
||||
};
|
||||
|
||||
let ranges = {
|
||||
let mut visitor = SequenceIndexVisitor::new(dict_name, index_name, value_name);
|
||||
let mut visitor = SequenceIndexVisitor::new(&dict_name.id, &index_name.id, &value_name.id);
|
||||
visitor.visit_body(&stmt_for.body);
|
||||
visitor.visit_body(&stmt_for.orelse);
|
||||
visitor.into_accesses()
|
||||
@@ -59,10 +60,10 @@ pub(crate) fn unnecessary_dict_index_lookup(checker: &mut Checker, stmt_for: &St
|
||||
|
||||
for range in ranges {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictIndexLookup, range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
value_name.to_string(),
|
||||
range,
|
||||
)));
|
||||
diagnostic.set_fix(Fix::safe_edits(
|
||||
Edit::range_replacement(value_name.id.to_string(), range),
|
||||
[noop(index_name), noop(value_name)],
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -93,7 +94,8 @@ pub(crate) fn unnecessary_dict_index_lookup_comprehension(checker: &mut Checker,
|
||||
};
|
||||
|
||||
let ranges = {
|
||||
let mut visitor = SequenceIndexVisitor::new(dict_name, index_name, value_name);
|
||||
let mut visitor =
|
||||
SequenceIndexVisitor::new(&dict_name.id, &index_name.id, &value_name.id);
|
||||
visitor.visit_expr(elt.as_ref());
|
||||
for expr in &comp.ifs {
|
||||
visitor.visit_expr(expr);
|
||||
@@ -103,10 +105,10 @@ pub(crate) fn unnecessary_dict_index_lookup_comprehension(checker: &mut Checker,
|
||||
|
||||
for range in ranges {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryDictIndexLookup, range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
value_name.to_string(),
|
||||
range,
|
||||
)));
|
||||
diagnostic.set_fix(Fix::safe_edits(
|
||||
Edit::range_replacement(value_name.id.to_string(), range),
|
||||
[noop(index_name), noop(value_name)],
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -115,7 +117,7 @@ pub(crate) fn unnecessary_dict_index_lookup_comprehension(checker: &mut Checker,
|
||||
fn dict_items<'a>(
|
||||
call_expr: &'a Expr,
|
||||
tuple_expr: &'a Expr,
|
||||
) -> Option<(&'a str, &'a str, &'a str)> {
|
||||
) -> Option<(&'a ast::ExprName, &'a ast::ExprName, &'a ast::ExprName)> {
|
||||
let ast::ExprCall {
|
||||
func, arguments, ..
|
||||
} = call_expr.as_call_expr()?;
|
||||
@@ -130,7 +132,7 @@ fn dict_items<'a>(
|
||||
return None;
|
||||
}
|
||||
|
||||
let Expr::Name(ast::ExprName { id: dict_name, .. }) = value.as_ref() else {
|
||||
let Expr::Name(dict_name) = value.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -142,19 +144,24 @@ fn dict_items<'a>(
|
||||
};
|
||||
|
||||
// Grab the variable names.
|
||||
let Expr::Name(ast::ExprName { id: index_name, .. }) = index else {
|
||||
let Expr::Name(index_name) = index else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Expr::Name(ast::ExprName { id: value_name, .. }) = value else {
|
||||
let Expr::Name(value_name) = value else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// If either of the variable names are intentionally ignored by naming them `_`, then don't
|
||||
// emit.
|
||||
if index_name == "_" || value_name == "_" {
|
||||
if index_name.id == "_" || value_name.id == "_" {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((dict_name, index_name, value_name))
|
||||
}
|
||||
|
||||
/// Return a no-op edit for the given name.
|
||||
fn noop(name: &ast::ExprName) -> Edit {
|
||||
Edit::range_replacement(name.id.to_string(), name.range())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{self as ast, Expr, StmtFor};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::pylint::helpers::SequenceIndexVisitor;
|
||||
@@ -53,7 +54,7 @@ pub(crate) fn unnecessary_list_index_lookup(checker: &mut Checker, stmt_for: &St
|
||||
};
|
||||
|
||||
let ranges = {
|
||||
let mut visitor = SequenceIndexVisitor::new(sequence, index_name, value_name);
|
||||
let mut visitor = SequenceIndexVisitor::new(&sequence.id, &index_name.id, &value_name.id);
|
||||
visitor.visit_body(&stmt_for.body);
|
||||
visitor.visit_body(&stmt_for.orelse);
|
||||
visitor.into_accesses()
|
||||
@@ -61,10 +62,10 @@ pub(crate) fn unnecessary_list_index_lookup(checker: &mut Checker, stmt_for: &St
|
||||
|
||||
for range in ranges {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListIndexLookup, range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
value_name.to_string(),
|
||||
range,
|
||||
)));
|
||||
diagnostic.set_fix(Fix::safe_edits(
|
||||
Edit::range_replacement(value_name.id.to_string(), range),
|
||||
[noop(index_name), noop(value_name)],
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -97,17 +98,18 @@ pub(crate) fn unnecessary_list_index_lookup_comprehension(checker: &mut Checker,
|
||||
};
|
||||
|
||||
let ranges = {
|
||||
let mut visitor = SequenceIndexVisitor::new(sequence, index_name, value_name);
|
||||
let mut visitor =
|
||||
SequenceIndexVisitor::new(&sequence.id, &index_name.id, &value_name.id);
|
||||
visitor.visit_expr(elt.as_ref());
|
||||
visitor.into_accesses()
|
||||
};
|
||||
|
||||
for range in ranges {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessaryListIndexLookup, range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
value_name.to_string(),
|
||||
range,
|
||||
)));
|
||||
diagnostic.set_fix(Fix::safe_edits(
|
||||
Edit::range_replacement(value_name.id.to_string(), range),
|
||||
[noop(index_name), noop(value_name)],
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -117,7 +119,7 @@ fn enumerate_items<'a>(
|
||||
call_expr: &'a Expr,
|
||||
tuple_expr: &'a Expr,
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<(&'a str, &'a str, &'a str)> {
|
||||
) -> Option<(&'a ast::ExprName, &'a ast::ExprName, &'a ast::ExprName)> {
|
||||
let ast::ExprCall {
|
||||
func, arguments, ..
|
||||
} = call_expr.as_call_expr()?;
|
||||
@@ -138,24 +140,29 @@ fn enumerate_items<'a>(
|
||||
};
|
||||
|
||||
// Grab the variable names.
|
||||
let Expr::Name(ast::ExprName { id: index_name, .. }) = index else {
|
||||
let Expr::Name(index_name) = index else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Expr::Name(ast::ExprName { id: value_name, .. }) = value else {
|
||||
let Expr::Name(value_name) = value else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// If either of the variable names are intentionally ignored by naming them `_`, then don't
|
||||
// emit.
|
||||
if index_name == "_" || value_name == "_" {
|
||||
if index_name.id == "_" || value_name.id == "_" {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the first argument of the enumerate call.
|
||||
let Some(Expr::Name(ast::ExprName { id: sequence, .. })) = arguments.args.first() else {
|
||||
let Some(Expr::Name(sequence)) = arguments.args.first() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((sequence, index_name, value_name))
|
||||
}
|
||||
|
||||
/// Return a no-op edit for the given name.
|
||||
fn noop(name: &ast::ExprName) -> Edit {
|
||||
Edit::range_replacement(name.id.to_string(), name.range())
|
||||
}
|
||||
|
||||
@@ -490,18 +490,10 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right
|
||||
contents.push_str(&format!(".format{params_string}"));
|
||||
|
||||
let mut diagnostic = Diagnostic::new(PrintfStringFormatting, expr.range());
|
||||
// Avoid fix if there are comments within the right-hand side:
|
||||
// ```
|
||||
// "%s" % (
|
||||
// 0, # 0
|
||||
// )
|
||||
// ```
|
||||
if !checker.indexer().comment_ranges().intersects(right.range()) {
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
expr.range(),
|
||||
)));
|
||||
}
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
contents,
|
||||
expr.range(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
|
||||
@@ -896,7 +896,7 @@ UP031_0.py:104:5: UP031 [*] Use format specifiers instead of percent format
|
||||
109 108 |
|
||||
110 109 | "%s" % (
|
||||
|
||||
UP031_0.py:110:1: UP031 Use format specifiers instead of percent format
|
||||
UP031_0.py:110:1: UP031 [*] Use format specifiers instead of percent format
|
||||
|
|
||||
108 | )
|
||||
109 |
|
||||
@@ -907,4 +907,36 @@ UP031_0.py:110:1: UP031 Use format specifiers instead of percent format
|
||||
|
|
||||
= help: Replace with format specifiers
|
||||
|
||||
ℹ Unsafe fix
|
||||
107 107 | % (x,)
|
||||
108 108 | )
|
||||
109 109 |
|
||||
110 |-"%s" % (
|
||||
110 |+"{}".format(
|
||||
111 111 | x, # comment
|
||||
112 112 | )
|
||||
113 113 |
|
||||
|
||||
UP031_0.py:115:8: UP031 [*] Use format specifiers instead of percent format
|
||||
|
|
||||
115 | path = "%s-%s-%s.pem" % (
|
||||
| ________^
|
||||
116 | | safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
|
||||
117 | | cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
|
||||
118 | | hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
|
||||
119 | | )
|
||||
| |_^ UP031
|
||||
|
|
||||
= help: Replace with format specifiers
|
||||
|
||||
ℹ Unsafe fix
|
||||
112 112 | )
|
||||
113 113 |
|
||||
114 114 |
|
||||
115 |-path = "%s-%s-%s.pem" % (
|
||||
115 |+path = "{}-{}-{}.pem".format(
|
||||
116 116 | safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
|
||||
117 117 | cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
|
||||
118 118 | hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
|
||||
|
||||
|
||||
|
||||
@@ -139,7 +139,6 @@ pub trait PreorderVisitor<'a> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
fn visit_pattern_keyword(&mut self, pattern_keyword: &'a PatternKeyword) {
|
||||
walk_pattern_keyword(self, pattern_keyword);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,8 @@ use std::fmt::{Debug, Write};
|
||||
|
||||
use insta::assert_snapshot;
|
||||
|
||||
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, ExceptHandler, Expr, Keyword, MatchCase, Mod, Operator,
|
||||
Parameter, Parameters, Pattern, Singleton, Stmt, TypeParam, UnaryOp, WithItem,
|
||||
};
|
||||
use ruff_python_ast::visitor::preorder::{PreorderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::{AnyNodeRef, BoolOp, CmpOp, Operator, Singleton, UnaryOp};
|
||||
use ruff_python_parser::lexer::lex;
|
||||
use ruff_python_parser::{parse_tokens, Mode};
|
||||
|
||||
@@ -128,6 +120,33 @@ fn function_type_parameters() {
|
||||
assert_snapshot!(trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_literals() {
|
||||
let source = r"'a' 'b' 'c'";
|
||||
|
||||
let trace = trace_preorder_visitation(source);
|
||||
|
||||
assert_snapshot!(trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_literals() {
|
||||
let source = r"b'a' b'b' b'c'";
|
||||
|
||||
let trace = trace_preorder_visitation(source);
|
||||
|
||||
assert_snapshot!(trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f_strings() {
|
||||
let source = r"'pre' f'foo {bar:.{x}f} baz'";
|
||||
|
||||
let trace = trace_preorder_visitation(source);
|
||||
|
||||
assert_snapshot!(trace);
|
||||
}
|
||||
|
||||
fn trace_preorder_visitation(source: &str) -> String {
|
||||
let tokens = lex(source, Mode::Module);
|
||||
let parsed = parse_tokens(tokens, source, Mode::Module, "test.py").unwrap();
|
||||
@@ -147,18 +166,6 @@ struct RecordVisitor {
|
||||
}
|
||||
|
||||
impl RecordVisitor {
|
||||
fn enter_node<'a, T>(&mut self, node: T)
|
||||
where
|
||||
T: Into<AnyNodeRef<'a>>,
|
||||
{
|
||||
self.emit(&node.into().kind());
|
||||
self.depth += 1;
|
||||
}
|
||||
|
||||
fn exit_node(&mut self) {
|
||||
self.depth -= 1;
|
||||
}
|
||||
|
||||
fn emit(&mut self, text: &dyn Debug) {
|
||||
for _ in 0..self.depth {
|
||||
self.output.push_str(" ");
|
||||
@@ -168,29 +175,16 @@ impl RecordVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
impl PreorderVisitor<'_> for RecordVisitor {
|
||||
fn visit_mod(&mut self, module: &Mod) {
|
||||
self.enter_node(module);
|
||||
walk_module(self, module);
|
||||
self.exit_node();
|
||||
impl<'a> PreorderVisitor<'a> for RecordVisitor {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
|
||||
self.emit(&node.kind());
|
||||
self.depth += 1;
|
||||
|
||||
TraversalSignal::Traverse
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
self.enter_node(stmt);
|
||||
walk_stmt(self, stmt);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_annotation(&mut self, expr: &Expr) {
|
||||
self.enter_node(expr);
|
||||
walk_expr(self, expr);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &Expr) {
|
||||
self.enter_node(expr);
|
||||
walk_expr(self, expr);
|
||||
self.exit_node();
|
||||
fn leave_node(&mut self, _node: AnyNodeRef<'a>) {
|
||||
self.depth -= 1;
|
||||
}
|
||||
|
||||
fn visit_singleton(&mut self, singleton: &Singleton) {
|
||||
@@ -212,70 +206,4 @@ impl PreorderVisitor<'_> for RecordVisitor {
|
||||
fn visit_cmp_op(&mut self, cmp_op: &CmpOp) {
|
||||
self.emit(&cmp_op);
|
||||
}
|
||||
|
||||
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
|
||||
self.enter_node(comprehension);
|
||||
walk_comprehension(self, comprehension);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_except_handler(&mut self, except_handler: &ExceptHandler) {
|
||||
self.enter_node(except_handler);
|
||||
walk_except_handler(self, except_handler);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_format_spec(&mut self, format_spec: &Expr) {
|
||||
self.enter_node(format_spec);
|
||||
walk_expr(self, format_spec);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_parameters(&mut self, parameters: &Parameters) {
|
||||
self.enter_node(parameters);
|
||||
walk_parameters(self, parameters);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_parameter(&mut self, parameter: &Parameter) {
|
||||
self.enter_node(parameter);
|
||||
walk_parameter(self, parameter);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_keyword(&mut self, keyword: &Keyword) {
|
||||
self.enter_node(keyword);
|
||||
walk_keyword(self, keyword);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_alias(&mut self, alias: &Alias) {
|
||||
self.enter_node(alias);
|
||||
walk_alias(self, alias);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_with_item(&mut self, with_item: &WithItem) {
|
||||
self.enter_node(with_item);
|
||||
walk_with_item(self, with_item);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_match_case(&mut self, match_case: &MatchCase) {
|
||||
self.enter_node(match_case);
|
||||
walk_match_case(self, match_case);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &Pattern) {
|
||||
self.enter_node(pattern);
|
||||
walk_pattern(self, pattern);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_type_param(&mut self, type_param: &TypeParam) {
|
||||
self.enter_node(type_param);
|
||||
walk_type_param(self, type_param);
|
||||
self.exit_node();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: crates/ruff_python_ast/tests/preorder.rs
|
||||
expression: trace
|
||||
---
|
||||
- ModModule
|
||||
- StmtExpr
|
||||
- ExprBytesLiteral
|
||||
- BytesLiteral
|
||||
- BytesLiteral
|
||||
- BytesLiteral
|
||||
|
||||
@@ -4,11 +4,12 @@ expression: trace
|
||||
---
|
||||
- ModModule
|
||||
- StmtClassDef
|
||||
- TypeParamTypeVar
|
||||
- ExprName
|
||||
- TypeParamTypeVar
|
||||
- TypeParamTypeVarTuple
|
||||
- TypeParamParamSpec
|
||||
- TypeParams
|
||||
- TypeParamTypeVar
|
||||
- ExprName
|
||||
- TypeParamTypeVar
|
||||
- TypeParamTypeVarTuple
|
||||
- TypeParamParamSpec
|
||||
- StmtExpr
|
||||
- ExprEllipsisLiteral
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ expression: trace
|
||||
---
|
||||
- ModModule
|
||||
- StmtFunctionDef
|
||||
- ExprName
|
||||
- Decorator
|
||||
- ExprName
|
||||
- Parameters
|
||||
- StmtPass
|
||||
- StmtClassDef
|
||||
- ExprName
|
||||
- Decorator
|
||||
- ExprName
|
||||
- StmtPass
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: crates/ruff_python_ast/tests/preorder.rs
|
||||
expression: trace
|
||||
---
|
||||
- ModModule
|
||||
- StmtExpr
|
||||
- ExprFString
|
||||
- StringLiteral
|
||||
- FString
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
- ExprFormattedValue
|
||||
- ExprName
|
||||
- ExprFString
|
||||
- ExprFString
|
||||
- FString
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
- ExprFormattedValue
|
||||
- ExprName
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
|
||||
@@ -5,16 +5,22 @@ expression: trace
|
||||
- ModModule
|
||||
- StmtFunctionDef
|
||||
- Parameters
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- Parameter
|
||||
- Parameter
|
||||
- Parameter
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- Parameter
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- Parameter
|
||||
- StmtPass
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ expression: trace
|
||||
- ModModule
|
||||
- StmtFunctionDef
|
||||
- Parameters
|
||||
- Parameter
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- ParameterWithDefault
|
||||
- Parameter
|
||||
- ExprNumberLiteral
|
||||
- Parameter
|
||||
- StmtPass
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ expression: trace
|
||||
---
|
||||
- ModModule
|
||||
- StmtFunctionDef
|
||||
- TypeParamTypeVar
|
||||
- ExprName
|
||||
- TypeParamTypeVar
|
||||
- TypeParamTypeVarTuple
|
||||
- TypeParamParamSpec
|
||||
- TypeParams
|
||||
- TypeParamTypeVar
|
||||
- ExprName
|
||||
- TypeParamTypeVar
|
||||
- TypeParamTypeVarTuple
|
||||
- TypeParamParamSpec
|
||||
- Parameters
|
||||
- StmtExpr
|
||||
- ExprEllipsisLiteral
|
||||
|
||||
@@ -8,21 +8,26 @@ expression: trace
|
||||
- MatchCase
|
||||
- PatternMatchClass
|
||||
- ExprName
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- PatternArguments
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- StmtExpr
|
||||
- ExprEllipsisLiteral
|
||||
- MatchCase
|
||||
- PatternMatchClass
|
||||
- ExprName
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- PatternArguments
|
||||
- PatternKeyword
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- PatternKeyword
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- PatternKeyword
|
||||
- PatternMatchValue
|
||||
- ExprNumberLiteral
|
||||
- StmtExpr
|
||||
- ExprEllipsisLiteral
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: crates/ruff_python_ast/tests/preorder.rs
|
||||
expression: trace
|
||||
---
|
||||
- ModModule
|
||||
- StmtExpr
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
- StringLiteral
|
||||
- StringLiteral
|
||||
|
||||
@@ -5,11 +5,12 @@ expression: trace
|
||||
- ModModule
|
||||
- StmtTypeAlias
|
||||
- ExprName
|
||||
- TypeParamTypeVar
|
||||
- ExprName
|
||||
- TypeParamTypeVar
|
||||
- TypeParamTypeVarTuple
|
||||
- TypeParamParamSpec
|
||||
- TypeParams
|
||||
- TypeParamTypeVar
|
||||
- ExprName
|
||||
- TypeParamTypeVar
|
||||
- TypeParamTypeVarTuple
|
||||
- TypeParamParamSpec
|
||||
- ExprSubscript
|
||||
- ExprName
|
||||
- ExprName
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_python_ast/tests/visitor.rs
|
||||
expression: trace
|
||||
---
|
||||
- StmtExpr
|
||||
- ExprBytesLiteral
|
||||
- BytesLiteral
|
||||
- BytesLiteral
|
||||
- BytesLiteral
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/ruff_python_ast/tests/visitor.rs
|
||||
expression: trace
|
||||
---
|
||||
- StmtExpr
|
||||
- ExprFString
|
||||
- StringLiteral
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
- ExprFormattedValue
|
||||
- ExprName
|
||||
- ExprFString
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
- ExprFormattedValue
|
||||
- ExprName
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_python_ast/tests/visitor.rs
|
||||
expression: trace
|
||||
---
|
||||
- StmtExpr
|
||||
- ExprStringLiteral
|
||||
- StringLiteral
|
||||
- StringLiteral
|
||||
- StringLiteral
|
||||
|
||||
@@ -6,14 +6,14 @@ use ruff_python_parser::lexer::lex;
|
||||
use ruff_python_parser::{parse_tokens, Mode};
|
||||
|
||||
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,
|
||||
walk_alias, walk_bytes_literal, walk_comprehension, walk_except_handler, walk_expr,
|
||||
walk_keyword, walk_match_case, walk_parameter, walk_parameters, walk_pattern, walk_stmt,
|
||||
walk_string_literal, 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,
|
||||
Alias, BoolOp, BytesLiteral, CmpOp, Comprehension, ExceptHandler, Expr, Keyword, MatchCase,
|
||||
Operator, Parameter, Parameters, Pattern, Stmt, StringLiteral, TypeParam, UnaryOp, WithItem,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -129,6 +129,33 @@ fn function_type_parameters() {
|
||||
assert_snapshot!(trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_literals() {
|
||||
let source = r"'a' 'b' 'c'";
|
||||
|
||||
let trace = trace_visitation(source);
|
||||
|
||||
assert_snapshot!(trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_literals() {
|
||||
let source = r"b'a' b'b' b'c'";
|
||||
|
||||
let trace = trace_visitation(source);
|
||||
|
||||
assert_snapshot!(trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f_strings() {
|
||||
let source = r"'pre' f'foo {bar:.{x}f} baz'";
|
||||
|
||||
let trace = trace_visitation(source);
|
||||
|
||||
assert_snapshot!(trace);
|
||||
}
|
||||
|
||||
fn trace_visitation(source: &str) -> String {
|
||||
let tokens = lex(source, Mode::Module);
|
||||
let parsed = parse_tokens(tokens, source, Mode::Module, "test.py").unwrap();
|
||||
@@ -281,4 +308,16 @@ impl Visitor<'_> for RecordVisitor {
|
||||
walk_type_param(self, type_param);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_string_literal(&mut self, string_literal: &StringLiteral) {
|
||||
self.enter_node(string_literal);
|
||||
walk_string_literal(self, string_literal);
|
||||
self.exit_node();
|
||||
}
|
||||
|
||||
fn visit_bytes_literal(&mut self, bytes_literal: &BytesLiteral) {
|
||||
self.enter_node(bytes_literal);
|
||||
walk_bytes_literal(self, bytes_literal);
|
||||
self.exit_node();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ countme = "3.0.1"
|
||||
itertools = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
|
||||
@@ -15,7 +15,6 @@ def g():
|
||||
# hi
|
||||
...
|
||||
|
||||
# FIXME(#8905): Uncomment, leads to unstable formatting
|
||||
# def h():
|
||||
# ...
|
||||
# # bye
|
||||
def h():
|
||||
...
|
||||
# bye
|
||||
|
||||
@@ -9,3 +9,12 @@ class MyClass:
|
||||
# fmt: on
|
||||
def method():
|
||||
print ( "str" )
|
||||
|
||||
@decor(
|
||||
a=1,
|
||||
# fmt: off
|
||||
b=(2, 3),
|
||||
# fmt: on
|
||||
)
|
||||
def func():
|
||||
pass
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# flags: --line-ranges=12-12
|
||||
# flags: --line-ranges=12-12 --line-ranges=21-21
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
@@ -10,3 +10,12 @@ class MyClass:
|
||||
# fmt: on
|
||||
def method():
|
||||
print("str")
|
||||
|
||||
@decor(
|
||||
a=1,
|
||||
# fmt: off
|
||||
b=(2, 3),
|
||||
# fmt: on
|
||||
)
|
||||
def func():
|
||||
pass
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"line_length": 6}
|
||||
{"line_width": 6}
|
||||
@@ -1 +1 @@
|
||||
{"line_length": 0}
|
||||
{"line_width": 1}
|
||||
@@ -48,7 +48,8 @@ def import_fixture(fixture: Path, fixture_set: str):
|
||||
if "--line-length=" in flags:
|
||||
[_, length_and_rest] = flags.split("--line-length=", 1)
|
||||
length = length_and_rest.split(" ", 1)[0]
|
||||
options["line_length"] = int(length)
|
||||
length = int(length)
|
||||
options["line_width"] = 1 if length == 0 else length
|
||||
|
||||
if "--skip-magic-trailing-comma" in flags:
|
||||
options["magic_trailing_comma"] = "ignore"
|
||||
|
||||
@@ -67,6 +67,27 @@ def doctest_last_line_continued():
|
||||
pass
|
||||
|
||||
|
||||
# Test that a doctest on the real last line of a docstring reformats
|
||||
# correctly.
|
||||
def doctest_really_last_line():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff( x )"""
|
||||
pass
|
||||
|
||||
|
||||
# Test that a continued doctest on the real last line of a docstring reformats
|
||||
# correctly.
|
||||
def doctest_really_last_line_continued():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
>>> cool_stuff( x )
|
||||
... more( y )"""
|
||||
pass
|
||||
|
||||
|
||||
# Test that a doctest is correctly identified and formatted with a blank
|
||||
# continuation line.
|
||||
def doctest_blank_continued():
|
||||
@@ -323,3 +344,487 @@ def doctest_invalid_skipped_with_triple_double_in_single_quote_string():
|
||||
>>> x = '\"\"\"'
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
###############################################################################
|
||||
# reStructuredText CODE EXAMPLES
|
||||
#
|
||||
# This section shows examples of docstrings that contain code snippets in
|
||||
# reStructuredText formatted code blocks.
|
||||
#
|
||||
# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#literal-blocks
|
||||
# See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block
|
||||
# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks
|
||||
# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-30
|
||||
# See: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#toc-entry-38
|
||||
###############################################################################
|
||||
|
||||
|
||||
def rst_literal_simple():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def rst_literal_simple_continued():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
def cool_stuff( x ):
|
||||
print( f"hi {x}" );
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Tests that we can end the literal block on the second
|
||||
# to last line of the docstring.
|
||||
def rst_literal_second_to_last():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Tests that we can end the literal block on the actual
|
||||
# last line of the docstring.
|
||||
def rst_literal_actually_last():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )"""
|
||||
pass
|
||||
|
||||
|
||||
def rst_literal_with_blank_lines():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
def cool_stuff( x ):
|
||||
print( f"hi {x}" );
|
||||
|
||||
def other_stuff( y ):
|
||||
print( y )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Extra blanks should be preserved.
|
||||
def rst_literal_extra_blanks():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# If a literal block is never properly ended (via a non-empty unindented line),
|
||||
# then the end of the block should be the last non-empty line. And subsequent
|
||||
# empty lines should be preserved as-is.
|
||||
def rst_literal_extra_blanks_at_end():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# A literal block can contain many empty lines and it should not end the block
|
||||
# if it continues.
|
||||
def rst_literal_extra_blanks_in_snippet():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
|
||||
cool_stuff( 2 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This tests that a unindented line appearing after an indented line (but where
|
||||
# the indent is still beyond the minimum) gets formatted properly.
|
||||
def rst_literal_subsequent_line_not_indented():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
if True:
|
||||
cool_stuff( '''
|
||||
hiya''' )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This checks that if the first line in a code snippet has been indented with
|
||||
# tabs, then so long as its "indentation length" is considered bigger than the
|
||||
# line with `::`, it is reformatted as code.
|
||||
#
|
||||
# (If your tabwidth is set to 4, then it looks like the code snippet
|
||||
# isn't indented at all, which is perhaps counter-intuitive. Indeed, reST
|
||||
# itself also seems to recognize this as a code block, although it appears
|
||||
# under-specified.)
|
||||
def rst_literal_first_line_indent_uses_tabs_4spaces():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Like the test above, but with multiple lines.
|
||||
def rst_literal_first_line_indent_uses_tabs_4spaces_multiple():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
cool_stuff( 2 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Another test with tabs, except in this case, if your tabwidth is less than
|
||||
# 8, than the code snippet actually looks like its indent is *less* than the
|
||||
# opening line with a `::`. One might presume this means that the code snippet
|
||||
# is not treated as a literal block and thus not reformatted, but since we
|
||||
# assume all tabs have tabwidth=8 when computing indentation length, the code
|
||||
# snippet is actually seen as being more indented than the opening `::` line.
|
||||
# As with the above example, reST seems to behave the same way here.
|
||||
def rst_literal_first_line_indent_uses_tabs_8spaces():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Like the test above, but with multiple lines.
|
||||
def rst_literal_first_line_indent_uses_tabs_8spaces_multiple():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
cool_stuff( 2 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Tests that if two lines in a literal block are indented to the same level
|
||||
# but by different means (tabs versus spaces), then we correctly recognize the
|
||||
# block and format it.
|
||||
def rst_literal_first_line_tab_second_line_spaces():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
cool_stuff( 2 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Tests that when two lines in a code snippet have weird and inconsistent
|
||||
# indentation, the code still gets formatted so long as the indent is greater
|
||||
# than the indent of the `::` line.
|
||||
#
|
||||
# In this case, the minimum indent is 5 spaces (from the second line) where as
|
||||
# the first line has an indent of 8 spaces via a tab (by assuming tabwidth=8).
|
||||
# The minimum indent is stripped from each code line. Since tabs aren't
|
||||
# divisible, the entire tab is stripped, which means the first and second lines
|
||||
# wind up with the same level of indentation.
|
||||
#
|
||||
# An alternative behavior here would be that the tab is replaced with 3 spaces
|
||||
# instead of being stripped entirely. The code snippet itself would then have
|
||||
# inconsistent indentation to the point of being invalid Python, and thus code
|
||||
# formatting would be skipped.
|
||||
#
|
||||
# I decided on the former behavior because it seems a bit easier to implement,
|
||||
# but we might want to switch to the alternative if cases like this show up in
|
||||
# the real world. ---AG
|
||||
def rst_literal_odd_indentation():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
cool_stuff( 2 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Tests that having a line with a lone `::` works as an introduction of a
|
||||
# literal block.
|
||||
def rst_literal_lone_colon():
|
||||
"""
|
||||
Do cool stuff.
|
||||
|
||||
::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def rst_directive_simple():
|
||||
"""
|
||||
.. code-block:: python
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def rst_directive_case_insensitive():
|
||||
"""
|
||||
.. cOdE-bLoCk:: python
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def rst_directive_sourcecode():
|
||||
"""
|
||||
.. sourcecode:: python
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def rst_directive_options():
|
||||
"""
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 2,3
|
||||
:name: blah blah
|
||||
|
||||
cool_stuff( 1 )
|
||||
cool_stuff( 2 )
|
||||
cool_stuff( 3 )
|
||||
cool_stuff( 4 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# In this case, since `pycon` isn't recognized as a Python code snippet, the
|
||||
# docstring reformatter ignores it. But it then picks up the doctest and
|
||||
# reformats it.
|
||||
def rst_directive_doctest():
|
||||
"""
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This checks that if the first non-empty line after the start of a literal
|
||||
# block is not indented more than the line containing the `::`, then it is not
|
||||
# treated as a code snippet.
|
||||
def rst_literal_skipped_first_line_not_indented():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Like the test above, but inserts an indented line after the un-indented one.
|
||||
# This should not cause the literal block to be resumed.
|
||||
def rst_literal_skipped_first_line_not_indented_then_indented():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
cool_stuff( 2 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This also checks that a code snippet is not reformatted when the indentation
|
||||
# of the first line is not more than the line with `::`, but this uses tabs to
|
||||
# make it a little more confounding. It relies on the fact that indentation
|
||||
# length is computed by assuming a tabwidth equal to 8. reST also rejects this
|
||||
# and doesn't treat it as a literal block.
|
||||
def rst_literal_skipped_first_line_not_indented_tab():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Like the previous test, but adds a second line.
|
||||
def rst_literal_skipped_first_line_not_indented_tab_multiple():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
cool_stuff( 1 )
|
||||
cool_stuff( 2 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Tests that a code block with a second line that is not properly indented gets
|
||||
# skipped. A valid code block needs to have an empty line separating these.
|
||||
#
|
||||
# One trick here is that we need to make sure the Python code in the snippet is
|
||||
# valid, otherwise it would be skipped because of invalid Python.
|
||||
def rst_literal_skipped_subsequent_line_not_indented():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
if True:
|
||||
cool_stuff( '''
|
||||
hiya''' )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# In this test, we write what looks like a code-block, but it should be treated
|
||||
# as invalid due to the missing `language` argument.
|
||||
#
|
||||
# It does still look like it could be a literal block according to the literal
|
||||
# rules, but we currently consider the `.. ` prefix to indicate that it is not
|
||||
# a literal block.
|
||||
def rst_literal_skipped_not_directive():
|
||||
"""
|
||||
.. code-block::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# In this test, we start a line with `.. `, which makes it look like it might
|
||||
# be a directive. But instead continue it as if it was just some periods from
|
||||
# the previous line, and then try to end it by starting a literal block.
|
||||
#
|
||||
# But because of the `.. ` in the beginning, we wind up not treating this as a
|
||||
# code snippet. The reST render I was using to test things does actually treat
|
||||
# this as a code block, so we may be out of conformance here.
|
||||
def rst_literal_skipped_possible_false_negative():
|
||||
"""
|
||||
This is a test.
|
||||
.. This is a test::
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This tests that a doctest inside of a reST literal block doesn't get
|
||||
# reformatted. It's plausible this isn't the right behavior, but it also seems
|
||||
# like it might be the right behavior since it is a literal block. (The doctest
|
||||
# makes the Python code invalid.)
|
||||
def rst_literal_skipped_doctest():
|
||||
"""
|
||||
Do cool stuff::
|
||||
|
||||
>>> cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def rst_directive_skipped_not_indented():
|
||||
"""
|
||||
.. code-block:: python
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def rst_directive_skipped_wrong_language():
|
||||
"""
|
||||
.. code-block:: rust
|
||||
|
||||
cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# This gets skipped for the same reason that the doctest in a literal block
|
||||
# gets skipped.
|
||||
def rst_directive_skipped_doctest():
|
||||
"""
|
||||
.. code-block:: python
|
||||
|
||||
>>> cool_stuff( 1 )
|
||||
|
||||
Done.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use std::borrow::Cow;
|
||||
// This gives tons of false positives in this file because of
|
||||
// "reStructuredText."
|
||||
#![allow(clippy::doc_markdown)]
|
||||
|
||||
use std::{borrow::Cow, collections::VecDeque};
|
||||
|
||||
use {once_cell::sync::Lazy, regex::Regex};
|
||||
|
||||
use ruff_python_trivia::PythonWhitespace;
|
||||
use {
|
||||
ruff_formatter::{write, Printed},
|
||||
ruff_formatter::{write, IndentStyle, Printed},
|
||||
ruff_python_trivia::{is_python_whitespace, PythonWhitespace},
|
||||
ruff_source_file::Locator,
|
||||
ruff_text_size::{Ranged, TextLen, TextRange, TextSize},
|
||||
};
|
||||
@@ -182,6 +188,7 @@ pub(super) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
||||
|
||||
DocstringLinePrinter {
|
||||
f,
|
||||
action_queue: VecDeque::new(),
|
||||
offset,
|
||||
stripped_indentation_length,
|
||||
already_normalized,
|
||||
@@ -218,17 +225,34 @@ fn contains_unescaped_newline(haystack: &str) -> bool {
|
||||
/// An abstraction for printing each line of a docstring.
|
||||
struct DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
|
||||
f: &'fmt mut PyFormatter<'ast, 'buf>,
|
||||
|
||||
/// A queue of actions to perform.
|
||||
///
|
||||
/// Whenever we process a line, it is possible for it to generate multiple
|
||||
/// actions to take. The most basic, and most common case, is for the line
|
||||
/// to just simply be printed as-is. But in some cases, a line is part of
|
||||
/// a code example that we'd like to reformat. In those cases, the actions
|
||||
/// can be more complicated.
|
||||
///
|
||||
/// Actions are pushed on to the end of the queue and popped from the
|
||||
/// beginning.
|
||||
action_queue: VecDeque<CodeExampleAddAction<'src>>,
|
||||
|
||||
/// The source offset of the beginning of the line that is currently being
|
||||
/// printed.
|
||||
offset: TextSize,
|
||||
|
||||
/// Indentation alignment based on the least indented line in the
|
||||
/// docstring.
|
||||
stripped_indentation_length: TextSize,
|
||||
|
||||
/// Whether the docstring is overall already considered normalized. When it
|
||||
/// is, the formatter can take a fast path.
|
||||
already_normalized: bool,
|
||||
|
||||
/// The quote style used by the docstring being printed.
|
||||
quote_style: QuoteStyle,
|
||||
|
||||
/// The current code example detected in the docstring.
|
||||
code_example: CodeExample<'src>,
|
||||
}
|
||||
@@ -253,7 +277,8 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
|
||||
self.offset += line.line.text_len() + "\n".text_len();
|
||||
self.add_one(line)?;
|
||||
}
|
||||
Ok(())
|
||||
self.code_example.finish(&mut self.action_queue);
|
||||
self.run_action_queue()
|
||||
}
|
||||
|
||||
/// Adds the given line to this printer.
|
||||
@@ -273,34 +298,40 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
|
||||
{
|
||||
return self.print_one(&line.as_output());
|
||||
}
|
||||
match self.code_example.add(line) {
|
||||
CodeExampleAddAction::Print { original } => self.print_one(&original.as_output())?,
|
||||
CodeExampleAddAction::Kept => {}
|
||||
CodeExampleAddAction::Reset { code, original } => {
|
||||
for codeline in code {
|
||||
self.print_one(&codeline.original.as_output())?;
|
||||
self.code_example.add(line, &mut self.action_queue);
|
||||
self.run_action_queue()
|
||||
}
|
||||
|
||||
/// Process any actions in this printer's queue until the queue is empty.
|
||||
fn run_action_queue(&mut self) -> FormatResult<()> {
|
||||
while let Some(action) = self.action_queue.pop_front() {
|
||||
match action {
|
||||
CodeExampleAddAction::Print { original } => {
|
||||
self.print_one(&original.as_output())?;
|
||||
}
|
||||
self.print_one(&original.as_output())?;
|
||||
}
|
||||
CodeExampleAddAction::Format { mut kind, original } => {
|
||||
let Some(formatted_lines) = self.format(kind.code())? else {
|
||||
// If formatting failed in a way that should not be
|
||||
// allowed, we back out what we're doing and print the
|
||||
// original lines we found as-is as if we did nothing.
|
||||
for codeline in kind.code() {
|
||||
CodeExampleAddAction::Kept => {}
|
||||
CodeExampleAddAction::Reset { code } => {
|
||||
for codeline in code {
|
||||
self.print_one(&codeline.original.as_output())?;
|
||||
}
|
||||
if let Some(original) = original {
|
||||
self.print_one(&original.as_output())?;
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
CodeExampleAddAction::Format { mut kind } => {
|
||||
let Some(formatted_lines) = self.format(kind.code())? else {
|
||||
// Since we've failed to emit these lines, we need to
|
||||
// put them back in the queue but have them jump to the
|
||||
// front of the queue to get processed before any other
|
||||
// action.
|
||||
self.action_queue.push_front(CodeExampleAddAction::Reset {
|
||||
code: kind.into_code(),
|
||||
});
|
||||
continue;
|
||||
};
|
||||
|
||||
self.already_normalized = false;
|
||||
match kind {
|
||||
CodeExampleKind::Doctest(CodeExampleDoctest { ps1_indent, .. }) => {
|
||||
let mut lines = formatted_lines.into_iter();
|
||||
if let Some(first) = lines.next() {
|
||||
self.already_normalized = false;
|
||||
match kind {
|
||||
CodeExampleKind::Doctest(CodeExampleDoctest { ps1_indent, .. }) => {
|
||||
let mut lines = formatted_lines.into_iter();
|
||||
let Some(first) = lines.next() else { continue };
|
||||
self.print_one(
|
||||
&first.map(|line| std::format!("{ps1_indent}>>> {line}")),
|
||||
)?;
|
||||
@@ -310,11 +341,21 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
|
||||
)?;
|
||||
}
|
||||
}
|
||||
CodeExampleKind::Rst(litblock) => {
|
||||
let Some(min_indent) = litblock.min_indent else {
|
||||
continue;
|
||||
};
|
||||
// This looks suspicious, but it's consistent with the whitespace
|
||||
// normalization that will occur anyway.
|
||||
let indent = " ".repeat(min_indent.to_usize());
|
||||
for docline in formatted_lines {
|
||||
self.print_one(
|
||||
&docline.map(|line| std::format!("{indent}{line}")),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(original) = original {
|
||||
self.print_one(&original.as_output())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -395,32 +436,37 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
|
||||
/// routine is silent about it. So from the user's perspective, this will
|
||||
/// fail silently. Ideally, this would at least emit a warning message,
|
||||
/// but at time of writing, it wasn't clear to me how to best do that.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This panics when the given slice is empty.
|
||||
fn format(
|
||||
&mut self,
|
||||
code: &[CodeExampleLine<'_>],
|
||||
) -> FormatResult<Option<Vec<OutputDocstringLine<'static>>>> {
|
||||
use ruff_python_parser::AsMode;
|
||||
|
||||
let offset = code
|
||||
.get(0)
|
||||
.expect("code blob must be non-empty")
|
||||
.original
|
||||
.offset;
|
||||
let last_line_is_last = code
|
||||
.last()
|
||||
.expect("code blob must be non-empty")
|
||||
.original
|
||||
.is_last();
|
||||
let (Some(unformatted_first), Some(unformatted_last)) = (code.first(), code.last()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let codeblob = code
|
||||
.iter()
|
||||
.map(|line| line.code)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
let printed = match docstring_format_source(self.f.options(), self.quote_style, &codeblob) {
|
||||
let options = self
|
||||
.f
|
||||
.options()
|
||||
.clone()
|
||||
// It's perhaps a little odd to be hard-coding the indent
|
||||
// style here, but I believe it is necessary as a result
|
||||
// of the whitespace normalization otherwise done in
|
||||
// docstrings. Namely, tabs are rewritten with ASCII
|
||||
// spaces. If code examples in docstrings are formatted
|
||||
// with tabs and those tabs end up getting rewritten, this
|
||||
// winds up screwing with the indentation in ways that
|
||||
// results in formatting no longer being idempotent. Since
|
||||
// tabs will get erased anyway, we just clobber them here
|
||||
// instead of later, and as a result, get more consistent
|
||||
// results.
|
||||
.with_indent_style(IndentStyle::Space);
|
||||
let printed = match docstring_format_source(options, self.quote_style, &codeblob) {
|
||||
Ok(printed) => printed,
|
||||
Err(FormatModuleError::FormatError(err)) => return Err(err),
|
||||
Err(
|
||||
@@ -461,12 +507,12 @@ impl<'ast, 'buf, 'fmt, 'src> DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
|
||||
.lines()
|
||||
.map(|line| OutputDocstringLine {
|
||||
line: Cow::Owned(line.to_string()),
|
||||
offset,
|
||||
offset: unformatted_first.original.offset,
|
||||
is_last: false,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(last) = lines.last_mut() {
|
||||
last.is_last = last_line_is_last;
|
||||
if let Some(reformatted_last) = lines.last_mut() {
|
||||
reformatted_last.is_last = unformatted_last.original.is_last();
|
||||
}
|
||||
Ok(Some(lines))
|
||||
}
|
||||
@@ -485,8 +531,10 @@ struct InputDocstringLine<'src> {
|
||||
/// unformatted line in a docstring, and owned when it corresponds to a
|
||||
/// reformatted line (e.g., from a code snippet) in a docstring.
|
||||
line: &'src str,
|
||||
|
||||
/// The offset into the source document which this line corresponds to.
|
||||
offset: TextSize,
|
||||
|
||||
/// For any input line that isn't the last line, this contains a reference
|
||||
/// to the line immediately following this one.
|
||||
///
|
||||
@@ -525,9 +573,11 @@ struct OutputDocstringLine<'src> {
|
||||
/// a line from a reformatted code snippet. In other cases, it is borrowed
|
||||
/// from the input docstring line as-is.
|
||||
line: Cow<'src, str>,
|
||||
|
||||
/// The offset into the source document which this line corresponds to.
|
||||
/// Currently, this is an estimate.
|
||||
offset: TextSize,
|
||||
|
||||
/// Whether this is the last line in a docstring or not. This is determined
|
||||
/// by whether the last line in the code snippet was also the last line in
|
||||
/// the docstring. If it was, then it follows that the last line in the
|
||||
@@ -568,38 +618,51 @@ impl<'src> CodeExample<'src> {
|
||||
/// Attempt to add an original line from a docstring to this code example.
|
||||
///
|
||||
/// Based on the line and the internal state of whether a code example is
|
||||
/// currently being collected or not, this will return an "action" for
|
||||
/// the caller to perform. The typical case is a "print" action, which
|
||||
/// instructs the caller to just print the line as though it were not part
|
||||
/// of a code snippet.
|
||||
fn add(&mut self, original: InputDocstringLine<'src>) -> CodeExampleAddAction<'src> {
|
||||
/// currently being collected or not, this will push an "action" to the
|
||||
/// given queue for the caller to perform. The typical case is a "print"
|
||||
/// action, which instructs the caller to just print the line as though it
|
||||
/// were not part of a code snippet.
|
||||
fn add(
|
||||
&mut self,
|
||||
original: InputDocstringLine<'src>,
|
||||
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
|
||||
) {
|
||||
match self.kind.take() {
|
||||
// There's no existing code example being built, so we look for
|
||||
// the start of one or otherwise tell the caller we couldn't find
|
||||
// anything.
|
||||
None => match self.add_start(original) {
|
||||
None => CodeExampleAddAction::Kept,
|
||||
Some(original) => CodeExampleAddAction::Print { original },
|
||||
},
|
||||
Some(CodeExampleKind::Doctest(mut doctest)) => {
|
||||
if doctest.add_code_line(original) {
|
||||
// Stay with the doctest kind while we accumulate all
|
||||
// PS2 prompts.
|
||||
self.kind = Some(CodeExampleKind::Doctest(doctest));
|
||||
return CodeExampleAddAction::Kept;
|
||||
}
|
||||
let original = self.add_start(original);
|
||||
CodeExampleAddAction::Format {
|
||||
kind: CodeExampleKind::Doctest(doctest),
|
||||
original,
|
||||
}
|
||||
None => {
|
||||
self.add_start(original, queue);
|
||||
}
|
||||
Some(CodeExampleKind::Doctest(doctest)) => {
|
||||
let Some(doctest) = doctest.add_code_line(original, queue) else {
|
||||
self.add_start(original, queue);
|
||||
return;
|
||||
};
|
||||
self.kind = Some(CodeExampleKind::Doctest(doctest));
|
||||
}
|
||||
Some(CodeExampleKind::Rst(litblock)) => {
|
||||
let Some(litblock) = litblock.add_code_line(original, queue) else {
|
||||
self.add_start(original, queue);
|
||||
return;
|
||||
};
|
||||
self.kind = Some(CodeExampleKind::Rst(litblock));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the code example by generating any final actions if applicable.
|
||||
///
|
||||
/// This typically adds an action when the end of a code example coincides
|
||||
/// with the end of the docstring.
|
||||
fn finish(&mut self, queue: &mut VecDeque<CodeExampleAddAction<'src>>) {
|
||||
let Some(kind) = self.kind.take() else { return };
|
||||
queue.push_back(CodeExampleAddAction::Format { kind });
|
||||
}
|
||||
|
||||
/// Looks for the start of a code example. If one was found, then the given
|
||||
/// line is kept and added as part of the code example. Otherwise, the line
|
||||
/// is returned unchanged and no code example was found.
|
||||
/// is pushed onto the queue unchanged to be printed as-is.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
@@ -609,13 +672,18 @@ impl<'src> CodeExample<'src> {
|
||||
fn add_start(
|
||||
&mut self,
|
||||
original: InputDocstringLine<'src>,
|
||||
) -> Option<InputDocstringLine<'src>> {
|
||||
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
|
||||
) {
|
||||
assert!(self.kind.is_none(), "expected no existing code example");
|
||||
if let Some(doctest) = CodeExampleDoctest::new(original) {
|
||||
self.kind = Some(CodeExampleKind::Doctest(doctest));
|
||||
return None;
|
||||
queue.push_back(CodeExampleAddAction::Kept);
|
||||
} else if let Some(litblock) = CodeExampleRst::new(original) {
|
||||
self.kind = Some(CodeExampleKind::Rst(litblock));
|
||||
queue.push_back(CodeExampleAddAction::Print { original });
|
||||
} else {
|
||||
queue.push_back(CodeExampleAddAction::Print { original });
|
||||
}
|
||||
Some(original)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,6 +701,12 @@ enum CodeExampleKind<'src> {
|
||||
///
|
||||
/// [regex matching]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L611-L622
|
||||
Doctest(CodeExampleDoctest<'src>),
|
||||
/// Code found from a reStructuredText "[literal block]" or "[code block
|
||||
/// directive]".
|
||||
///
|
||||
/// [literal block]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks
|
||||
/// [code block directive]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block
|
||||
Rst(CodeExampleRst<'src>),
|
||||
}
|
||||
|
||||
impl<'src> CodeExampleKind<'src> {
|
||||
@@ -643,6 +717,20 @@ impl<'src> CodeExampleKind<'src> {
|
||||
fn code(&mut self) -> &[CodeExampleLine<'src>] {
|
||||
match *self {
|
||||
CodeExampleKind::Doctest(ref doctest) => &doctest.lines,
|
||||
CodeExampleKind::Rst(ref mut litblock) => litblock.indented_code(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume this code example and return only the lines that have been
|
||||
/// accrued so far.
|
||||
///
|
||||
/// This is useful when the code example being collected has been
|
||||
/// determined to be invalid, and one wants to "give up" and print the
|
||||
/// original lines through unchanged without attempting formatting.
|
||||
fn into_code(self) -> Vec<CodeExampleLine<'src>> {
|
||||
match self {
|
||||
CodeExampleKind::Doctest(doctest) => doctest.lines,
|
||||
CodeExampleKind::Rst(litblock) => litblock.lines,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,6 +740,7 @@ impl<'src> CodeExampleKind<'src> {
|
||||
struct CodeExampleDoctest<'src> {
|
||||
/// The lines that have been seen so far that make up the doctest.
|
||||
lines: Vec<CodeExampleLine<'src>>,
|
||||
|
||||
/// The indent observed in the first doctest line.
|
||||
///
|
||||
/// More precisely, this corresponds to the whitespace observed before
|
||||
@@ -681,19 +770,24 @@ impl<'src> CodeExampleDoctest<'src> {
|
||||
Some(doctest)
|
||||
}
|
||||
|
||||
/// Looks for a valid doctest PS2 prompt in the line given.
|
||||
/// Looks for a valid doctest PS2 prompt in the line given. If one is
|
||||
/// found, it is added to this code example and ownership of the example is
|
||||
/// returned to the caller. In this case, callers should continue trying to
|
||||
/// add PS2 prompt lines.
|
||||
///
|
||||
/// If one is found, then the code portion of the line following the PS2 prompt
|
||||
/// is returned.
|
||||
/// But if one isn't found, then the given line is not part of the code
|
||||
/// example and ownership of this example is not returned.
|
||||
///
|
||||
/// Callers must provide a string containing the original indentation of the
|
||||
/// PS1 prompt that started the doctest containing the potential PS2 prompt
|
||||
/// in the line given. If the line contains a PS2 prompt, its indentation must
|
||||
/// match the indentation used for the corresponding PS1 prompt (otherwise
|
||||
/// `None` will be returned).
|
||||
fn add_code_line(&mut self, original: InputDocstringLine<'src>) -> bool {
|
||||
/// In either case, relevant actions will be added to the given queue to
|
||||
/// process.
|
||||
fn add_code_line(
|
||||
mut self,
|
||||
original: InputDocstringLine<'src>,
|
||||
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
|
||||
) -> Option<CodeExampleDoctest<'src>> {
|
||||
let Some((ps2_indent, ps2_after)) = original.line.split_once("...") else {
|
||||
return false;
|
||||
queue.push_back(self.into_format_action());
|
||||
return None;
|
||||
};
|
||||
// PS2 prompts must have the same indentation as their
|
||||
// corresponding PS1 prompt.[1] While the 'doctest' Python
|
||||
@@ -702,7 +796,8 @@ impl<'src> CodeExampleDoctest<'src> {
|
||||
//
|
||||
// [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L733
|
||||
if self.ps1_indent != ps2_indent {
|
||||
return false;
|
||||
queue.push_back(self.into_format_action());
|
||||
return None;
|
||||
}
|
||||
// PS2 prompts must be followed by an ASCII space character unless
|
||||
// it's an otherwise empty line[1].
|
||||
@@ -710,11 +805,354 @@ impl<'src> CodeExampleDoctest<'src> {
|
||||
// [1]: https://github.com/python/cpython/blob/0ff6368519ed7542ad8b443de01108690102420a/Lib/doctest.py#L809-L812
|
||||
let code = match ps2_after.strip_prefix(' ') {
|
||||
None if ps2_after.is_empty() => "",
|
||||
None => return false,
|
||||
None => {
|
||||
queue.push_back(self.into_format_action());
|
||||
return None;
|
||||
}
|
||||
Some(code) => code,
|
||||
};
|
||||
self.lines.push(CodeExampleLine { original, code });
|
||||
true
|
||||
queue.push_back(CodeExampleAddAction::Kept);
|
||||
Some(self)
|
||||
}
|
||||
|
||||
/// Consume this doctest and turn it into a formatting action.
|
||||
fn into_format_action(self) -> CodeExampleAddAction<'src> {
|
||||
CodeExampleAddAction::Format {
|
||||
kind: CodeExampleKind::Doctest(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// State corresponding to a single reStructuredText literal block or
|
||||
/// code-block directive.
|
||||
///
|
||||
/// While a literal block and code-block directive are technically two
|
||||
/// different reStructuredText constructs, we use one type to represent
|
||||
/// both because they are exceptionally similar. Basically, they are
|
||||
/// the same with two main differences:
|
||||
///
|
||||
/// 1. Literal blocks are began with a line that ends with `::`. Code block
|
||||
/// directives are began with a line like `.. code-block:: python`.
|
||||
/// 2. Code block directives permit a list of options as a "field list"
|
||||
/// immediately after the opening line. Literal blocks have no options.
|
||||
///
|
||||
/// Otherwise, everything else, including the indentation structure, is the
|
||||
/// same.
|
||||
#[derive(Debug)]
|
||||
struct CodeExampleRst<'src> {
|
||||
/// The lines that have been seen so far that make up the block.
|
||||
lines: Vec<CodeExampleLine<'src>>,
|
||||
|
||||
/// The indent of the line "opening" this block measured via
|
||||
/// `indentation_length`.
|
||||
///
|
||||
/// It can either be the indent of a line ending with `::` (for a literal
|
||||
/// block) or the indent of a line starting with `.. ` (a directive).
|
||||
///
|
||||
/// The content body of a block needs to be indented more than the line
|
||||
/// opening the block, so we use this indentation to look for indentation
|
||||
/// that is "more than" it.
|
||||
opening_indent: TextSize,
|
||||
|
||||
/// The minimum indent of the block measured via `indentation_length`.
|
||||
///
|
||||
/// This is `None` until the first such line is seen. If no such line is
|
||||
/// found, then we consider it an invalid block and bail out of trying to
|
||||
/// find a code snippet. Otherwise, we update this indentation as we see
|
||||
/// lines in the block with less indentation. (Usually, the minimum is the
|
||||
/// indentation of the first block, but this is not required.)
|
||||
///
|
||||
/// By construction, all lines part of the block must have at least this
|
||||
/// indentation. Additionally, it is guaranteed that the indentation length
|
||||
/// of the opening indent is strictly less than the indentation of the
|
||||
/// minimum indent. Namely, the block ends once we find a line that has
|
||||
/// been unindented to at most the indent of the opening line.
|
||||
///
|
||||
/// When the code snippet has been extracted, it is re-built before being
|
||||
/// reformatted. The minimum indent is stripped from each line when it is
|
||||
/// re-built.
|
||||
min_indent: Option<TextSize>,
|
||||
|
||||
/// Whether this is a directive block or not. When not a directive, this is
|
||||
/// a literal block. The main difference between them is that they start
|
||||
/// differently. A literal block is started merely by trailing a line with
|
||||
/// `::`. A directive block is started with `.. code-block:: python`.
|
||||
///
|
||||
/// The other difference is that directive blocks can have options
|
||||
/// (represented as a reStructuredText "field list") after the beginning of
|
||||
/// the directive and before the body content of the directive.
|
||||
is_directive: bool,
|
||||
}
|
||||
|
||||
impl<'src> CodeExampleRst<'src> {
|
||||
/// Looks for the start of a reStructuredText [literal block] or [code
|
||||
/// block directive].
|
||||
///
|
||||
/// If the start of a block is found, then this returns a correctly
|
||||
/// initialized reStructuredText block. Callers should print the line as
|
||||
/// given as it is not retained as part of the block.
|
||||
///
|
||||
/// [literal block]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#literal-blocks
|
||||
/// [code block directive]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block
|
||||
fn new(original: InputDocstringLine<'src>) -> Option<CodeExampleRst> {
|
||||
let (opening_indent, rest) = indent_with_suffix(original.line);
|
||||
if rest.starts_with(".. ") {
|
||||
if let Some(litblock) = CodeExampleRst::new_code_block(original) {
|
||||
return Some(litblock);
|
||||
}
|
||||
// In theory, we could still have something that looks like a literal block,
|
||||
// but if the line starts with `.. `, then it seems like it probably shouldn't
|
||||
// be a literal block. For example:
|
||||
//
|
||||
// .. code-block::
|
||||
//
|
||||
// cool_stuff( 1 )
|
||||
//
|
||||
// The above is not valid because the `language` argument is missing from
|
||||
// the `code-block` directive. Because of how we handle it here, the above
|
||||
// is not treated as a code snippet.
|
||||
return None;
|
||||
}
|
||||
// At this point, we know we didn't find a code block, so the only
|
||||
// thing we can hope for is a literal block which must end with a `::`.
|
||||
if !rest.trim_end().ends_with("::") {
|
||||
return None;
|
||||
}
|
||||
Some(CodeExampleRst {
|
||||
lines: vec![],
|
||||
opening_indent: indentation_length(opening_indent),
|
||||
min_indent: None,
|
||||
is_directive: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempts to create a new reStructuredText code example from a
|
||||
/// `code-block` or `sourcecode` directive. If one couldn't be found, then
|
||||
/// `None` is returned.
|
||||
fn new_code_block(original: InputDocstringLine<'src>) -> Option<CodeExampleRst> {
|
||||
// This regex attempts to parse the start of a reStructuredText code
|
||||
// block [directive]. From the reStructuredText spec:
|
||||
//
|
||||
// > Directives are indicated by an explicit markup start (".. ")
|
||||
// > followed by the directive type, two colons, and whitespace
|
||||
// > (together called the "directive marker"). Directive types
|
||||
// > are case-insensitive single words (alphanumerics plus
|
||||
// > isolated internal hyphens, underscores, plus signs, colons,
|
||||
// > and periods; no whitespace).
|
||||
//
|
||||
// The language names matched here (e.g., `python` or `py`) are taken
|
||||
// from the [Pygments lexer names], which is referenced from the docs
|
||||
// for the [code-block] directive.
|
||||
//
|
||||
// [directives]: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#directives
|
||||
// [Pygments lexer names]: https://pygments.org/docs/lexers/
|
||||
// [code-block]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block
|
||||
static DIRECTIVE_START: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r"(?m)^\s*\.\. \s*(?i:code-block|sourcecode)::\s*(?i:python|py|python3|py3)$",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
if !DIRECTIVE_START.is_match(original.line) {
|
||||
return None;
|
||||
}
|
||||
Some(CodeExampleRst {
|
||||
lines: vec![],
|
||||
opening_indent: indentation_length(original.line),
|
||||
min_indent: None,
|
||||
is_directive: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the code collected in this example as a sequence of lines.
|
||||
///
|
||||
/// The lines returned have the minimum indentation stripped from their
|
||||
/// prefix in-place. Based on the definition of minimum indentation, this
|
||||
/// implies there is at least one line in the slice returned with no
|
||||
/// whitespace prefix.
|
||||
fn indented_code(&mut self) -> &[CodeExampleLine<'src>] {
|
||||
let Some(min_indent) = self.min_indent else {
|
||||
return &[];
|
||||
};
|
||||
for line in &mut self.lines {
|
||||
line.code = if line.original.line.trim().is_empty() {
|
||||
""
|
||||
} else {
|
||||
indentation_trim(min_indent, line.original.line)
|
||||
};
|
||||
}
|
||||
&self.lines
|
||||
}
|
||||
|
||||
/// Attempts to add the given line from a docstring to the reStructuredText
|
||||
/// code snippet being collected.
|
||||
///
|
||||
/// This takes ownership of `self`, and if ownership is returned to the
|
||||
/// caller, that means the caller should continue trying to add lines to
|
||||
/// this code snippet. Otherwise, if ownership is not returned, then this
|
||||
/// implies at least one action was added to the give queue to either reset
|
||||
/// the code block or format. That is, the code snippet was either found to
|
||||
/// be invalid or it was completed and should be reformatted.
|
||||
///
|
||||
/// Note that actions may be added even if ownership is returned. For
|
||||
/// example, empty lines immediately preceding the actual code snippet will
|
||||
/// be returned back as an action to print them verbatim, but the caller
|
||||
/// should still continue to try to add lines to this code snippet.
|
||||
fn add_code_line(
|
||||
mut self,
|
||||
original: InputDocstringLine<'src>,
|
||||
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
|
||||
) -> Option<CodeExampleRst<'src>> {
|
||||
// If we haven't started populating the minimum indent yet, then
|
||||
// we haven't found the first code line and may need to find and
|
||||
// pass through leading empty lines.
|
||||
let Some(min_indent) = self.min_indent else {
|
||||
return self.add_first_line(original, queue);
|
||||
};
|
||||
let (indent, rest) = indent_with_suffix(original.line);
|
||||
if rest.is_empty() {
|
||||
// This is the standard way we close a block: when we see
|
||||
// an empty line followed by an unindented non-empty line.
|
||||
if let Some(next) = original.next {
|
||||
let (next_indent, next_rest) = indent_with_suffix(next);
|
||||
if !next_rest.is_empty() && indentation_length(next_indent) <= self.opening_indent {
|
||||
self.push_format_action(queue);
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
self.push_format_action(queue);
|
||||
return None;
|
||||
}
|
||||
self.push(original);
|
||||
queue.push_back(CodeExampleAddAction::Kept);
|
||||
return Some(self);
|
||||
}
|
||||
let indent_len = indentation_length(indent);
|
||||
if indent_len <= self.opening_indent {
|
||||
// If we find an unindented non-empty line at the same (or less)
|
||||
// indentation of the opening line at this point, then we know it
|
||||
// must be wrong because we didn't see it immediately following an
|
||||
// empty line.
|
||||
queue.push_back(self.into_reset_action());
|
||||
return None;
|
||||
} else if indent_len < min_indent {
|
||||
// While the minimum indent is usually the indentation of the first
|
||||
// line in a code snippet, it is not guaranteed to be the case.
|
||||
// And indeed, reST is happy to let blocks have a first line whose
|
||||
// indentation is greater than a subsequent line in the block. The
|
||||
// only real restriction is that every line in the block must be
|
||||
// indented at least past the indentation of the `::` line.
|
||||
self.min_indent = Some(indent_len);
|
||||
}
|
||||
self.push(original);
|
||||
queue.push_back(CodeExampleAddAction::Kept);
|
||||
Some(self)
|
||||
}
|
||||
|
||||
/// Looks for the first line in a literal or code block.
|
||||
///
|
||||
/// If a first line is found, then this returns true. Otherwise, an empty
|
||||
/// line has been found and the caller should pass it through to the
|
||||
/// docstring unchanged. (Empty lines are allowed to precede a
|
||||
/// block. And there must be at least one of them.)
|
||||
///
|
||||
/// If the given line is invalid for a reStructuredText block (i.e., no
|
||||
/// empty lines seen between the opening line), then an error variant is
|
||||
/// returned. In this case, callers should bail out of parsing this code
|
||||
/// example.
|
||||
///
|
||||
/// When this returns `true`, it is guaranteed that `self.min_indent` is
|
||||
/// set to a non-None value.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Callers must only call this when the first indentation has not yet been
|
||||
/// found. If it has, then this panics.
|
||||
fn add_first_line(
|
||||
mut self,
|
||||
original: InputDocstringLine<'src>,
|
||||
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
|
||||
) -> Option<CodeExampleRst<'src>> {
|
||||
assert!(self.min_indent.is_none());
|
||||
|
||||
// While the rst spec isn't completely clear on this point, through
|
||||
// experimentation, I found that multiple empty lines before the first
|
||||
// non-empty line are ignored.
|
||||
let (indent, rest) = indent_with_suffix(original.line);
|
||||
if rest.is_empty() {
|
||||
queue.push_back(CodeExampleAddAction::Print { original });
|
||||
return Some(self);
|
||||
}
|
||||
// Ignore parameters in field lists. These can only occur in
|
||||
// directives, not literal blocks.
|
||||
if self.is_directive && is_rst_option(rest) {
|
||||
queue.push_back(CodeExampleAddAction::Print { original });
|
||||
return Some(self);
|
||||
}
|
||||
let min_indent = indentation_length(indent);
|
||||
// At this point, we found a non-empty line. The only thing we require
|
||||
// is that its indentation is strictly greater than the indentation of
|
||||
// the line containing the `::`. Otherwise, we treat this as an invalid
|
||||
// block and bail.
|
||||
if min_indent <= self.opening_indent {
|
||||
queue.push_back(self.into_reset_action());
|
||||
return None;
|
||||
}
|
||||
self.min_indent = Some(min_indent);
|
||||
self.push(original);
|
||||
queue.push_back(CodeExampleAddAction::Kept);
|
||||
Some(self)
|
||||
}
|
||||
|
||||
/// Pushes the given line as part of this code example.
|
||||
fn push(&mut self, original: InputDocstringLine<'src>) {
|
||||
// N.B. We record the code portion as identical to the original line.
|
||||
// When we go to reformat the code lines, we change them by removing
|
||||
// the `min_indent`. This design is necessary because the true value of
|
||||
// `min_indent` isn't known until the entire block has been parsed.
|
||||
let code = original.line;
|
||||
self.lines.push(CodeExampleLine { original, code });
|
||||
}
|
||||
|
||||
/// Consume this block and add actions to the give queue for formatting.
|
||||
///
|
||||
/// This may trim lines from the end of the block and add them to the queue
|
||||
/// for printing as-is. For example, this happens when there are trailing
|
||||
/// empty lines, as we would like to preserve those since they aren't
|
||||
/// generally treated as part of the code block.
|
||||
fn push_format_action(mut self, queue: &mut VecDeque<CodeExampleAddAction<'src>>) {
|
||||
let has_non_whitespace = |line: &CodeExampleLine| {
|
||||
line.original
|
||||
.line
|
||||
.chars()
|
||||
.any(|ch| !is_python_whitespace(ch))
|
||||
};
|
||||
let first_trailing_empty_line = self
|
||||
.lines
|
||||
.iter()
|
||||
.rposition(has_non_whitespace)
|
||||
.map_or(0, |i| i + 1);
|
||||
let trailing_lines = self.lines.split_off(first_trailing_empty_line);
|
||||
queue.push_back(CodeExampleAddAction::Format {
|
||||
kind: CodeExampleKind::Rst(self),
|
||||
});
|
||||
queue.extend(
|
||||
trailing_lines
|
||||
.into_iter()
|
||||
.map(|line| CodeExampleAddAction::Print {
|
||||
original: line.original,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Consume this block and turn it into a reset action.
|
||||
///
|
||||
/// This occurs when we started collecting a code example from something
|
||||
/// that looked like a block, but later determined that it wasn't a valid
|
||||
/// block.
|
||||
fn into_reset_action(self) -> CodeExampleAddAction<'src> {
|
||||
CodeExampleAddAction::Reset { code: self.lines }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,6 +1175,7 @@ struct CodeExampleLine<'src> {
|
||||
/// example, contain a `>>> ` or `... ` prefix if this code example is a
|
||||
/// doctest.
|
||||
original: InputDocstringLine<'src>,
|
||||
|
||||
/// The code extracted from the line.
|
||||
code: &'src str,
|
||||
}
|
||||
@@ -763,20 +1202,10 @@ enum CodeExampleAddAction<'src> {
|
||||
Kept,
|
||||
/// The line added indicated that the code example is finished and should
|
||||
/// be formatted and printed. The line added is not treated as part of
|
||||
/// the code example. If the line added indicated the start of another
|
||||
/// code example, then is won't be returned to the caller here. Otherwise,
|
||||
/// callers should pass it through to the formatter as-is.
|
||||
/// the code example.
|
||||
Format {
|
||||
/// The kind of code example that was found.
|
||||
///
|
||||
/// This is guaranteed to have a non-empty code snippet.
|
||||
kind: CodeExampleKind<'src>,
|
||||
/// When set, the line is considered not part of any code example and
|
||||
/// should be formatted as if the [`Print`] action were returned.
|
||||
/// Otherwise, if there is no line, then either one does not exist
|
||||
/// or it is part of another code example and should be treated as a
|
||||
/// [`Kept`] action.
|
||||
original: Option<InputDocstringLine<'src>>,
|
||||
},
|
||||
/// This occurs when adding a line to an existing code example
|
||||
/// results in that code example becoming invalid. In this case,
|
||||
@@ -787,9 +1216,6 @@ enum CodeExampleAddAction<'src> {
|
||||
/// The lines of code that we collected but should be printed back to
|
||||
/// the docstring as-is and not formatted.
|
||||
code: Vec<CodeExampleLine<'src>>,
|
||||
/// The line that was added and triggered this reset to occur. It
|
||||
/// should be written back to the docstring as-is after the code lines.
|
||||
original: InputDocstringLine<'src>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -804,7 +1230,7 @@ enum CodeExampleAddAction<'src> {
|
||||
/// explicitly sets the context to indicate that formatting is taking place
|
||||
/// inside of a docstring.
|
||||
fn docstring_format_source(
|
||||
options: &crate::PyFormatOptions,
|
||||
options: crate::PyFormatOptions,
|
||||
docstring_quote_style: QuoteStyle,
|
||||
source: &str,
|
||||
) -> Result<Printed, FormatModuleError> {
|
||||
@@ -818,7 +1244,7 @@ fn docstring_format_source(
|
||||
let comments = crate::Comments::from_ast(&module, source_code, &comment_ranges);
|
||||
let locator = Locator::new(source);
|
||||
|
||||
let ctx = PyFormatContext::new(options.clone(), locator.contents(), comments)
|
||||
let ctx = PyFormatContext::new(options, locator.contents(), comments)
|
||||
.in_docstring(docstring_quote_style);
|
||||
let formatted = crate::format!(ctx, [module.format()])?;
|
||||
formatted
|
||||
@@ -855,6 +1281,59 @@ fn indentation_length(line: &str) -> TextSize {
|
||||
TextSize::new(indentation)
|
||||
}
|
||||
|
||||
/// Trims at most `indent_len` indentation from the beginning of `line`.
|
||||
///
|
||||
/// This treats indentation in precisely the same way as `indentation_length`.
|
||||
/// As such, it is expected that `indent_len` is computed from
|
||||
/// `indentation_length`. This is useful when one needs to trim some minimum
|
||||
/// level of indentation from a code snippet collected from a docstring before
|
||||
/// attempting to reformat it.
|
||||
fn indentation_trim(indent_len: TextSize, line: &str) -> &str {
|
||||
let mut seen_indent_len = 0u32;
|
||||
let mut trimmed = line;
|
||||
for char in line.chars() {
|
||||
if seen_indent_len >= indent_len.to_u32() {
|
||||
return trimmed;
|
||||
}
|
||||
if char == '\t' {
|
||||
// Pad to the next multiple of tab_width
|
||||
seen_indent_len += 8 - (seen_indent_len.rem_euclid(8));
|
||||
trimmed = &trimmed[1..];
|
||||
} else if char.is_whitespace() {
|
||||
seen_indent_len += u32::from(char.text_len());
|
||||
trimmed = &trimmed[char.len_utf8()..];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
/// Returns the indentation of the given line and everything following it.
|
||||
fn indent_with_suffix(line: &str) -> (&str, &str) {
|
||||
let suffix = line.trim_whitespace_start();
|
||||
let indent_len = line
|
||||
.len()
|
||||
.checked_sub(suffix.len())
|
||||
.expect("suffix <= line");
|
||||
let indent = &line[..indent_len];
|
||||
(indent, suffix)
|
||||
}
|
||||
|
||||
/// Returns true if this line looks like a reStructuredText option in a
|
||||
/// field list.
|
||||
///
|
||||
/// That is, a line that looks like `:name: optional-value`.
|
||||
fn is_rst_option(line: &str) -> bool {
|
||||
let line = line.trim_start();
|
||||
if !line.starts_with(':') {
|
||||
return false;
|
||||
}
|
||||
line.chars()
|
||||
.take_while(|&ch| !is_python_whitespace(ch))
|
||||
.any(|ch| ch == ':')
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::str::FromStr;
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(default)
|
||||
serde(default, deny_unknown_fields)
|
||||
)]
|
||||
pub struct PyFormatOptions {
|
||||
/// Whether we're in a `.py` file or `.pyi` file, which have different rules.
|
||||
|
||||
@@ -60,7 +60,7 @@ impl Transformer for Normalizer {
|
||||
}
|
||||
|
||||
fn visit_string_literal(&self, string_literal: &mut ast::StringLiteral) {
|
||||
static STRIP_CODE_SNIPPETS: Lazy<Regex> = Lazy::new(|| {
|
||||
static STRIP_DOC_TESTS: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"(?mx)
|
||||
(
|
||||
@@ -75,14 +75,27 @@ impl Transformer for Normalizer {
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
static STRIP_RST_BLOCKS: Lazy<Regex> = Lazy::new(|| {
|
||||
// This is kind of unfortunate, but it's pretty tricky (likely
|
||||
// impossible) to detect a reStructuredText block with a simple
|
||||
// regex. So we just look for the start of a block and remove
|
||||
// everything after it. Talk about a hammer.
|
||||
Regex::new(r#"::(?s:.*)"#).unwrap()
|
||||
});
|
||||
|
||||
// Start by (1) stripping everything that looks like a code
|
||||
// snippet, since code snippets may be completely reformatted if
|
||||
// they are Python code.
|
||||
string_literal.value = STRIP_CODE_SNIPPETS
|
||||
string_literal.value = STRIP_DOC_TESTS
|
||||
.replace_all(
|
||||
&string_literal.value,
|
||||
"<CODE-SNIPPET: Removed by normalizer>\n",
|
||||
"<DOCTEST-CODE-SNIPPET: Removed by normalizer>\n",
|
||||
)
|
||||
.into_owned();
|
||||
string_literal.value = STRIP_RST_BLOCKS
|
||||
.replace_all(
|
||||
&string_literal.value,
|
||||
"<RSTBLOCK-CODE-SNIPPET: Removed by normalizer>\n",
|
||||
)
|
||||
.into_owned();
|
||||
// Normalize a string by (2) stripping any leading and trailing space from each
|
||||
|
||||
@@ -22,10 +22,9 @@ def g():
|
||||
# hi
|
||||
...
|
||||
|
||||
# FIXME(#8905): Uncomment, leads to unstable formatting
|
||||
# def h():
|
||||
# ...
|
||||
# # bye
|
||||
def h():
|
||||
...
|
||||
# bye
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
@@ -41,17 +40,6 @@ def g():
|
||||
class y: ... # comment
|
||||
|
||||
# whitespace doesn't matter (note the next line has a trailing space and tab)
|
||||
@@ -13,6 +12,7 @@
|
||||
# hi
|
||||
...
|
||||
|
||||
-def h():
|
||||
- ...
|
||||
- # bye
|
||||
+# FIXME(#8905): Uncomment, leads to unstable formatting
|
||||
+# def h():
|
||||
+# ...
|
||||
+# # bye
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -71,10 +59,9 @@ def g():
|
||||
# hi
|
||||
...
|
||||
|
||||
# FIXME(#8905): Uncomment, leads to unstable formatting
|
||||
# def h():
|
||||
# ...
|
||||
# # bye
|
||||
def h():
|
||||
...
|
||||
# bye
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
@@ -16,6 +16,15 @@ class MyClass:
|
||||
# fmt: on
|
||||
def method():
|
||||
print ( "str" )
|
||||
|
||||
@decor(
|
||||
a=1,
|
||||
# fmt: off
|
||||
b=(2, 3),
|
||||
# fmt: on
|
||||
)
|
||||
def func():
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
@@ -23,8 +32,8 @@ class MyClass:
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,12 +1,10 @@
|
||||
-# flags: --line-ranges=12-12
|
||||
@@ -1,15 +1,13 @@
|
||||
-# flags: --line-ranges=12-12 --line-ranges=21-21
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
@@ -37,6 +46,15 @@ class MyClass:
|
||||
def method():
|
||||
- print("str")
|
||||
+ print ( "str" )
|
||||
|
||||
@decor(
|
||||
a=1,
|
||||
@@ -18,4 +16,4 @@
|
||||
# fmt: on
|
||||
)
|
||||
def func():
|
||||
- pass
|
||||
+ pass
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -52,12 +70,21 @@ class MyClass:
|
||||
# fmt: on
|
||||
def method():
|
||||
print ( "str" )
|
||||
|
||||
@decor(
|
||||
a=1,
|
||||
# fmt: off
|
||||
b=(2, 3),
|
||||
# fmt: on
|
||||
)
|
||||
def func():
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# flags: --line-ranges=12-12
|
||||
# flags: --line-ranges=12-12 --line-ranges=21-21
|
||||
# NOTE: If you need to modify this file, pay special attention to the --line-ranges=
|
||||
# flag above as it's formatting specifically these lines.
|
||||
|
||||
@@ -69,6 +96,15 @@ class MyClass:
|
||||
# fmt: on
|
||||
def method():
|
||||
print("str")
|
||||
|
||||
@decor(
|
||||
a=1,
|
||||
# fmt: off
|
||||
b=(2, 3),
|
||||
# fmt: on
|
||||
)
|
||||
def func():
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -13,21 +13,24 @@ importA;()<<0**0#
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,6 +1,2 @@
|
||||
importA
|
||||
-(
|
||||
- ()
|
||||
- << 0
|
||||
@@ -2,5 +2,5 @@
|
||||
(
|
||||
()
|
||||
<< 0
|
||||
- ** 0
|
||||
-) #
|
||||
+() << 0**0 #
|
||||
+ **0
|
||||
) #
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
importA
|
||||
() << 0**0 #
|
||||
(
|
||||
()
|
||||
<< 0
|
||||
**0
|
||||
) #
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -338,7 +338,7 @@ You can also change the default selection using the [`include`](settings.md#incl
|
||||
|
||||
!!! warning
|
||||
Paths provided to `include` _must_ match files. For example, `include = ["src"]` will fail since it
|
||||
matches a directory.
|
||||
matches a directory.
|
||||
|
||||
## Jupyter Notebook discovery
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ on the testing repositories:
|
||||
apk add ruff
|
||||
```
|
||||
|
||||
For **openSUSE Tumbleweed** users, Ruff is also available in the distribution repository:
|
||||
|
||||
```shell
|
||||
sudo zypper install python3-ruff
|
||||
```
|
||||
|
||||
On **Docker**, it is published as `ghcr.io/astral-sh/ruff`, tagged for each release and `latest` for
|
||||
the latest release.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user