Compare commits
5 Commits
v0.0.287
...
cli/previe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd4bf76adf | ||
|
|
6761f33600 | ||
|
|
792b76583b | ||
|
|
3e0d849c24 | ||
|
|
8b8fd411e2 |
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -4,10 +4,8 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels: ["internal"]
|
||||
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels: ["internal"]
|
||||
day: "monday"
|
||||
time: "12:00"
|
||||
timezone: "America/New_York"
|
||||
commit-message:
|
||||
prefix: "ci(deps)"
|
||||
|
||||
2
.github/workflows/docs.yaml
vendored
2
.github/workflows/docs.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: mkdocs build --strict -f mkdocs.generated.yml
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.1.1
|
||||
uses: cloudflare/wrangler-action@v3.1.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
2
.github/workflows/playground.yaml
vendored
2
.github/workflows/playground.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.1.1
|
||||
uses: cloudflare/wrangler-action@v3.1.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
@@ -129,7 +129,6 @@ At time of writing, the repository includes the following crates:
|
||||
intermediate representation. The backend for `ruff_python_formatter`.
|
||||
- `crates/ruff_index`: library crate inspired by `rustc_index`.
|
||||
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
|
||||
- `crates/ruff_notebook`: library crate for parsing and manipulating Jupyter notebooks.
|
||||
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
|
||||
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
|
||||
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
|
||||
|
||||
548
Cargo.lock
generated
548
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.0.287
|
||||
rev: v0.0.286
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -398,7 +398,6 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
- [Reflex](https://github.com/reflex-dev/reflex)
|
||||
- [Rippling](https://rippling.com)
|
||||
- [Robyn](https://github.com/sansyrox/robyn)
|
||||
- Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client))
|
||||
- Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.287"
|
||||
version = "0.0.286"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.287"
|
||||
version = "0.0.286"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -18,7 +18,6 @@ name = "ruff"
|
||||
ruff_cache = { path = "../ruff_cache" }
|
||||
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
|
||||
ruff_index = { path = "../ruff_index" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
ruff_macros = { path = "../ruff_macros" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
|
||||
ruff_python_codegen = { path = "../ruff_python_codegen" }
|
||||
@@ -65,15 +64,17 @@ schemars = { workspace = true, optional = true }
|
||||
semver = { version = "1.0.16" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { version = "3.0.0" }
|
||||
similar = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
thiserror = { version = "1.0.43" }
|
||||
toml = { workspace = true }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unicode-width = { workspace = true }
|
||||
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
|
||||
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
|
||||
wsl = { version = "0.1.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -3,20 +3,16 @@ def bar():
|
||||
|
||||
|
||||
def foo():
|
||||
"""foo""" # OK, docstrings are handled by another rule
|
||||
"""foo""" # OK
|
||||
|
||||
|
||||
def buzz():
|
||||
print("buzz") # ERROR PYI010
|
||||
print("buzz") # OK, not in stub file
|
||||
|
||||
|
||||
def foo2():
|
||||
123 # ERROR PYI010
|
||||
123 # OK, not in a stub file
|
||||
|
||||
|
||||
def bizz():
|
||||
x = 123 # ERROR PYI010
|
||||
|
||||
|
||||
def foo3():
|
||||
pass # OK, pass is handled by another rule
|
||||
x = 123 # OK, not in a stub file
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
def bar(): ... # OK
|
||||
def foo():
|
||||
"""foo""" # OK, docstrings are handled by another rule
|
||||
"""foo""" # OK, strings are handled by another rule
|
||||
|
||||
def buzz():
|
||||
print("buzz") # ERROR PYI010
|
||||
@@ -10,6 +10,3 @@ def foo2():
|
||||
|
||||
def bizz():
|
||||
x = 123 # ERROR PYI010
|
||||
|
||||
def foo3():
|
||||
pass # OK, pass is handled by another rule
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
def bar():
|
||||
... # OK
|
||||
def bar(): # OK
|
||||
...
|
||||
|
||||
|
||||
def bar():
|
||||
pass # OK
|
||||
|
||||
|
||||
def bar():
|
||||
"""oof""" # OK
|
||||
|
||||
|
||||
def oof(): # ERROR PYI048
|
||||
def oof(): # OK, docstrings are handled by another rule
|
||||
"""oof"""
|
||||
print("foo")
|
||||
|
||||
|
||||
def foo(): # ERROR PYI048
|
||||
def foo(): # Ok not in Stub file
|
||||
"""foo"""
|
||||
print("foo")
|
||||
print("foo")
|
||||
|
||||
|
||||
def buzz(): # ERROR PYI048
|
||||
def buzz(): # Ok not in Stub file
|
||||
print("fizz")
|
||||
print("buzz")
|
||||
print("test")
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
def bar(): ... # OK
|
||||
def bar():
|
||||
pass # OK
|
||||
... # OK
|
||||
|
||||
def bar():
|
||||
"""oof""" # OK
|
||||
|
||||
def oof(): # ERROR PYI048
|
||||
"""oof"""
|
||||
print("foo")
|
||||
def oof(): # OK, docstrings are handled by another rule
|
||||
"""oof"""
|
||||
print("foo")
|
||||
|
||||
def foo(): # ERROR PYI048
|
||||
|
||||
|
||||
def foo(): # ERROR PYI048
|
||||
"""foo"""
|
||||
print("foo")
|
||||
print("foo")
|
||||
|
||||
def buzz(): # ERROR PYI048
|
||||
|
||||
def buzz(): # ERROR PYI048
|
||||
print("fizz")
|
||||
print("buzz")
|
||||
print("test")
|
||||
|
||||
@@ -4,15 +4,17 @@ use std::collections::BTreeSet;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel, SourceMap};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::autofix::source_map::SourceMap;
|
||||
use crate::linter::FixTable;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
pub(crate) mod codemods;
|
||||
pub(crate) mod edits;
|
||||
pub(crate) mod snippet;
|
||||
pub(crate) mod source_map;
|
||||
|
||||
pub(crate) struct FixResult {
|
||||
/// The resulting source code, after applying all fixes.
|
||||
@@ -138,9 +140,10 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi
|
||||
mod tests {
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, SourceMarker};
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::autofix::source_map::SourceMarker;
|
||||
use crate::autofix::{apply_fixes, FixResult};
|
||||
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
|
||||
|
||||
@@ -204,8 +207,14 @@ print("hello world")
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker::new(10.into(), 10.into(),),
|
||||
SourceMarker::new(10.into(), 21.into(),),
|
||||
SourceMarker {
|
||||
source: 10.into(),
|
||||
dest: 10.into(),
|
||||
},
|
||||
SourceMarker {
|
||||
source: 10.into(),
|
||||
dest: 21.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -241,8 +250,14 @@ class A(Bar):
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker::new(8.into(), 8.into(),),
|
||||
SourceMarker::new(14.into(), 11.into(),),
|
||||
SourceMarker {
|
||||
source: 8.into(),
|
||||
dest: 8.into(),
|
||||
},
|
||||
SourceMarker {
|
||||
source: 14.into(),
|
||||
dest: 11.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -274,8 +289,14 @@ class A:
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker::new(7.into(), 7.into()),
|
||||
SourceMarker::new(15.into(), 7.into()),
|
||||
SourceMarker {
|
||||
source: 7.into(),
|
||||
dest: 7.into()
|
||||
},
|
||||
SourceMarker {
|
||||
source: 15.into(),
|
||||
dest: 7.into()
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -311,10 +332,22 @@ class A(object):
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker::new(8.into(), 8.into()),
|
||||
SourceMarker::new(16.into(), 8.into()),
|
||||
SourceMarker::new(22.into(), 14.into(),),
|
||||
SourceMarker::new(30.into(), 14.into(),),
|
||||
SourceMarker {
|
||||
source: 8.into(),
|
||||
dest: 8.into()
|
||||
},
|
||||
SourceMarker {
|
||||
source: 16.into(),
|
||||
dest: 8.into()
|
||||
},
|
||||
SourceMarker {
|
||||
source: 22.into(),
|
||||
dest: 14.into(),
|
||||
},
|
||||
SourceMarker {
|
||||
source: 30.into(),
|
||||
dest: 14.into(),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -349,8 +382,14 @@ class A:
|
||||
assert_eq!(
|
||||
source_map.markers(),
|
||||
&[
|
||||
SourceMarker::new(7.into(), 7.into(),),
|
||||
SourceMarker::new(15.into(), 7.into(),),
|
||||
SourceMarker {
|
||||
source: 7.into(),
|
||||
dest: 7.into(),
|
||||
},
|
||||
SourceMarker {
|
||||
source: 15.into(),
|
||||
dest: 7.into(),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::Edit;
|
||||
use ruff_diagnostics::Edit;
|
||||
|
||||
/// Lightweight sourcemap marker representing the source and destination
|
||||
/// position for an [`Edit`].
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SourceMarker {
|
||||
pub(crate) struct SourceMarker {
|
||||
/// Position of the marker in the original source.
|
||||
source: TextSize,
|
||||
pub(crate) source: TextSize,
|
||||
/// Position of the marker in the transformed code.
|
||||
dest: TextSize,
|
||||
}
|
||||
|
||||
impl SourceMarker {
|
||||
pub fn new(source: TextSize, dest: TextSize) -> Self {
|
||||
Self { source, dest }
|
||||
}
|
||||
|
||||
pub const fn source(&self) -> TextSize {
|
||||
self.source
|
||||
}
|
||||
|
||||
pub const fn dest(&self) -> TextSize {
|
||||
self.dest
|
||||
}
|
||||
pub(crate) dest: TextSize,
|
||||
}
|
||||
|
||||
/// A collection of [`SourceMarker`].
|
||||
@@ -32,12 +18,12 @@ impl SourceMarker {
|
||||
/// the transformed code. Here, only the boundaries of edits are tracked instead
|
||||
/// of every single character.
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
pub struct SourceMap(Vec<SourceMarker>);
|
||||
pub(crate) struct SourceMap(Vec<SourceMarker>);
|
||||
|
||||
impl SourceMap {
|
||||
/// Returns a slice of all the markers in the sourcemap in the order they
|
||||
/// were added.
|
||||
pub fn markers(&self) -> &[SourceMarker] {
|
||||
pub(crate) fn markers(&self) -> &[SourceMarker] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
@@ -45,7 +31,7 @@ impl SourceMap {
|
||||
///
|
||||
/// The `output_length` is the length of the transformed string before the
|
||||
/// edit is applied.
|
||||
pub fn push_start_marker(&mut self, edit: &Edit, output_length: TextSize) {
|
||||
pub(crate) fn push_start_marker(&mut self, edit: &Edit, output_length: TextSize) {
|
||||
self.0.push(SourceMarker {
|
||||
source: edit.start(),
|
||||
dest: output_length,
|
||||
@@ -56,7 +42,7 @@ impl SourceMap {
|
||||
///
|
||||
/// The `output_length` is the length of the transformed string after the
|
||||
/// edit has been applied.
|
||||
pub fn push_end_marker(&mut self, edit: &Edit, output_length: TextSize) {
|
||||
pub(crate) fn push_end_marker(&mut self, edit: &Edit, output_length: TextSize) {
|
||||
if edit.is_insertion() {
|
||||
self.0.push(SourceMarker {
|
||||
source: edit.start(),
|
||||
@@ -51,8 +51,8 @@ impl PartialEq<&str> for NoqaCode {
|
||||
pub enum RuleGroup {
|
||||
/// The rule has not been assigned to any specific group.
|
||||
Unspecified,
|
||||
/// The rule is still under development, and must be enabled explicitly.
|
||||
Nursery,
|
||||
/// The rule is unstable, and must be enabled explicitly or by enabling preview.
|
||||
Preview,
|
||||
}
|
||||
|
||||
#[ruff_macros::map_codes]
|
||||
@@ -64,39 +64,39 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
Some(match (linter, code) {
|
||||
// pycodestyle errors
|
||||
(Pycodestyle, "E101") => (RuleGroup::Unspecified, rules::pycodestyle::rules::MixedSpacesAndTabs),
|
||||
(Pycodestyle, "E111") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple),
|
||||
(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),
|
||||
(Pycodestyle, "E113") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation),
|
||||
(Pycodestyle, "E114") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment),
|
||||
(Pycodestyle, "E115") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment),
|
||||
(Pycodestyle, "E116") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment),
|
||||
(Pycodestyle, "E117") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::OverIndented),
|
||||
(Pycodestyle, "E201") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
|
||||
(Pycodestyle, "E202") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
|
||||
(Pycodestyle, "E203") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
|
||||
(Pycodestyle, "E211") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
|
||||
(Pycodestyle, "E221") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
|
||||
(Pycodestyle, "E222") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
|
||||
(Pycodestyle, "E223") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeOperator),
|
||||
(Pycodestyle, "E224") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterOperator),
|
||||
(Pycodestyle, "E225") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator),
|
||||
(Pycodestyle, "E226") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator),
|
||||
(Pycodestyle, "E227") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator),
|
||||
(Pycodestyle, "E228") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator),
|
||||
(Pycodestyle, "E231") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespace),
|
||||
(Pycodestyle, "E241") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma),
|
||||
(Pycodestyle, "E242") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterComma),
|
||||
(Pycodestyle, "E251") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals),
|
||||
(Pycodestyle, "E252") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals),
|
||||
(Pycodestyle, "E261") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment),
|
||||
(Pycodestyle, "E262") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment),
|
||||
(Pycodestyle, "E265") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment),
|
||||
(Pycodestyle, "E266") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment),
|
||||
(Pycodestyle, "E271") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword),
|
||||
(Pycodestyle, "E272") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword),
|
||||
(Pycodestyle, "E273") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterKeyword),
|
||||
(Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
|
||||
(Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
|
||||
(Pycodestyle, "E111") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple),
|
||||
(Pycodestyle, "E112") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),
|
||||
(Pycodestyle, "E113") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation),
|
||||
(Pycodestyle, "E114") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment),
|
||||
(Pycodestyle, "E115") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment),
|
||||
(Pycodestyle, "E116") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment),
|
||||
(Pycodestyle, "E117") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::OverIndented),
|
||||
(Pycodestyle, "E201") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
|
||||
(Pycodestyle, "E202") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
|
||||
(Pycodestyle, "E203") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
|
||||
(Pycodestyle, "E211") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
|
||||
(Pycodestyle, "E221") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
|
||||
(Pycodestyle, "E222") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
|
||||
(Pycodestyle, "E223") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabBeforeOperator),
|
||||
(Pycodestyle, "E224") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterOperator),
|
||||
(Pycodestyle, "E225") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator),
|
||||
(Pycodestyle, "E226") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator),
|
||||
(Pycodestyle, "E227") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator),
|
||||
(Pycodestyle, "E228") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator),
|
||||
(Pycodestyle, "E231") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespace),
|
||||
(Pycodestyle, "E241") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma),
|
||||
(Pycodestyle, "E242") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterComma),
|
||||
(Pycodestyle, "E251") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals),
|
||||
(Pycodestyle, "E252") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals),
|
||||
(Pycodestyle, "E261") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment),
|
||||
(Pycodestyle, "E262") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment),
|
||||
(Pycodestyle, "E265") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment),
|
||||
(Pycodestyle, "E266") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment),
|
||||
(Pycodestyle, "E271") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword),
|
||||
(Pycodestyle, "E272") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword),
|
||||
(Pycodestyle, "E273") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabAfterKeyword),
|
||||
(Pycodestyle, "E274") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
|
||||
(Pycodestyle, "E275") => (RuleGroup::Preview, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
|
||||
(Pycodestyle, "E401") => (RuleGroup::Unspecified, rules::pycodestyle::rules::MultipleImportsOnOneLine),
|
||||
(Pycodestyle, "E402") => (RuleGroup::Unspecified, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile),
|
||||
(Pycodestyle, "E501") => (RuleGroup::Unspecified, rules::pycodestyle::rules::LineTooLong),
|
||||
@@ -176,7 +176,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots),
|
||||
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
|
||||
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
|
||||
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
|
||||
(Pylint, "C1901") => (RuleGroup::Preview, rules::pylint::rules::CompareToEmptyString),
|
||||
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
|
||||
(Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit),
|
||||
(Pylint, "E0101") => (RuleGroup::Unspecified, rules::pylint::rules::ReturnInInit),
|
||||
@@ -216,7 +216,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
|
||||
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
|
||||
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
|
||||
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
|
||||
(Pylint, "R6301") => (RuleGroup::Preview, rules::pylint::rules::NoSelfUse),
|
||||
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
|
||||
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
|
||||
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),
|
||||
@@ -228,9 +228,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W1508") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidEnvvarDefault),
|
||||
(Pylint, "W1509") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessPopenPreexecFn),
|
||||
(Pylint, "W1510") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessRunWithoutCheck),
|
||||
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
|
||||
(Pylint, "W1641") => (RuleGroup::Preview, rules::pylint::rules::EqWithoutHash),
|
||||
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
|
||||
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
|
||||
(Pylint, "W3201") => (RuleGroup::Preview, rules::pylint::rules::BadDunderMethodName),
|
||||
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
|
||||
|
||||
// flake8-async
|
||||
@@ -403,7 +403,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault),
|
||||
|
||||
// flake8-copyright
|
||||
(Flake8Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice),
|
||||
(Flake8Copyright, "001") => (RuleGroup::Preview, rules::flake8_copyright::rules::MissingCopyrightNotice),
|
||||
|
||||
// pyupgrade
|
||||
(Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType),
|
||||
@@ -815,10 +815,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault),
|
||||
(Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional),
|
||||
#[cfg(feature = "unreachable-code")] // When removing this feature gate, also update rules_selector.rs
|
||||
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
|
||||
(Ruff, "014") => (RuleGroup::Preview, rules::ruff::rules::UnreachableCode),
|
||||
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
|
||||
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
|
||||
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
|
||||
(Ruff, "017") => (RuleGroup::Preview, rules::ruff::rules::QuadraticListSummation),
|
||||
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
|
||||
|
||||
@@ -866,9 +866,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Slots, "002") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
|
||||
|
||||
// refurb
|
||||
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
|
||||
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
|
||||
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
|
||||
(Refurb, "113") => (RuleGroup::Preview, rules::refurb::rules::RepeatedAppend),
|
||||
(Refurb, "131") => (RuleGroup::Preview, rules::refurb::rules::DeleteFullSlice),
|
||||
(Refurb, "132") => (RuleGroup::Preview, rules::refurb::rules::CheckAndRemoveFromSet),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::iter;
|
||||
use std::path::Path;
|
||||
use std::{io, iter};
|
||||
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::Serialize;
|
||||
use serde_json::error::Category;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use ruff_diagnostics::{SourceMap, SourceMarker};
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_parser::lexer::lex;
|
||||
use ruff_python_parser::Mode;
|
||||
use ruff_source_file::{NewlineWithTrailingNewline, UniversalNewlineIterator};
|
||||
use ruff_text_size::TextSize;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::index::NotebookIndex;
|
||||
use crate::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
|
||||
use crate::autofix::source_map::{SourceMap, SourceMarker};
|
||||
use crate::jupyter::index::NotebookIndex;
|
||||
use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
|
||||
use crate::rules::pycodestyle::rules::SyntaxError;
|
||||
use crate::IOError;
|
||||
|
||||
pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
|
||||
|
||||
/// Run round-trip source code generation on a given Jupyter notebook file path.
|
||||
pub fn round_trip(path: &Path) -> anyhow::Result<String> {
|
||||
@@ -31,7 +37,7 @@ pub fn round_trip(path: &Path) -> anyhow::Result<String> {
|
||||
let code = notebook.source_code().to_string();
|
||||
notebook.update_cell_content(&code);
|
||||
let mut writer = Vec::new();
|
||||
notebook.write(&mut writer)?;
|
||||
notebook.write_inner(&mut writer)?;
|
||||
Ok(String::from_utf8(writer)?)
|
||||
}
|
||||
|
||||
@@ -90,21 +96,6 @@ impl Cell {
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that can occur while deserializing a Jupyter Notebook.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NotebookError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Json(serde_json::Error),
|
||||
#[error("Expected a Jupyter Notebook, which must be internally stored as JSON, but this file isn't valid JSON: {0}")]
|
||||
InvalidJson(serde_json::Error),
|
||||
#[error("This file does not match the schema expected of Jupyter Notebooks: {0}")]
|
||||
InvalidSchema(serde_json::Error),
|
||||
#[error("Expected Jupyter Notebook format 4, found: {0}")]
|
||||
InvalidFormat(i64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Notebook {
|
||||
/// Python source code of the notebook.
|
||||
@@ -130,12 +121,19 @@ pub struct Notebook {
|
||||
|
||||
impl Notebook {
|
||||
/// Read the Jupyter Notebook from the given [`Path`].
|
||||
pub fn from_path(path: &Path) -> Result<Self, NotebookError> {
|
||||
Self::from_reader(BufReader::new(File::open(path)?))
|
||||
pub fn from_path(path: &Path) -> Result<Self, Box<Diagnostic>> {
|
||||
Self::from_reader(BufReader::new(File::open(path).map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?))
|
||||
}
|
||||
|
||||
/// Read the Jupyter Notebook from its JSON string.
|
||||
pub fn from_source_code(source_code: &str) -> Result<Self, NotebookError> {
|
||||
pub fn from_source_code(source_code: &str) -> Result<Self, Box<Diagnostic>> {
|
||||
Self::from_reader(Cursor::new(source_code))
|
||||
}
|
||||
|
||||
@@ -143,7 +141,7 @@ impl Notebook {
|
||||
///
|
||||
/// See also the black implementation
|
||||
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
|
||||
fn from_reader<R>(mut reader: R) -> Result<Self, NotebookError>
|
||||
fn from_reader<R>(mut reader: R) -> Result<Self, Box<Diagnostic>>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
@@ -151,27 +149,95 @@ impl Notebook {
|
||||
let mut buf = [0; 1];
|
||||
reader.read_exact(&mut buf).is_ok_and(|_| buf[0] == b'\n')
|
||||
});
|
||||
reader.rewind()?;
|
||||
reader.rewind().map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
|
||||
Ok(notebook) => notebook,
|
||||
Err(err) => {
|
||||
// Translate the error into a diagnostic
|
||||
return Err(match err.classify() {
|
||||
Category::Io => NotebookError::Json(err),
|
||||
Category::Syntax | Category::Eof => NotebookError::InvalidJson(err),
|
||||
Category::Data => {
|
||||
// We could try to read the schema version here but if this fails it's
|
||||
// a bug anyway.
|
||||
NotebookError::InvalidSchema(err)
|
||||
return Err(Box::new({
|
||||
match err.classify() {
|
||||
Category::Io => Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
),
|
||||
Category::Syntax | Category::Eof => {
|
||||
// Maybe someone saved the python sources (those with the `# %%` separator)
|
||||
// as jupyter notebook instead. Let's help them.
|
||||
let mut contents = String::new();
|
||||
reader
|
||||
.rewind()
|
||||
.and_then(|_| reader.read_to_string(&mut contents))
|
||||
.map_err(|err| {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Check if tokenizing was successful and the file is non-empty
|
||||
if lex(&contents, Mode::Module).any(|result| result.is_err()) {
|
||||
Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"A Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT}) must internally be JSON, \
|
||||
but this file isn't valid JSON: {err}"
|
||||
),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
} else {
|
||||
Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT}), \
|
||||
which must be internally stored as JSON, \
|
||||
but found a Python source file: {err}"
|
||||
),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Category::Data => {
|
||||
// We could try to read the schema version here but if this fails it's
|
||||
// a bug anyway
|
||||
Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"This file does not match the schema expected of Jupyter Notebooks: {err}"
|
||||
),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// v4 is what everybody uses
|
||||
if raw_notebook.nbformat != 4 {
|
||||
// bail because we should have already failed at the json schema stage
|
||||
return Err(NotebookError::InvalidFormat(raw_notebook.nbformat));
|
||||
return Err(Box::new(Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: format!(
|
||||
"Expected Jupyter Notebook format 4, found {}",
|
||||
raw_notebook.nbformat
|
||||
),
|
||||
},
|
||||
TextRange::default(),
|
||||
)));
|
||||
}
|
||||
|
||||
let valid_code_cells = raw_notebook
|
||||
@@ -238,13 +304,13 @@ impl Notebook {
|
||||
// The first offset is always going to be at 0, so skip it.
|
||||
for offset in self.cell_offsets.iter_mut().skip(1).rev() {
|
||||
let closest_marker = match last_marker {
|
||||
Some(marker) if marker.source() <= *offset => marker,
|
||||
Some(marker) if marker.source <= *offset => marker,
|
||||
_ => {
|
||||
let Some(marker) = source_map
|
||||
.markers()
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|marker| marker.source() <= *offset)
|
||||
.find(|m| m.source <= *offset)
|
||||
else {
|
||||
// There are no markers above the current offset, so we can
|
||||
// stop here.
|
||||
@@ -255,9 +321,9 @@ impl Notebook {
|
||||
}
|
||||
};
|
||||
|
||||
match closest_marker.source().cmp(&closest_marker.dest()) {
|
||||
Ordering::Less => *offset += closest_marker.dest() - closest_marker.source(),
|
||||
Ordering::Greater => *offset -= closest_marker.source() - closest_marker.dest(),
|
||||
match closest_marker.source.cmp(&closest_marker.dest) {
|
||||
Ordering::Less => *offset += closest_marker.dest - closest_marker.source,
|
||||
Ordering::Greater => *offset -= closest_marker.source - closest_marker.dest,
|
||||
Ordering::Equal => (),
|
||||
}
|
||||
}
|
||||
@@ -365,23 +431,18 @@ impl Notebook {
|
||||
/// The index is built only once when required. This is only used to
|
||||
/// report diagnostics, so by that time all of the autofixes must have
|
||||
/// been applied if `--fix` was passed.
|
||||
pub fn index(&self) -> &NotebookIndex {
|
||||
pub(crate) fn index(&self) -> &NotebookIndex {
|
||||
self.index.get_or_init(|| self.build_index())
|
||||
}
|
||||
|
||||
/// Return the cell offsets for the concatenated source code corresponding
|
||||
/// the Jupyter notebook.
|
||||
pub fn cell_offsets(&self) -> &[TextSize] {
|
||||
pub(crate) fn cell_offsets(&self) -> &[TextSize] {
|
||||
&self.cell_offsets
|
||||
}
|
||||
|
||||
/// Return `true` if the notebook has a trailing newline, `false` otherwise.
|
||||
pub fn trailing_newline(&self) -> bool {
|
||||
self.trailing_newline
|
||||
}
|
||||
|
||||
/// Update the notebook with the given sourcemap and transformed content.
|
||||
pub fn update(&mut self, source_map: &SourceMap, transformed: String) {
|
||||
pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: String) {
|
||||
// Cell offsets must be updated before updating the cell content as
|
||||
// it depends on the offsets to extract the cell content.
|
||||
self.index.take();
|
||||
@@ -404,8 +465,7 @@ impl Notebook {
|
||||
.map_or(true, |language| language.name == "python")
|
||||
}
|
||||
|
||||
/// Write the notebook back to the given [`Write`] implementor.
|
||||
pub fn write(&self, writer: &mut dyn Write) -> anyhow::Result<()> {
|
||||
fn write_inner(&self, writer: &mut impl Write) -> anyhow::Result<()> {
|
||||
// https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041
|
||||
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
|
||||
let mut serializer = serde_json::Serializer::with_formatter(writer, formatter);
|
||||
@@ -415,6 +475,13 @@ impl Notebook {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write back with an indent of 1, just like black
|
||||
pub fn write(&self, path: &Path) -> anyhow::Result<()> {
|
||||
let mut writer = BufWriter::new(File::create(path)?);
|
||||
self.write_inner(&mut writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -424,41 +491,58 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::{Cell, Notebook, NotebookError, NotebookIndex};
|
||||
use crate::jupyter::index::NotebookIndex;
|
||||
use crate::jupyter::schema::Cell;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::registry::Rule;
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::test::{
|
||||
read_jupyter_notebook, test_contents, test_notebook_path, test_resource_path,
|
||||
TestedNotebook,
|
||||
};
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
|
||||
fn notebook_path(path: impl AsRef<Path>) -> std::path::PathBuf {
|
||||
Path::new("./resources/test/fixtures/jupyter").join(path)
|
||||
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
|
||||
fn read_jupyter_cell(path: impl AsRef<Path>) -> Result<Cell> {
|
||||
let path = test_resource_path("fixtures/jupyter/cell").join(path);
|
||||
let source_code = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&source_code)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_python() -> Result<(), NotebookError> {
|
||||
let notebook = Notebook::from_path(¬ebook_path("valid.ipynb"))?;
|
||||
assert!(notebook.is_python_notebook());
|
||||
Ok(())
|
||||
fn test_valid() {
|
||||
assert!(read_jupyter_notebook(Path::new("valid.ipynb")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r() -> Result<(), NotebookError> {
|
||||
let notebook = Notebook::from_path(¬ebook_path("R.ipynb"))?;
|
||||
assert!(!notebook.is_python_notebook());
|
||||
Ok(())
|
||||
fn test_r() {
|
||||
// We can load this, it will be filtered out later
|
||||
assert!(read_jupyter_notebook(Path::new("R.ipynb")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
assert!(matches!(
|
||||
Notebook::from_path(¬ebook_path("invalid_extension.ipynb")),
|
||||
Err(NotebookError::InvalidJson(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
Notebook::from_path(¬ebook_path("not_json.ipynb")),
|
||||
Err(NotebookError::InvalidJson(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
Notebook::from_path(¬ebook_path("wrong_schema.ipynb")),
|
||||
Err(NotebookError::InvalidSchema(_))
|
||||
));
|
||||
let path = Path::new("resources/test/fixtures/jupyter/invalid_extension.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: Expected a Jupyter Notebook (.ipynb), \
|
||||
which must be internally stored as JSON, \
|
||||
but found a Python source file: \
|
||||
expected value at line 1 column 1"
|
||||
);
|
||||
let path = Path::new("resources/test/fixtures/jupyter/not_json.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: A Jupyter Notebook (.ipynb) must internally be JSON, \
|
||||
but this file isn't valid JSON: \
|
||||
expected value at line 1 column 1"
|
||||
);
|
||||
let path = Path::new("resources/test/fixtures/jupyter/wrong_schema.ipynb");
|
||||
assert_eq!(
|
||||
Notebook::from_path(path).unwrap_err().kind.body,
|
||||
"SyntaxError: This file does not match the schema expected of Jupyter Notebooks: \
|
||||
missing field `cells` at line 1 column 2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case(Path::new("markdown.json"), false; "markdown")]
|
||||
@@ -467,20 +551,13 @@ mod tests {
|
||||
#[test_case(Path::new("only_code.json"), true; "only_code")]
|
||||
#[test_case(Path::new("cell_magic.json"), false; "cell_magic")]
|
||||
fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> {
|
||||
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
|
||||
fn read_jupyter_cell(path: impl AsRef<Path>) -> Result<Cell> {
|
||||
let path = notebook_path("cell").join(path);
|
||||
let source_code = std::fs::read_to_string(path)?;
|
||||
Ok(serde_json::from_str(&source_code)?)
|
||||
}
|
||||
|
||||
assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concat_notebook() -> Result<(), NotebookError> {
|
||||
let notebook = Notebook::from_path(¬ebook_path("valid.ipynb"))?;
|
||||
fn test_concat_notebook() -> Result<()> {
|
||||
let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?;
|
||||
assert_eq!(
|
||||
notebook.source_code,
|
||||
r#"def unused_variable():
|
||||
@@ -520,4 +597,110 @@ print("after empty cells")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_sorting() -> Result<()> {
|
||||
let path = "isort.ipynb".to_string();
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("isort_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnsortedImports),
|
||||
)?;
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipy_escape_command() -> Result<()> {
|
||||
let path = "ipy_escape_command.ipynb".to_string();
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("ipy_escape_command_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unused_variable() -> Result<()> {
|
||||
let path = "unused_variable.ipynb".to_string();
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&path,
|
||||
Path::new("unused_variable_expected.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedVariable),
|
||||
)?;
|
||||
assert_messages!(messages, path, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_consistency() -> Result<()> {
|
||||
let path = "before_fix.ipynb".to_string();
|
||||
let TestedNotebook {
|
||||
linted_notebook: fixed_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
path,
|
||||
Path::new("after_fix.ipynb"),
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
let mut writer = Vec::new();
|
||||
fixed_notebook.write_inner(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
let expected =
|
||||
std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?;
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("before_fix.ipynb"), true; "trailing_newline")]
|
||||
#[test_case(Path::new("no_trailing_newline.ipynb"), false; "no_trailing_newline")]
|
||||
fn test_trailing_newline(path: &Path, trailing_newline: bool) -> Result<()> {
|
||||
let notebook = read_jupyter_notebook(path)?;
|
||||
assert_eq!(notebook.trailing_newline, trailing_newline);
|
||||
|
||||
let mut writer = Vec::new();
|
||||
notebook.write_inner(&mut writer)?;
|
||||
let string = String::from_utf8(writer)?;
|
||||
assert_eq!(string.ends_with('\n'), trailing_newline);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Version <4.5, don't emit cell ids
|
||||
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
|
||||
// Version 4.5, cell ids are missing and need to be added
|
||||
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
|
||||
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
|
||||
let source_notebook = read_jupyter_notebook(path)?;
|
||||
let source_kind = SourceKind::IpyNotebook(source_notebook);
|
||||
let (_, transformed) = test_contents(
|
||||
&source_kind,
|
||||
path,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
);
|
||||
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
|
||||
let mut writer = Vec::new();
|
||||
linted_notebook.write_inner(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
if has_id {
|
||||
assert!(actual.contains(r#""id": ""#));
|
||||
} else {
|
||||
assert!(!actual.contains(r#""id":"#));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
|
||||
///
|
||||
/// use serde::Serialize;
|
||||
///
|
||||
/// use ruff_notebook::SortAlphabetically;
|
||||
/// use ruff::jupyter::SortAlphabetically;
|
||||
///
|
||||
/// #[derive(Serialize)]
|
||||
/// struct MyStruct {
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: crates/ruff/src/linter.rs
|
||||
source: crates/ruff/src/jupyter/notebook.rs
|
||||
---
|
||||
isort.ipynb:cell 1:1:1: I001 [*] Import block is un-sorted or un-formatted
|
||||
|
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: crates/ruff/src/linter.rs
|
||||
source: crates/ruff/src/jupyter/notebook.rs
|
||||
---
|
||||
ipy_escape_command.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
||||
|
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: crates/ruff/src/linter.rs
|
||||
source: crates/ruff/src/jupyter/notebook.rs
|
||||
---
|
||||
unused_variable.ipynb:cell 1:2:5: F841 [*] Local variable `foo1` is assigned to but never used
|
||||
|
|
||||
@@ -6,7 +6,7 @@
|
||||
//! [Ruff]: https://github.com/astral-sh/ruff
|
||||
|
||||
pub use rule_selector::RuleSelector;
|
||||
pub use rules::pycodestyle::rules::{IOError, SyntaxError};
|
||||
pub use rules::pycodestyle::rules::IOError;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -20,6 +20,7 @@ mod doc_lines;
|
||||
mod docstrings;
|
||||
pub mod fs;
|
||||
mod importer;
|
||||
pub mod jupyter;
|
||||
mod lex;
|
||||
pub mod line_width;
|
||||
pub mod linter;
|
||||
|
||||
@@ -6,6 +6,8 @@ use anyhow::{anyhow, Result};
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use log::error;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::{AsMode, ParseError};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
@@ -13,8 +15,7 @@ use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::{AsMode, ParseError};
|
||||
|
||||
use ruff_source_file::{Locator, SourceFileBuilder};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -608,133 +609,3 @@ This indicates a bug in `{}`. If you could open an issue at:
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::test::{test_contents, test_notebook_path, TestedNotebook};
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
|
||||
fn notebook_path(path: impl AsRef<Path>) -> std::path::PathBuf {
|
||||
Path::new("../ruff_notebook/resources/test/fixtures/jupyter").join(path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_sorting() -> Result<(), NotebookError> {
|
||||
let actual = notebook_path("isort.ipynb");
|
||||
let expected = notebook_path("isort_expected.ipynb");
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::Settings::for_rule(Rule::UnsortedImports),
|
||||
)?;
|
||||
assert_messages!(messages, actual, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipy_escape_command() -> Result<(), NotebookError> {
|
||||
let actual = notebook_path("ipy_escape_command.ipynb");
|
||||
let expected = notebook_path("ipy_escape_command_expected.ipynb");
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
assert_messages!(messages, actual, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unused_variable() -> Result<(), NotebookError> {
|
||||
let actual = notebook_path("unused_variable.ipynb");
|
||||
let expected = notebook_path("unused_variable_expected.ipynb");
|
||||
let TestedNotebook {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::Settings::for_rule(Rule::UnusedVariable),
|
||||
)?;
|
||||
assert_messages!(messages, actual, source_notebook);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_consistency() -> Result<()> {
|
||||
let actual_path = notebook_path("before_fix.ipynb");
|
||||
let expected_path = notebook_path("after_fix.ipynb");
|
||||
|
||||
let TestedNotebook {
|
||||
linted_notebook: fixed_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
actual_path,
|
||||
&expected_path,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
)?;
|
||||
let mut writer = Vec::new();
|
||||
fixed_notebook.write(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
let expected = std::fs::read_to_string(expected_path)?;
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("before_fix.ipynb"), true; "trailing_newline")]
|
||||
#[test_case(Path::new("no_trailing_newline.ipynb"), false; "no_trailing_newline")]
|
||||
fn test_trailing_newline(path: &Path, trailing_newline: bool) -> Result<()> {
|
||||
let notebook = Notebook::from_path(¬ebook_path(path))?;
|
||||
assert_eq!(notebook.trailing_newline(), trailing_newline);
|
||||
|
||||
let mut writer = Vec::new();
|
||||
notebook.write(&mut writer)?;
|
||||
let string = String::from_utf8(writer)?;
|
||||
assert_eq!(string.ends_with('\n'), trailing_newline);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Version <4.5, don't emit cell ids
|
||||
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
|
||||
// Version 4.5, cell ids are missing and need to be added
|
||||
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
|
||||
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
|
||||
let source_notebook = Notebook::from_path(¬ebook_path(path))?;
|
||||
let source_kind = SourceKind::IpyNotebook(source_notebook);
|
||||
let (_, transformed) = test_contents(
|
||||
&source_kind,
|
||||
path,
|
||||
&settings::Settings::for_rule(Rule::UnusedImport),
|
||||
);
|
||||
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
|
||||
let mut writer = Vec::new();
|
||||
linted_notebook.write(&mut writer)?;
|
||||
let actual = String::from_utf8(writer)?;
|
||||
if has_id {
|
||||
assert!(actual.contains(r#""id": ""#));
|
||||
} else {
|
||||
assert!(!actual.contains(r#""id":"#));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use ruff_python_parser::{ParseError, ParseErrorType};
|
||||
use ruff_source_file::{OneIndexed, SourceCode, SourceLocation};
|
||||
|
||||
use crate::fs;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::source_kind::SourceKind;
|
||||
use ruff_notebook::Notebook;
|
||||
|
||||
pub static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ use std::num::NonZeroUsize;
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::jupyter::{Notebook, NotebookIndex};
|
||||
use crate::message::diff::calculate_print_width;
|
||||
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
|
||||
use crate::message::{
|
||||
|
||||
@@ -14,11 +14,12 @@ pub use json_lines::JsonLinesEmitter;
|
||||
pub use junit::JunitEmitter;
|
||||
pub use pylint::PylintEmitter;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_source_file::{SourceFile, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
pub use text::TextEmitter;
|
||||
|
||||
use crate::jupyter::Notebook;
|
||||
|
||||
mod azure;
|
||||
mod diff;
|
||||
mod github;
|
||||
|
||||
@@ -7,11 +7,11 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, Sou
|
||||
use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::{OneIndexed, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::jupyter::{Notebook, NotebookIndex};
|
||||
use crate::line_width::{LineWidthBuilder, TabSize};
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext, Message};
|
||||
|
||||
@@ -14,8 +14,8 @@ use crate::rule_redirects::get_redirect;
|
||||
pub enum RuleSelector {
|
||||
/// Select all stable rules.
|
||||
All,
|
||||
/// Select all nursery rules.
|
||||
Nursery,
|
||||
/// Category to select all preview rules, previously known as the nursery
|
||||
Preview,
|
||||
/// Legacy category to select both the `mccabe` and `flake8-comprehensions` linters
|
||||
/// via a single selector.
|
||||
C,
|
||||
@@ -43,7 +43,9 @@ impl FromStr for RuleSelector {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"ALL" => Ok(Self::All),
|
||||
"NURSERY" => Ok(Self::Nursery),
|
||||
// Legacy support for selecting preview rules as "nursery"
|
||||
"NURSERY" => Ok(Self::Preview),
|
||||
"PREVIEW" => Ok(Self::Preview),
|
||||
"C" => Ok(Self::C),
|
||||
"T" => Ok(Self::T),
|
||||
_ => {
|
||||
@@ -81,7 +83,7 @@ impl RuleSelector {
|
||||
pub fn prefix_and_code(&self) -> (&'static str, &'static str) {
|
||||
match self {
|
||||
RuleSelector::All => ("", "ALL"),
|
||||
RuleSelector::Nursery => ("", "NURSERY"),
|
||||
RuleSelector::Preview => ("", "PREVIEW"),
|
||||
RuleSelector::C => ("", "C"),
|
||||
RuleSelector::T => ("", "T"),
|
||||
RuleSelector::Prefix { prefix, .. } => {
|
||||
@@ -150,11 +152,9 @@ impl IntoIterator for &RuleSelector {
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
match self {
|
||||
RuleSelector::All => {
|
||||
RuleSelectorIter::All(Rule::iter().filter(|rule| !rule.is_nursery()))
|
||||
}
|
||||
RuleSelector::Nursery => {
|
||||
RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_nursery))
|
||||
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
|
||||
RuleSelector::Preview => {
|
||||
RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_preview))
|
||||
}
|
||||
RuleSelector::C => RuleSelectorIter::Chain(
|
||||
Linter::Flake8Comprehensions
|
||||
@@ -173,7 +173,7 @@ impl IntoIterator for &RuleSelector {
|
||||
}
|
||||
|
||||
pub enum RuleSelectorIter {
|
||||
All(std::iter::Filter<RuleIter, fn(&Rule) -> bool>),
|
||||
All(RuleIter),
|
||||
Nursery(std::iter::Filter<RuleIter, fn(&Rule) -> bool>),
|
||||
Chain(std::iter::Chain<std::vec::IntoIter<Rule>, std::vec::IntoIter<Rule>>),
|
||||
Vec(std::vec::IntoIter<Rule>),
|
||||
@@ -266,7 +266,7 @@ impl RuleSelector {
|
||||
pub fn specificity(&self) -> Specificity {
|
||||
match self {
|
||||
RuleSelector::All => Specificity::All,
|
||||
RuleSelector::Nursery => Specificity::All,
|
||||
RuleSelector::Preview => Specificity::All,
|
||||
RuleSelector::T => Specificity::LinterGroup,
|
||||
RuleSelector::C => Specificity::LinterGroup,
|
||||
RuleSelector::Linter(..) => Specificity::Linter,
|
||||
@@ -285,7 +285,7 @@ impl RuleSelector {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(EnumIter, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
|
||||
#[derive(EnumIter, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
|
||||
pub enum Specificity {
|
||||
All,
|
||||
LinterGroup,
|
||||
|
||||
@@ -149,17 +149,6 @@ import os
|
||||
# Content Content Content Content Content Content Content Content Content Content
|
||||
|
||||
# Copyright 2023
|
||||
"#
|
||||
.trim(),
|
||||
&settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]),
|
||||
);
|
||||
assert_messages!(diagnostics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn char_boundary() {
|
||||
let diagnostics = test_snippet(
|
||||
r#"কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
|
||||
"#
|
||||
.trim(),
|
||||
&settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
use crate::settings::Settings;
|
||||
|
||||
@@ -32,7 +33,11 @@ pub(crate) fn missing_copyright_notice(
|
||||
}
|
||||
|
||||
// Only search the first 1024 bytes in the file.
|
||||
let contents = locator.up_to(locator.floor_char_boundary(TextSize::new(1024)));
|
||||
let contents = if locator.len() < 1024 {
|
||||
locator.contents()
|
||||
} else {
|
||||
locator.up_to(TextSize::from(1024))
|
||||
};
|
||||
|
||||
// Locate the copyright notice.
|
||||
if let Some(match_) = settings.flake8_copyright.notice_rgx.find(contents) {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_copyright/mod.rs
|
||||
---
|
||||
<filename>:1:1: CPY001 Missing copyright notice at top of file
|
||||
|
|
||||
1 | কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
|
||||
| CPY001
|
||||
|
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -23,35 +23,18 @@ impl AlwaysAutofixableViolation for NonEmptyStubBody {
|
||||
|
||||
/// PYI010
|
||||
pub(crate) fn non_empty_stub_body(checker: &mut Checker, body: &[Stmt]) {
|
||||
// Ignore multi-statement bodies (covered by PYI048).
|
||||
let [stmt] = body else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Ignore `pass` statements (covered by PYI009).
|
||||
if stmt.is_pass_stmt() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore docstrings (covered by PYI021).
|
||||
if is_docstring_stmt(stmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore `...` (the desired case).
|
||||
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
|
||||
if let [Stmt::Expr(ast::StmtExpr { value, range: _ })] = body {
|
||||
if let Expr::Constant(ast::ExprConstant { value, .. }) = value.as_ref() {
|
||||
if value.is_ellipsis() {
|
||||
if matches!(value, Constant::Ellipsis | Constant::Str(_)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(NonEmptyStubBody, stmt.range());
|
||||
let mut diagnostic = Diagnostic::new(NonEmptyStubBody, body[0].range());
|
||||
if checker.patch(Rule::NonEmptyStubBody) {
|
||||
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
format!("..."),
|
||||
stmt.range(),
|
||||
body[0].range(),
|
||||
)));
|
||||
};
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -22,16 +22,17 @@ impl AlwaysAutofixableViolation for PassStatementStubBody {
|
||||
|
||||
/// PYI009
|
||||
pub(crate) fn pass_statement_stub_body(checker: &mut Checker, body: &[Stmt]) {
|
||||
let [Stmt::Pass(pass)] = body else {
|
||||
let [stmt] = body else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(PassStatementStubBody, pass.range());
|
||||
if checker.patch(Rule::PassStatementStubBody) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
format!("..."),
|
||||
pass.range(),
|
||||
)));
|
||||
};
|
||||
checker.diagnostics.push(diagnostic);
|
||||
if stmt.is_pass_stmt() {
|
||||
let mut diagnostic = Diagnostic::new(PassStatementStubBody, stmt.range());
|
||||
if checker.patch(Rule::PassStatementStubBody) {
|
||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||
format!("..."),
|
||||
stmt.range(),
|
||||
)));
|
||||
};
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use ruff_python_ast::Stmt;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::is_docstring_stmt;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::Stmt;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -15,12 +17,21 @@ impl Violation for StubBodyMultipleStatements {
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI048
|
||||
/// PYI010
|
||||
pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt, body: &[Stmt]) {
|
||||
if body.len() > 1 {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
StubBodyMultipleStatements,
|
||||
stmt.identifier(),
|
||||
));
|
||||
// If the function body consists of exactly one statement, abort.
|
||||
if body.len() == 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the function body consists of exactly two statements, and the first is a
|
||||
// docstring, abort (this is covered by PYI021).
|
||||
if body.len() == 2 && is_docstring_stmt(&body[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
StubBodyMultipleStatements,
|
||||
stmt.identifier(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ PYI010.pyi:6:5: PYI010 [*] Function body must contain only `...`
|
||||
|
|
||||
= help: Replace function body with `...`
|
||||
|
||||
ℹ Suggested fix
|
||||
3 3 | """foo""" # OK, docstrings are handled by another rule
|
||||
ℹ Fix
|
||||
3 3 | """foo""" # OK, strings are handled by another rule
|
||||
4 4 |
|
||||
5 5 | def buzz():
|
||||
6 |- print("buzz") # ERROR PYI010
|
||||
@@ -31,7 +31,7 @@ PYI010.pyi:9:5: PYI010 [*] Function body must contain only `...`
|
||||
|
|
||||
= help: Replace function body with `...`
|
||||
|
||||
ℹ Suggested fix
|
||||
ℹ Fix
|
||||
6 6 | print("buzz") # ERROR PYI010
|
||||
7 7 |
|
||||
8 8 | def foo2():
|
||||
@@ -46,19 +46,14 @@ PYI010.pyi:12:5: PYI010 [*] Function body must contain only `...`
|
||||
11 | def bizz():
|
||||
12 | x = 123 # ERROR PYI010
|
||||
| ^^^^^^^ PYI010
|
||||
13 |
|
||||
14 | def foo3():
|
||||
|
|
||||
= help: Replace function body with `...`
|
||||
|
||||
ℹ Suggested fix
|
||||
ℹ Fix
|
||||
9 9 | 123 # ERROR PYI010
|
||||
10 10 |
|
||||
11 11 | def bizz():
|
||||
12 |- x = 123 # ERROR PYI010
|
||||
12 |+ ... # ERROR PYI010
|
||||
13 13 |
|
||||
14 14 | def foo3():
|
||||
15 15 | pass # OK, pass is handled by another rule
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI048.pyi:8:5: PYI048 Function body must contain exactly one statement
|
||||
PYI048.pyi:11:5: PYI048 Function body must contain exactly one statement
|
||||
|
|
||||
6 | """oof""" # OK
|
||||
7 |
|
||||
8 | def oof(): # ERROR PYI048
|
||||
11 | def foo(): # ERROR PYI048
|
||||
| ^^^ PYI048
|
||||
9 | """oof"""
|
||||
10 | print("foo")
|
||||
|
|
||||
|
||||
PYI048.pyi:12:5: PYI048 Function body must contain exactly one statement
|
||||
|
|
||||
10 | print("foo")
|
||||
11 |
|
||||
12 | def foo(): # ERROR PYI048
|
||||
| ^^^ PYI048
|
||||
13 | """foo"""
|
||||
14 | print("foo")
|
||||
12 | """foo"""
|
||||
13 | print("foo")
|
||||
|
|
||||
|
||||
PYI048.pyi:17:5: PYI048 Function body must contain exactly one statement
|
||||
|
|
||||
15 | print("foo")
|
||||
16 |
|
||||
17 | def buzz(): # ERROR PYI048
|
||||
17 | def buzz(): # ERROR PYI048
|
||||
| ^^^^ PYI048
|
||||
18 | print("fizz")
|
||||
19 | print("buzz")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, ExceptHandler, MatchCase, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use std::iter::Peekable;
|
||||
use std::slice;
|
||||
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_ast::{self as ast, ElifElseClause, ExceptHandler, MatchCase, Stmt};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::directives::IsortDirectives;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::rules::isort::helpers;
|
||||
use crate::source_kind::SourceKind;
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ pub(crate) use ambiguous_variable_name::*;
|
||||
pub(crate) use bare_except::*;
|
||||
pub(crate) use compound_statements::*;
|
||||
pub(crate) use doc_line_too_long::*;
|
||||
pub use errors::IOError;
|
||||
pub(crate) use errors::*;
|
||||
pub use errors::{IOError, SyntaxError};
|
||||
pub(crate) use imports::*;
|
||||
|
||||
pub(crate) use invalid_escape_sequence::*;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ruff_diagnostics::SourceMap;
|
||||
use ruff_notebook::Notebook;
|
||||
use crate::autofix::source_map::SourceMap;
|
||||
use crate::jupyter::Notebook;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
|
||||
pub enum SourceKind {
|
||||
|
||||
@@ -21,6 +21,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::autofix::{fix_file, FixResult};
|
||||
use crate::directives;
|
||||
use crate::jupyter::Notebook;
|
||||
use crate::linter::{check_path, LinterResult};
|
||||
use crate::message::{Emitter, EmitterContext, Message, TextEmitter};
|
||||
use crate::packaging::detect_package_root;
|
||||
@@ -28,7 +29,18 @@ use crate::registry::AsRule;
|
||||
use crate::rules::pycodestyle::rules::syntax_error;
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::source_kind::SourceKind;
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
|
||||
#[cfg(not(fuzzing))]
|
||||
pub(crate) fn read_jupyter_notebook(path: &Path) -> Result<Notebook> {
|
||||
let path = test_resource_path("fixtures/jupyter").join(path);
|
||||
Notebook::from_path(&path).map_err(|err| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to read notebook file `{}`: {:?}",
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(fuzzing))]
|
||||
pub(crate) fn test_resource_path(path: impl AsRef<Path>) -> std::path::PathBuf {
|
||||
@@ -55,12 +67,12 @@ pub(crate) fn test_notebook_path(
|
||||
path: impl AsRef<Path>,
|
||||
expected: impl AsRef<Path>,
|
||||
settings: &Settings,
|
||||
) -> Result<TestedNotebook, NotebookError> {
|
||||
let source_notebook = Notebook::from_path(path.as_ref())?;
|
||||
) -> Result<TestedNotebook> {
|
||||
let source_notebook = read_jupyter_notebook(path.as_ref())?;
|
||||
|
||||
let source_kind = SourceKind::IpyNotebook(source_notebook);
|
||||
let (messages, transformed) = test_contents(&source_kind, path.as_ref(), settings);
|
||||
let expected_notebook = Notebook::from_path(expected.as_ref())?;
|
||||
let expected_notebook = read_jupyter_notebook(expected.as_ref())?;
|
||||
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
|
||||
|
||||
assert_eq!(
|
||||
@@ -261,8 +273,12 @@ Source with applied fixes:
|
||||
(messages, transformed)
|
||||
}
|
||||
|
||||
fn print_diagnostics(diagnostics: Vec<Diagnostic>, path: &Path, source: &SourceKind) -> String {
|
||||
let filename = path.file_name().unwrap().to_string_lossy();
|
||||
fn print_diagnostics(
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
file_path: &Path,
|
||||
source: &SourceKind,
|
||||
) -> String {
|
||||
let filename = file_path.file_name().unwrap().to_string_lossy();
|
||||
let source_file = SourceFileBuilder::new(filename.as_ref(), source.source_code()).finish();
|
||||
|
||||
let messages: Vec<_> = diagnostics
|
||||
@@ -275,7 +291,7 @@ fn print_diagnostics(diagnostics: Vec<Diagnostic>, path: &Path, source: &SourceK
|
||||
.collect();
|
||||
|
||||
if let Some(notebook) = source.notebook() {
|
||||
print_jupyter_messages(&messages, path, notebook)
|
||||
print_jupyter_messages(&messages, &filename, notebook)
|
||||
} else {
|
||||
print_messages(&messages)
|
||||
}
|
||||
@@ -283,7 +299,7 @@ fn print_diagnostics(diagnostics: Vec<Diagnostic>, path: &Path, source: &SourceK
|
||||
|
||||
pub(crate) fn print_jupyter_messages(
|
||||
messages: &[Message],
|
||||
path: &Path,
|
||||
filename: &str,
|
||||
notebook: &Notebook,
|
||||
) -> String {
|
||||
let mut output = Vec::new();
|
||||
@@ -296,7 +312,7 @@ pub(crate) fn print_jupyter_messages(
|
||||
&mut output,
|
||||
messages,
|
||||
&EmitterContext::new(&FxHashMap::from_iter([(
|
||||
path.file_name().unwrap().to_string_lossy().to_string(),
|
||||
filename.to_string(),
|
||||
notebook.clone(),
|
||||
)])),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.287"
|
||||
version = "0.0.286"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -25,7 +25,6 @@ ruff = { path = "../ruff", features = ["clap"] }
|
||||
ruff_cache = { path = "../ruff_cache" }
|
||||
ruff_diagnostics = { path = "../ruff_diagnostics" }
|
||||
ruff_formatter = { path = "../ruff_formatter" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
ruff_macros = { path = "../ruff_macros" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_formatter = { path = "../ruff_python_formatter" }
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::ExitStatus;
|
||||
use ruff_workspace::options::Options;
|
||||
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub(crate) fn config(key: Option<&str>) -> Result<()> {
|
||||
pub(crate) fn config(key: Option<&str>) -> ExitStatus {
|
||||
match key {
|
||||
None => print!("{}", Options::metadata()),
|
||||
Some(key) => match Options::metadata().get(key) {
|
||||
None => {
|
||||
return Err(anyhow!("Unknown option: {key}"));
|
||||
println!("Unknown option");
|
||||
return ExitStatus::Error;
|
||||
}
|
||||
Some(entry) => {
|
||||
print!("{entry}");
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
ExitStatus::Success
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ struct Explanation<'a> {
|
||||
message_formats: &'a [&'a str],
|
||||
autofix: String,
|
||||
explanation: Option<&'a str>,
|
||||
nursery: bool,
|
||||
preview: bool,
|
||||
}
|
||||
|
||||
impl<'a> Explanation<'a> {
|
||||
@@ -35,7 +35,7 @@ impl<'a> Explanation<'a> {
|
||||
message_formats: rule.message_formats(),
|
||||
autofix,
|
||||
explanation: rule.explanation(),
|
||||
nursery: rule.is_nursery(),
|
||||
preview: rule.is_preview(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,10 @@ fn format_rule_text(rule: Rule) -> String {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
if rule.is_nursery() {
|
||||
if rule.is_preview() {
|
||||
output.push_str(&format!(
|
||||
r#"This rule is part of the **nursery**, a collection of newer lints that are
|
||||
still under development. As such, it must be enabled by explicitly selecting
|
||||
{}."#,
|
||||
r#"This rule is in preview and is not stable. It may be enabled by explicitly selecting {}"
|
||||
" or providing the `--preview` flag."#,
|
||||
rule.noqa_code()
|
||||
));
|
||||
output.push('\n');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#![cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
|
||||
use std::fs::{write, File};
|
||||
use std::fs::write;
|
||||
use std::io;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::io::Write;
|
||||
use std::ops::AddAssign;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -14,19 +14,18 @@ use filetime::FileTime;
|
||||
use log::{debug, error, warn};
|
||||
use rustc_hash::FxHashMap;
|
||||
use similar::TextDiff;
|
||||
use thiserror::Error;
|
||||
|
||||
use ruff::jupyter::{Cell, Notebook};
|
||||
use ruff::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult};
|
||||
use ruff::logging::DisplayParseError;
|
||||
use ruff::message::Message;
|
||||
use ruff::pyproject_toml::lint_pyproject_toml;
|
||||
use ruff::registry::AsRule;
|
||||
use ruff::registry::Rule;
|
||||
use ruff::settings::{flags, AllSettings, Settings};
|
||||
use ruff::source_kind::SourceKind;
|
||||
use ruff::{fs, IOError, SyntaxError};
|
||||
use ruff::{fs, IOError};
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_notebook::{Cell, Notebook, NotebookError};
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
|
||||
use ruff_source_file::{LineIndex, SourceCode, SourceFileBuilder};
|
||||
@@ -77,39 +76,27 @@ impl Diagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate [`Diagnostics`] based on a [`SourceExtractionError`].
|
||||
pub(crate) fn from_source_error(
|
||||
err: &SourceExtractionError,
|
||||
path: Option<&Path>,
|
||||
settings: &Settings,
|
||||
) -> Self {
|
||||
let diagnostic = Diagnostic::from(err);
|
||||
if settings.rules.enabled(diagnostic.kind.rule()) {
|
||||
let name = path.map_or_else(|| "-".into(), std::path::Path::to_string_lossy);
|
||||
let dummy = SourceFileBuilder::new(name, "").finish();
|
||||
/// Generate [`Diagnostics`] based on an [`io::Error`].
|
||||
pub(crate) fn from_io_error(err: &io::Error, path: &Path, settings: &Settings) -> Self {
|
||||
if settings.rules.enabled(Rule::IOError) {
|
||||
let io_err = Diagnostic::new(
|
||||
IOError {
|
||||
message: err.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
);
|
||||
let dummy = SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();
|
||||
Self::new(
|
||||
vec![Message::from_diagnostic(
|
||||
diagnostic,
|
||||
dummy,
|
||||
TextSize::default(),
|
||||
)],
|
||||
vec![Message::from_diagnostic(io_err, dummy, TextSize::default())],
|
||||
ImportMap::default(),
|
||||
)
|
||||
} else {
|
||||
match path {
|
||||
Some(path) => {
|
||||
warn!(
|
||||
"{}{}{} {err}",
|
||||
"Failed to lint ".bold(),
|
||||
fs::relativize_path(path).bold(),
|
||||
":".bold()
|
||||
);
|
||||
}
|
||||
None => {
|
||||
warn!("{}{} {err}", "Failed to lint".bold(), ":".bold());
|
||||
}
|
||||
}
|
||||
|
||||
warn!(
|
||||
"{}{}{} {err}",
|
||||
"Failed to lint ".bold(),
|
||||
fs::relativize_path(path).bold(),
|
||||
":".bold()
|
||||
);
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
@@ -134,6 +121,76 @@ impl AddAssign for Diagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a Jupyter Notebook from disk.
|
||||
///
|
||||
/// Returns either an indexed Python Jupyter Notebook or a diagnostic (which is empty if we skip).
|
||||
fn notebook_from_path(path: &Path) -> Result<Notebook, Box<Diagnostics>> {
|
||||
let notebook = match Notebook::from_path(path) {
|
||||
Ok(notebook) => {
|
||||
if !notebook.is_python_notebook() {
|
||||
// Not a python notebook, this could e.g. be an R notebook which we want to just skip.
|
||||
debug!(
|
||||
"Skipping {} because it's not a Python notebook",
|
||||
path.display()
|
||||
);
|
||||
return Err(Box::default());
|
||||
}
|
||||
notebook
|
||||
}
|
||||
Err(diagnostic) => {
|
||||
// Failed to read the jupyter notebook
|
||||
return Err(Box::new(Diagnostics {
|
||||
messages: vec![Message::from_diagnostic(
|
||||
*diagnostic,
|
||||
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(),
|
||||
TextSize::default(),
|
||||
)],
|
||||
..Diagnostics::default()
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(notebook)
|
||||
}
|
||||
|
||||
/// Parse a Jupyter Notebook from a JSON string.
|
||||
///
|
||||
/// Returns either an indexed Python Jupyter Notebook or a diagnostic (which is empty if we skip).
|
||||
fn notebook_from_source_code(
|
||||
source_code: &str,
|
||||
path: Option<&Path>,
|
||||
) -> Result<Notebook, Box<Diagnostics>> {
|
||||
let notebook = match Notebook::from_source_code(source_code) {
|
||||
Ok(notebook) => {
|
||||
if !notebook.is_python_notebook() {
|
||||
// Not a python notebook, this could e.g. be an R notebook which we want to just skip.
|
||||
if let Some(path) = path {
|
||||
debug!(
|
||||
"Skipping {} because it's not a Python notebook",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
return Err(Box::default());
|
||||
}
|
||||
notebook
|
||||
}
|
||||
Err(diagnostic) => {
|
||||
// Failed to read the jupyter notebook
|
||||
return Err(Box::new(Diagnostics {
|
||||
messages: vec![Message::from_diagnostic(
|
||||
*diagnostic,
|
||||
SourceFileBuilder::new(path.map(Path::to_string_lossy).unwrap_or_default(), "")
|
||||
.finish(),
|
||||
TextSize::default(),
|
||||
)],
|
||||
..Diagnostics::default()
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(notebook)
|
||||
}
|
||||
|
||||
/// Lint the source code at the given `Path`.
|
||||
pub(crate) fn lint_path(
|
||||
path: &Path,
|
||||
@@ -178,17 +235,12 @@ pub(crate) fn lint_path(
|
||||
.iter_enabled()
|
||||
.any(|rule_code| rule_code.lint_source().is_pyproject_toml())
|
||||
{
|
||||
let contents =
|
||||
match std::fs::read_to_string(path).map_err(SourceExtractionError::Io) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) => {
|
||||
return Ok(Diagnostics::from_source_error(
|
||||
&err,
|
||||
Some(path),
|
||||
&settings.lib,
|
||||
));
|
||||
}
|
||||
};
|
||||
let contents = match std::fs::read_to_string(path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) => {
|
||||
return Ok(Diagnostics::from_io_error(&err, path, &settings.lib));
|
||||
}
|
||||
};
|
||||
let source_file = SourceFileBuilder::new(path.to_string_lossy(), contents).finish();
|
||||
lint_pyproject_toml(source_file, &settings.lib)
|
||||
} else {
|
||||
@@ -205,14 +257,12 @@ pub(crate) fn lint_path(
|
||||
|
||||
// Extract the sources from the file.
|
||||
let LintSource(source_kind) = match LintSource::try_from_path(path, source_type) {
|
||||
Ok(Some(sources)) => sources,
|
||||
Ok(None) => return Ok(Diagnostics::default()),
|
||||
Err(err) => {
|
||||
return Ok(Diagnostics::from_source_error(
|
||||
&err,
|
||||
Some(path),
|
||||
&settings.lib,
|
||||
));
|
||||
Ok(sources) => sources,
|
||||
Err(SourceExtractionError::Io(err)) => {
|
||||
return Ok(Diagnostics::from_io_error(&err, path, &settings.lib));
|
||||
}
|
||||
Err(SourceExtractionError::Diagnostics(diagnostics)) => {
|
||||
return Ok(*diagnostics);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -243,8 +293,7 @@ pub(crate) fn lint_path(
|
||||
write(path, transformed.as_bytes())?;
|
||||
}
|
||||
SourceKind::IpyNotebook(notebook) => {
|
||||
let mut writer = BufWriter::new(File::create(path)?);
|
||||
notebook.write(&mut writer)?;
|
||||
notebook.write(path)?;
|
||||
}
|
||||
},
|
||||
flags::FixMode::Diff => {
|
||||
@@ -394,13 +443,17 @@ pub(crate) fn lint_stdin(
|
||||
};
|
||||
|
||||
// Extract the sources from the file.
|
||||
let LintSource(source_kind) = match LintSource::try_from_source_code(contents, source_type) {
|
||||
Ok(Some(sources)) => sources,
|
||||
Ok(None) => return Ok(Diagnostics::default()),
|
||||
Err(err) => {
|
||||
return Ok(Diagnostics::from_source_error(&err, path, settings));
|
||||
}
|
||||
};
|
||||
let LintSource(source_kind) =
|
||||
match LintSource::try_from_source_code(contents, path, source_type) {
|
||||
Ok(sources) => sources,
|
||||
Err(SourceExtractionError::Io(err)) => {
|
||||
// SAFETY: An `io::Error` can only occur if we're reading from a path.
|
||||
return Ok(Diagnostics::from_io_error(&err, path.unwrap(), settings));
|
||||
}
|
||||
Err(SourceExtractionError::Diagnostics(diagnostics)) => {
|
||||
return Ok(*diagnostics);
|
||||
}
|
||||
};
|
||||
|
||||
// Lint the inputs.
|
||||
let (
|
||||
@@ -510,16 +563,15 @@ impl LintSource {
|
||||
fn try_from_path(
|
||||
path: &Path,
|
||||
source_type: PySourceType,
|
||||
) -> Result<Option<LintSource>, SourceExtractionError> {
|
||||
) -> Result<LintSource, SourceExtractionError> {
|
||||
if source_type.is_ipynb() {
|
||||
let notebook = Notebook::from_path(path)?;
|
||||
Ok(notebook
|
||||
.is_python_notebook()
|
||||
.then_some(LintSource(SourceKind::IpyNotebook(notebook))))
|
||||
let notebook = notebook_from_path(path).map_err(SourceExtractionError::Diagnostics)?;
|
||||
let source_kind = SourceKind::IpyNotebook(notebook);
|
||||
Ok(LintSource(source_kind))
|
||||
} else {
|
||||
// This is tested by ruff_cli integration test `unreadable_file`
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(Some(LintSource(SourceKind::Python(contents))))
|
||||
let contents = std::fs::read_to_string(path).map_err(SourceExtractionError::Io)?;
|
||||
Ok(LintSource(SourceKind::Python(contents)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,53 +580,48 @@ impl LintSource {
|
||||
/// the file path should be used for diagnostics, but not for reading the file from disk.
|
||||
fn try_from_source_code(
|
||||
source_code: String,
|
||||
path: Option<&Path>,
|
||||
source_type: PySourceType,
|
||||
) -> Result<Option<LintSource>, SourceExtractionError> {
|
||||
) -> Result<LintSource, SourceExtractionError> {
|
||||
if source_type.is_ipynb() {
|
||||
let notebook = Notebook::from_source_code(&source_code)?;
|
||||
Ok(notebook
|
||||
.is_python_notebook()
|
||||
.then_some(LintSource(SourceKind::IpyNotebook(notebook))))
|
||||
let notebook = notebook_from_source_code(&source_code, path)
|
||||
.map_err(SourceExtractionError::Diagnostics)?;
|
||||
let source_kind = SourceKind::IpyNotebook(notebook);
|
||||
Ok(LintSource(source_kind))
|
||||
} else {
|
||||
Ok(Some(LintSource(SourceKind::Python(source_code))))
|
||||
Ok(LintSource(SourceKind::Python(source_code)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum SourceExtractionError {
|
||||
#[derive(Debug)]
|
||||
enum SourceExtractionError {
|
||||
/// The extraction failed due to an [`io::Error`].
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
/// The extraction failed due to a [`NotebookError`].
|
||||
#[error(transparent)]
|
||||
Notebook(#[from] NotebookError),
|
||||
Io(io::Error),
|
||||
/// The extraction failed, and generated [`Diagnostics`] to report.
|
||||
Diagnostics(Box<Diagnostics>),
|
||||
}
|
||||
|
||||
impl From<&SourceExtractionError> for Diagnostic {
|
||||
fn from(err: &SourceExtractionError) -> Self {
|
||||
match err {
|
||||
// IO errors.
|
||||
SourceExtractionError::Io(_)
|
||||
| SourceExtractionError::Notebook(NotebookError::Io(_) | NotebookError::Json(_)) => {
|
||||
Diagnostic::new(
|
||||
IOError {
|
||||
message: err.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
)
|
||||
}
|
||||
// Syntax errors.
|
||||
SourceExtractionError::Notebook(
|
||||
NotebookError::InvalidJson(_)
|
||||
| NotebookError::InvalidSchema(_)
|
||||
| NotebookError::InvalidFormat(_),
|
||||
) => Diagnostic::new(
|
||||
SyntaxError {
|
||||
message: err.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
),
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::diagnostics::{notebook_from_path, notebook_from_source_code, Diagnostics};
|
||||
|
||||
#[test]
|
||||
fn test_r() {
|
||||
let path = Path::new("../ruff/resources/test/fixtures/jupyter/R.ipynb");
|
||||
// No diagnostics is used as skip signal.
|
||||
assert_eq!(
|
||||
notebook_from_path(path).unwrap_err(),
|
||||
Box::<Diagnostics>::default()
|
||||
);
|
||||
|
||||
let contents = std::fs::read_to_string(path).unwrap();
|
||||
// No diagnostics is used as skip signal.
|
||||
assert_eq!(
|
||||
notebook_from_source_code(&contents, Some(path)).unwrap_err(),
|
||||
Box::<Diagnostics>::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,27 +139,18 @@ quoting the executed command, along with the relevant file contents and `pyproje
|
||||
if let Some(rule) = rule {
|
||||
commands::rule::rule(rule, format)?;
|
||||
}
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Config { option } => {
|
||||
commands::config::config(option.as_deref())?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Linter { format } => {
|
||||
commands::linter::linter(format)?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Clean => {
|
||||
commands::clean::clean(log_level)?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Config { option } => return Ok(commands::config::config(option.as_deref())),
|
||||
Command::Linter { format } => commands::linter::linter(format)?,
|
||||
Command::Clean => commands::clean::clean(log_level)?,
|
||||
Command::GenerateShellCompletion { shell } => {
|
||||
shell.generate(&mut Args::command(), &mut stdout());
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Command::Check(args) => check(args, log_level),
|
||||
Command::Format(args) => format(args, log_level),
|
||||
Command::Check(args) => return check(args, log_level),
|
||||
Command::Format(args) => return format(args, log_level),
|
||||
}
|
||||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
fn format(args: FormatCommand, log_level: LogLevel) -> Result<ExitStatus> {
|
||||
|
||||
@@ -18,7 +18,6 @@ ruff_formatter = { path = "../ruff_formatter" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_codegen = { path = "../ruff_python_codegen" }
|
||||
ruff_python_formatter = { path = "../ruff_python_formatter" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
ruff_python_literal = { path = "../ruff_python_literal" }
|
||||
ruff_python_parser = { path = "../ruff_python_parser" }
|
||||
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
|
||||
|
||||
@@ -43,11 +43,10 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
if rule.is_nursery() {
|
||||
if rule.is_preview() {
|
||||
output.push_str(&format!(
|
||||
r#"This rule is part of the **nursery**, a collection of newer lints that are
|
||||
still under development. As such, it must be enabled by explicitly selecting
|
||||
{}."#,
|
||||
r#"This rule is in preview and is not stable. It may be enabled by explicitly selecting {}"
|
||||
" or providing the `--preview` flag."#,
|
||||
rule.noqa_code()
|
||||
));
|
||||
output.push('\n');
|
||||
|
||||
@@ -11,7 +11,7 @@ use ruff_diagnostics::AutofixKind;
|
||||
use ruff_workspace::options::Options;
|
||||
|
||||
const FIX_SYMBOL: &str = "🛠";
|
||||
const NURSERY_SYMBOL: &str = "🌅";
|
||||
const PREVIEW_SYMBOL: &str = "🌅";
|
||||
|
||||
fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>, linter: &Linter) {
|
||||
table_out.push_str("| Code | Name | Message | |");
|
||||
@@ -25,12 +25,12 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
}
|
||||
AutofixKind::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
|
||||
};
|
||||
let nursery_token = if rule.is_nursery() {
|
||||
format!("<span style='opacity: 1'>{NURSERY_SYMBOL}</span>")
|
||||
let preview_token = if rule.is_preview() {
|
||||
format!("<span style='opacity: 1'>{PREVIEW_SYMBOL}</span>")
|
||||
} else {
|
||||
format!("<span style='opacity: 0.1'>{NURSERY_SYMBOL}</span>")
|
||||
format!("<span style='opacity: 0.1'>{PREVIEW_SYMBOL}</span>")
|
||||
};
|
||||
let status_token = format!("{fix_token} {nursery_token}");
|
||||
let status_token = format!("{fix_token} {preview_token}");
|
||||
|
||||
let rule_name = rule.as_ref();
|
||||
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn generate() -> String {
|
||||
table_out.push('\n');
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"The {NURSERY_SYMBOL} emoji indicates that a rule is part of the [\"nursery\"](../faq/#what-is-the-nursery)."
|
||||
"The {PREVIEW_SYMBOL} emoji indicates that a rule is part of the [\"nursery\"](../faq/#what-is-the-nursery)."
|
||||
));
|
||||
table_out.push('\n');
|
||||
table_out.push('\n');
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff::jupyter;
|
||||
use ruff_python_codegen::round_trip;
|
||||
use ruff_python_stdlib::path::is_jupyter_notebook;
|
||||
|
||||
@@ -19,7 +20,7 @@ pub(crate) struct Args {
|
||||
pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
let path = args.file.as_path();
|
||||
if is_jupyter_notebook(path) {
|
||||
println!("{}", ruff_notebook::round_trip(path)?);
|
||||
println!("{}", jupyter::round_trip(path)?);
|
||||
} else {
|
||||
let contents = fs::read_to_string(&args.file)?;
|
||||
println!("{}", round_trip(&contents, &args.file.to_string_lossy())?);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
pub use diagnostic::{Diagnostic, DiagnosticKind};
|
||||
pub use edit::Edit;
|
||||
pub use fix::{Applicability, Fix, IsolationLevel};
|
||||
pub use source_map::{SourceMap, SourceMarker};
|
||||
pub use violation::{AlwaysAutofixableViolation, AutofixKind, Violation};
|
||||
|
||||
mod diagnostic;
|
||||
mod edit;
|
||||
mod fix;
|
||||
mod source_map;
|
||||
mod violation;
|
||||
|
||||
@@ -8,7 +8,7 @@ use syn::{
|
||||
Ident, ItemFn, LitStr, Pat, Path, Stmt, Token,
|
||||
};
|
||||
|
||||
use crate::rule_code_prefix::{get_prefix_ident, if_all_same, is_nursery};
|
||||
use crate::rule_code_prefix::{get_prefix_ident, if_all_same, is_preview};
|
||||
|
||||
/// A rule entry in the big match statement such a
|
||||
/// `(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),`
|
||||
@@ -199,7 +199,7 @@ fn rules_by_prefix(
|
||||
// Nursery rules have to be explicitly selected, so we ignore them when looking at
|
||||
// prefix-level selectors (e.g., `--select SIM10`), but add the rule itself under
|
||||
// its fully-qualified code (e.g., `--select SIM101`).
|
||||
if is_nursery(&rule.group) {
|
||||
if is_preview(&rule.group) {
|
||||
rules_by_prefix.insert(code.clone(), vec![(rule.path.clone(), rule.attrs.clone())]);
|
||||
continue;
|
||||
}
|
||||
@@ -211,7 +211,7 @@ fn rules_by_prefix(
|
||||
.filter_map(|(code, rule)| {
|
||||
// Nursery rules have to be explicitly selected, so we ignore them when
|
||||
// looking at prefixes.
|
||||
if is_nursery(&rule.group) {
|
||||
if is_preview(&rule.group) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -311,8 +311,8 @@ See also https://github.com/astral-sh/ruff/issues/2186.
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_nursery(&self) -> bool {
|
||||
matches!(self.group(), RuleGroup::Nursery)
|
||||
pub fn is_preview(&self) -> bool {
|
||||
matches!(self.group(), RuleGroup::Preview)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ fn generate_iter_impl(
|
||||
let mut linter_rules_match_arms = quote!();
|
||||
let mut linter_all_rules_match_arms = quote!();
|
||||
for (linter, map) in linter_to_rules {
|
||||
let rule_paths = map.values().filter(|rule| !is_nursery(&rule.group)).map(
|
||||
let rule_paths = map.values().filter(|rule| !is_preview(&rule.group)).map(
|
||||
|Rule { attrs, path, .. }| {
|
||||
let rule_name = path.segments.last().unwrap();
|
||||
quote!(#(#attrs)* Rule::#rule_name)
|
||||
|
||||
@@ -15,7 +15,7 @@ pub(crate) fn expand<'a>(
|
||||
for (variant, group, attr) in variants {
|
||||
let code_str = variant.to_string();
|
||||
// Nursery rules have to be explicitly selected, so we ignore them when looking at prefixes.
|
||||
if is_nursery(group) {
|
||||
if is_preview(group) {
|
||||
prefix_to_codes
|
||||
.entry(code_str.clone())
|
||||
.or_default()
|
||||
@@ -126,13 +126,13 @@ pub(crate) fn get_prefix_ident(prefix: &str) -> Ident {
|
||||
Ident::new(&prefix, Span::call_site())
|
||||
}
|
||||
|
||||
/// Returns true if the given group is the "nursery" group.
|
||||
pub(crate) fn is_nursery(group: &Path) -> bool {
|
||||
/// Returns true if the given group is the "preview" group.
|
||||
pub(crate) fn is_preview(group: &Path) -> bool {
|
||||
let group = group
|
||||
.segments
|
||||
.iter()
|
||||
.map(|segment| segment.ident.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("::");
|
||||
group == "RuleGroup::Nursery"
|
||||
group == "RuleGroup::Preview"
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
[package]
|
||||
name = "ruff_notebook"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
ruff_diagnostics = { path = "../ruff_diagnostics" }
|
||||
ruff_source_file = { path = "../ruff_source_file" }
|
||||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { version = "3.0.0" }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
@@ -2716,7 +2716,7 @@ impl Ranged for crate::nodes::StmtContinue {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
impl Ranged for crate::nodes::StmtIpyEscapeCommand {
|
||||
impl Ranged for StmtIpyEscapeCommand {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
@@ -2888,7 +2888,7 @@ impl Ranged for crate::nodes::ExprSlice {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
impl Ranged for crate::nodes::ExprIpyEscapeCommand {
|
||||
impl Ranged for ExprIpyEscapeCommand {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
@@ -2927,6 +2927,7 @@ impl Ranged for crate::Expr {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for crate::nodes::Comprehension {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
@@ -2944,6 +2945,7 @@ impl Ranged for crate::ExceptHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for crate::nodes::Parameter {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
@@ -3084,173 +3086,6 @@ impl Ranged for crate::nodes::ParameterWithDefault {
|
||||
}
|
||||
}
|
||||
|
||||
/// An expression that may be parenthesized.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParenthesizedExpr {
|
||||
/// The range of the expression, including any parentheses.
|
||||
pub range: TextRange,
|
||||
/// The underlying expression.
|
||||
pub expr: Expr,
|
||||
}
|
||||
impl Ranged for ParenthesizedExpr {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
impl From<Expr> for ParenthesizedExpr {
|
||||
fn from(expr: Expr) -> Self {
|
||||
ParenthesizedExpr {
|
||||
range: expr.range(),
|
||||
expr,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<ParenthesizedExpr> for Expr {
|
||||
fn from(parenthesized_expr: ParenthesizedExpr) -> Self {
|
||||
parenthesized_expr.expr
|
||||
}
|
||||
}
|
||||
impl From<ExprIpyEscapeCommand> for ParenthesizedExpr {
|
||||
fn from(payload: ExprIpyEscapeCommand) -> Self {
|
||||
Expr::IpyEscapeCommand(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprBoolOp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprBoolOp) -> Self {
|
||||
Expr::BoolOp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprNamedExpr> for ParenthesizedExpr {
|
||||
fn from(payload: ExprNamedExpr) -> Self {
|
||||
Expr::NamedExpr(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprBinOp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprBinOp) -> Self {
|
||||
Expr::BinOp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprUnaryOp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprUnaryOp) -> Self {
|
||||
Expr::UnaryOp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprLambda> for ParenthesizedExpr {
|
||||
fn from(payload: ExprLambda) -> Self {
|
||||
Expr::Lambda(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprIfExp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprIfExp) -> Self {
|
||||
Expr::IfExp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprDict> for ParenthesizedExpr {
|
||||
fn from(payload: ExprDict) -> Self {
|
||||
Expr::Dict(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprSet> for ParenthesizedExpr {
|
||||
fn from(payload: ExprSet) -> Self {
|
||||
Expr::Set(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprListComp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprListComp) -> Self {
|
||||
Expr::ListComp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprSetComp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprSetComp) -> Self {
|
||||
Expr::SetComp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprDictComp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprDictComp) -> Self {
|
||||
Expr::DictComp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprGeneratorExp> for ParenthesizedExpr {
|
||||
fn from(payload: ExprGeneratorExp) -> Self {
|
||||
Expr::GeneratorExp(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprAwait> for ParenthesizedExpr {
|
||||
fn from(payload: ExprAwait) -> Self {
|
||||
Expr::Await(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprYield> for ParenthesizedExpr {
|
||||
fn from(payload: ExprYield) -> Self {
|
||||
Expr::Yield(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprYieldFrom> for ParenthesizedExpr {
|
||||
fn from(payload: ExprYieldFrom) -> Self {
|
||||
Expr::YieldFrom(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprCompare> for ParenthesizedExpr {
|
||||
fn from(payload: ExprCompare) -> Self {
|
||||
Expr::Compare(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprCall> for ParenthesizedExpr {
|
||||
fn from(payload: ExprCall) -> Self {
|
||||
Expr::Call(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprFormattedValue> for ParenthesizedExpr {
|
||||
fn from(payload: ExprFormattedValue) -> Self {
|
||||
Expr::FormattedValue(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprFString> for ParenthesizedExpr {
|
||||
fn from(payload: ExprFString) -> Self {
|
||||
Expr::FString(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprConstant> for ParenthesizedExpr {
|
||||
fn from(payload: ExprConstant) -> Self {
|
||||
Expr::Constant(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprAttribute> for ParenthesizedExpr {
|
||||
fn from(payload: ExprAttribute) -> Self {
|
||||
Expr::Attribute(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprSubscript> for ParenthesizedExpr {
|
||||
fn from(payload: ExprSubscript) -> Self {
|
||||
Expr::Subscript(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprStarred> for ParenthesizedExpr {
|
||||
fn from(payload: ExprStarred) -> Self {
|
||||
Expr::Starred(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprName> for ParenthesizedExpr {
|
||||
fn from(payload: ExprName) -> Self {
|
||||
Expr::Name(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprList> for ParenthesizedExpr {
|
||||
fn from(payload: ExprList) -> Self {
|
||||
Expr::List(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprTuple> for ParenthesizedExpr {
|
||||
fn from(payload: ExprTuple) -> Self {
|
||||
Expr::Tuple(payload).into()
|
||||
}
|
||||
}
|
||||
impl From<ExprSlice> for ParenthesizedExpr {
|
||||
fn from(payload: ExprSlice) -> Self {
|
||||
Expr::Slice(payload).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
mod size_assertions {
|
||||
use static_assertions::assert_eq_size;
|
||||
|
||||
@@ -33,27 +33,3 @@ result_f = (
|
||||
# comment
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}' # comment
|
||||
f'{2}'
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}'
|
||||
f'{2}' # comment
|
||||
)
|
||||
|
||||
(
|
||||
1, ( # comment
|
||||
f'{2}'
|
||||
)
|
||||
)
|
||||
|
||||
(
|
||||
(
|
||||
f'{1}'
|
||||
# comment
|
||||
),
|
||||
2
|
||||
)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
def func():
|
||||
pass
|
||||
# fmt: off
|
||||
x = 1
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
x = 1
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
# fmt: off
|
||||
def func():
|
||||
pass
|
||||
|
||||
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
# fmt: on
|
||||
def func():
|
||||
pass
|
||||
@@ -1,161 +0,0 @@
|
||||
###
|
||||
# Blank lines around functions
|
||||
###
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
x = 1
|
||||
|
||||
# comment
|
||||
# comment
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
# comment
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
# comment
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
###
|
||||
# Blank lines around imports.
|
||||
###
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
# comment
|
||||
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
|
||||
# comment
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x
|
||||
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f():
|
||||
import x # comment
|
||||
# comment
|
||||
|
||||
import y
|
||||
|
||||
|
||||
def f(): pass # comment
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
# comment
|
||||
|
||||
x = 1
|
||||
@@ -1,29 +1,6 @@
|
||||
# Pragma reserved width fixtures
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # nocoverage: This should break
|
||||
|
||||
|
||||
# Pragma fixtures for non-breaking space (lead by NBSP)
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pyright: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # pylint: This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa This shouldn't break
|
||||
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # nocoverage: This should break
|
||||
|
||||
|
||||
# As of adding this fixture Black adds a space before the non-breaking space if part of a type pragma.
|
||||
# https://github.com/psf/black/blob/b4dca26c7d93f930bbd5a7b552807370b60d4298/src/black/comments.py#L122-L129
|
||||
i = "" # type: Add space before leading NBSP followed by spaces
|
||||
i = "" #type: A space is added
|
||||
i = "" # type: Add space before leading NBSP followed by a space
|
||||
i = "" # type: Add space before leading NBSP
|
||||
i = "" # type: Add space before two leading NBSP
|
||||
|
||||
|
||||
# A noqa as `#\u{A0}\u{A0}noqa` becomes `# \u{A0}noqa`
|
||||
i = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # noqa
|
||||
i2 = "" # type: Add space before leading NBSP followed by spaces
|
||||
i3 = "" #type: A space is added
|
||||
i4 = "" # type: Add space before leading NBSP followed by a space
|
||||
i5 = "" # type: Add space before leading NBSP
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
|
||||
use ruff_python_ast::node::{AnyNodeRef, AstNode};
|
||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::comments::{CommentLinePosition, SourceComment};
|
||||
use crate::context::NodeLevel;
|
||||
@@ -299,10 +299,10 @@ impl Format<PyFormatContext<'_>> for FormatComment<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper that inserts the appropriate number of empty lines before a comment, depending on the node level:
|
||||
/// - Top-level: Up to two empty lines.
|
||||
/// - Parenthesized: A single empty line.
|
||||
/// - Otherwise: Up to a single empty line.
|
||||
// Helper that inserts the appropriate number of empty lines before a comment, depending on the node level.
|
||||
// Top level: Up to two empty lines
|
||||
// parenthesized: A single empty line
|
||||
// other: Up to a single empty line
|
||||
pub(crate) const fn empty_lines(lines: u32) -> FormatEmptyLines {
|
||||
FormatEmptyLines { lines }
|
||||
}
|
||||
@@ -357,33 +357,17 @@ impl Format<PyFormatContext<'_>> for FormatTrailingEndOfLineComment<'_> {
|
||||
|
||||
let normalized_comment = normalize_comment(self.comment, source)?;
|
||||
|
||||
// Trim the normalized comment to detect excluded pragmas (strips NBSP).
|
||||
let trimmed = strip_comment_prefix(&normalized_comment)?.trim_start();
|
||||
// Start with 2 because of the two leading spaces.
|
||||
let mut reserved_width = 2;
|
||||
|
||||
let is_pragma = if let Some((maybe_pragma, _)) = trimmed.split_once(':') {
|
||||
matches!(maybe_pragma, "noqa" | "type" | "pyright" | "pylint")
|
||||
} else {
|
||||
trimmed.starts_with("noqa")
|
||||
};
|
||||
|
||||
// Don't reserve width for excluded pragma comments.
|
||||
let reserved_width = if is_pragma {
|
||||
0
|
||||
} else {
|
||||
// Start with 2 because of the two leading spaces.
|
||||
let mut width = 2;
|
||||
|
||||
// SAFETY: The formatted file is <= 4GB, and each comment should as well.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
for c in normalized_comment.chars() {
|
||||
width += match c {
|
||||
'\t' => f.options().tab_width().value(),
|
||||
c => c.width().unwrap_or(0) as u32,
|
||||
}
|
||||
// SAFE: The formatted file is <= 4GB, and each comment should as well.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
for c in normalized_comment.chars() {
|
||||
reserved_width += match c {
|
||||
'\t' => f.options().tab_width().value(),
|
||||
c => c.width().unwrap_or(0) as u32,
|
||||
}
|
||||
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
@@ -458,7 +442,11 @@ fn normalize_comment<'a>(
|
||||
|
||||
let trimmed = comment_text.trim_end();
|
||||
|
||||
let content = strip_comment_prefix(trimmed)?;
|
||||
let Some(content) = trimmed.strip_prefix('#') else {
|
||||
return Err(FormatError::syntax_error(
|
||||
"Didn't find expected comment token `#`",
|
||||
));
|
||||
};
|
||||
|
||||
if content.is_empty() {
|
||||
return Ok(Cow::Borrowed("#"));
|
||||
@@ -474,70 +462,16 @@ fn normalize_comment<'a>(
|
||||
if content.starts_with('\u{A0}') {
|
||||
let trimmed = content.trim_start_matches('\u{A0}');
|
||||
|
||||
// Black adds a space before the non-breaking space if part of a type pragma.
|
||||
if trimmed.trim_start().starts_with("type:") {
|
||||
// Black adds a space before the non-breaking space if part of a type pragma.
|
||||
Ok(Cow::Owned(std::format!("# {content}")))
|
||||
} else if trimmed.starts_with(' ') {
|
||||
// Black replaces the non-breaking space with a space if followed by a space.
|
||||
Ok(Cow::Owned(std::format!("# {trimmed}")))
|
||||
} else {
|
||||
// Otherwise we replace the first non-breaking space with a regular space.
|
||||
Ok(Cow::Owned(std::format!("# {}", &content["\u{A0}".len()..])))
|
||||
return Ok(Cow::Owned(std::format!("# \u{A0}{trimmed}")));
|
||||
}
|
||||
} else {
|
||||
Ok(Cow::Owned(std::format!("# {}", content.trim_start())))
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for stripping '#' from comments.
|
||||
fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
|
||||
let Some(content) = comment_text.strip_prefix('#') else {
|
||||
return Err(FormatError::syntax_error(
|
||||
"Didn't find expected comment token `#`",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Format the empty lines between a node and its trailing comments.
|
||||
///
|
||||
/// For example, given:
|
||||
/// ```python
|
||||
/// def func():
|
||||
/// ...
|
||||
/// # comment
|
||||
/// ```
|
||||
///
|
||||
/// This builder will insert two empty lines before the comment.
|
||||
/// ```
|
||||
pub(crate) const fn empty_lines_before_trailing_comments(
|
||||
comments: &[SourceComment],
|
||||
expected: u32,
|
||||
) -> FormatEmptyLinesBeforeTrailingComments {
|
||||
FormatEmptyLinesBeforeTrailingComments { comments, expected }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct FormatEmptyLinesBeforeTrailingComments<'a> {
|
||||
/// The trailing comments of the node.
|
||||
comments: &'a [SourceComment],
|
||||
/// The expected number of empty lines before the trailing comments.
|
||||
expected: u32,
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
|
||||
if let Some(comment) = self
|
||||
.comments
|
||||
.iter()
|
||||
.find(|comment| comment.line_position().is_own_line())
|
||||
{
|
||||
let actual = lines_before(comment.start(), f.context().source()).saturating_sub(1);
|
||||
for _ in actual..self.expected {
|
||||
write!(f, [empty_line()])?;
|
||||
}
|
||||
// Black replaces the non-breaking space with a space if followed by a space.
|
||||
if trimmed.starts_with(' ') {
|
||||
return Ok(Cow::Owned(std::format!("# {trimmed}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(Cow::Owned(std::format!("# {}", content.trim_start())))
|
||||
}
|
||||
|
||||
@@ -70,20 +70,6 @@ fn handle_parenthesized_comment<'a>(
|
||||
comment: DecoratedComment<'a>,
|
||||
locator: &Locator,
|
||||
) -> CommentPlacement<'a> {
|
||||
// As a special-case, ignore comments within f-strings, like:
|
||||
// ```python
|
||||
// (
|
||||
// f'{1}' # comment
|
||||
// f'{2}'
|
||||
// )
|
||||
// ```
|
||||
// These can't be parenthesized, as they must fall between two string tokens in an implicit
|
||||
// concatenation. But the expression ranges only include the `1` and `2` above, so we also
|
||||
// can't lex the contents between them.
|
||||
if comment.enclosing_node().is_expr_f_string() {
|
||||
return CommentPlacement::Default(comment);
|
||||
}
|
||||
|
||||
let Some(preceding) = comment.preceding_node() else {
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
@@ -439,7 +425,7 @@ fn handle_own_line_comment_around_body<'a>(
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
|
||||
// If there's any non-trivia token between the preceding node and the comment, then it means
|
||||
// If there's any non-trivia token between the preceding node and the comment, than it means
|
||||
// we're past the case of the alternate branch, defer to the default rules
|
||||
// ```python
|
||||
// if a:
|
||||
@@ -460,78 +446,11 @@ fn handle_own_line_comment_around_body<'a>(
|
||||
}
|
||||
|
||||
// Check if we're between bodies and should attach to the following body.
|
||||
handle_own_line_comment_between_branches(comment, preceding, locator)
|
||||
.or_else(|comment| {
|
||||
// Otherwise, there's no following branch or the indentation is too deep, so attach to the
|
||||
// recursively last statement in the preceding body with the matching indentation.
|
||||
handle_own_line_comment_after_branch(comment, preceding, locator)
|
||||
})
|
||||
.or_else(|comment| handle_own_line_comment_between_statements(comment, locator))
|
||||
}
|
||||
|
||||
/// Handles own-line comments between statements. If an own-line comment is between two statements,
|
||||
/// it's treated as a leading comment of the following statement _if_ there are no empty lines
|
||||
/// separating the comment and the statement; otherwise, it's treated as a trailing comment of the
|
||||
/// preceding statement.
|
||||
///
|
||||
/// For example, this comment would be a trailing comment of `x = 1`:
|
||||
/// ```python
|
||||
/// x = 1
|
||||
/// # comment
|
||||
///
|
||||
/// y = 2
|
||||
/// ```
|
||||
///
|
||||
/// However, this comment would be a leading comment of `y = 2`:
|
||||
/// ```python
|
||||
/// x = 1
|
||||
///
|
||||
/// # comment
|
||||
/// y = 2
|
||||
/// ```
|
||||
fn handle_own_line_comment_between_statements<'a>(
|
||||
comment: DecoratedComment<'a>,
|
||||
locator: &Locator,
|
||||
) -> CommentPlacement<'a> {
|
||||
let Some(preceding) = comment.preceding_node() else {
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
|
||||
let Some(following) = comment.following_node() else {
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
|
||||
// We're looking for comments between two statements, like:
|
||||
// ```python
|
||||
// x = 1
|
||||
// # comment
|
||||
// y = 2
|
||||
// ```
|
||||
if !preceding.is_statement() || !following.is_statement() {
|
||||
return CommentPlacement::Default(comment);
|
||||
}
|
||||
|
||||
// If the comment is directly attached to the following statement; make it a leading
|
||||
// comment:
|
||||
// ```python
|
||||
// x = 1
|
||||
//
|
||||
// # leading comment
|
||||
// y = 2
|
||||
// ```
|
||||
//
|
||||
// Otherwise, if there's at least one empty line, make it a trailing comment:
|
||||
// ```python
|
||||
// x = 1
|
||||
// # trailing comment
|
||||
//
|
||||
// y = 2
|
||||
// ```
|
||||
if max_empty_lines(locator.slice(TextRange::new(comment.end(), following.start()))) == 0 {
|
||||
CommentPlacement::leading(following, comment)
|
||||
} else {
|
||||
CommentPlacement::trailing(preceding, comment)
|
||||
}
|
||||
handle_own_line_comment_between_branches(comment, preceding, locator).or_else(|comment| {
|
||||
// Otherwise, there's no following branch or the indentation is too deep, so attach to the
|
||||
// recursively last statement in the preceding body with the matching indentation.
|
||||
handle_own_line_comment_after_branch(comment, preceding, locator)
|
||||
})
|
||||
}
|
||||
|
||||
/// Handles own line comments between two branches of a node.
|
||||
@@ -1918,7 +1837,6 @@ fn max_empty_lines(contents: &str) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
max_new_lines = newlines.max(max_new_lines);
|
||||
max_new_lines.saturating_sub(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,7 @@ expression: comments.debug(test_case.source_code)
|
||||
},
|
||||
],
|
||||
"dangling": [],
|
||||
"trailing": [
|
||||
SourceComment {
|
||||
text: "# own line comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
"trailing": [],
|
||||
},
|
||||
Node {
|
||||
kind: StmtIf,
|
||||
@@ -54,4 +48,19 @@ expression: comments.debug(test_case.source_code)
|
||||
"dangling": [],
|
||||
"trailing": [],
|
||||
},
|
||||
Node {
|
||||
kind: StmtExpr,
|
||||
range: 234..246,
|
||||
source: `test(10, 20)`,
|
||||
}: {
|
||||
"leading": [
|
||||
SourceComment {
|
||||
text: "# own line comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
"dangling": [],
|
||||
"trailing": [],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,21 +3,6 @@ source: crates/ruff_python_formatter/src/comments/mod.rs
|
||||
expression: comments.debug(test_case.source_code)
|
||||
---
|
||||
{
|
||||
Node {
|
||||
kind: StmtMatch,
|
||||
range: 27..550,
|
||||
source: `match pt:⏎`,
|
||||
}: {
|
||||
"leading": [],
|
||||
"dangling": [],
|
||||
"trailing": [
|
||||
SourceComment {
|
||||
text: "# After match comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
Node {
|
||||
kind: MatchCase,
|
||||
range: 84..132,
|
||||
@@ -123,4 +108,19 @@ expression: comments.debug(test_case.source_code)
|
||||
},
|
||||
],
|
||||
},
|
||||
Node {
|
||||
kind: StmtExpr,
|
||||
range: 656..670,
|
||||
source: `print("other")`,
|
||||
}: {
|
||||
"leading": [
|
||||
SourceComment {
|
||||
text: "# After match comment",
|
||||
position: OwnLine,
|
||||
formatted: false,
|
||||
},
|
||||
],
|
||||
"dangling": [],
|
||||
"trailing": [],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ use ruff_python_ast::{Decorator, StmtClassDef};
|
||||
use ruff_python_trivia::lines_after_ignoring_trivia;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::format::empty_lines_before_trailing_comments;
|
||||
use crate::comments::{leading_comments, trailing_comments, SourceComment};
|
||||
use crate::context::NodeLevel;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
|
||||
use crate::statement::suite::SuiteKind;
|
||||
@@ -110,33 +108,7 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
|
||||
),
|
||||
clause_body(body, trailing_definition_comments).with_kind(SuiteKind::Class),
|
||||
]
|
||||
)?;
|
||||
|
||||
// If the class contains trailing comments, insert newlines before them.
|
||||
// For example, given:
|
||||
// ```python
|
||||
// class Class:
|
||||
// ...
|
||||
// # comment
|
||||
// ```
|
||||
//
|
||||
// At the top-level, reformat as:
|
||||
// ```python
|
||||
// class Class:
|
||||
// ...
|
||||
//
|
||||
//
|
||||
// # comment
|
||||
// ```
|
||||
empty_lines_before_trailing_comments(
|
||||
comments.trailing(item),
|
||||
if f.context().node_level() == NodeLevel::TopLevel {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
},
|
||||
)
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
fn fmt_dangling_comments(
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use crate::comments::format::empty_lines_before_trailing_comments;
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::{Parameters, StmtFunctionDef};
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::context::NodeLevel;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::{Parentheses, Parenthesize};
|
||||
use crate::prelude::*;
|
||||
@@ -146,33 +144,7 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
|
||||
),
|
||||
clause_body(body, trailing_definition_comments).with_kind(SuiteKind::Function),
|
||||
]
|
||||
)?;
|
||||
|
||||
// If the function contains trailing comments, insert newlines before them.
|
||||
// For example, given:
|
||||
// ```python
|
||||
// def func():
|
||||
// ...
|
||||
// # comment
|
||||
// ```
|
||||
//
|
||||
// At the top-level, reformat as:
|
||||
// ```python
|
||||
// def func():
|
||||
// ...
|
||||
//
|
||||
//
|
||||
// # comment
|
||||
// ```
|
||||
empty_lines_before_trailing_comments(
|
||||
comments.trailing(item),
|
||||
if f.context().node_level() == NodeLevel::TopLevel {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
},
|
||||
)
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
fn fmt_dangling_comments(
|
||||
|
||||
@@ -2,7 +2,7 @@ use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWi
|
||||
use ruff_python_ast::helpers::is_compound_statement;
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::{self as ast, Constant, Expr, ExprConstant, Stmt, Suite};
|
||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
|
||||
use ruff_python_trivia::{lines_after_ignoring_trivia, lines_before};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::comments::{leading_comments, trailing_comments, Comments};
|
||||
@@ -143,11 +143,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
};
|
||||
|
||||
while let Some(following) = iter.next() {
|
||||
// Add empty lines before and after a function or class definition. If the preceding
|
||||
// node is a function or class, and contains trailing comments, then the statement
|
||||
// itself will add the requisite empty lines when formatting its comments.
|
||||
if (is_class_or_function_definition(preceding)
|
||||
&& !comments.has_trailing_own_line(preceding))
|
||||
if is_class_or_function_definition(preceding)
|
||||
|| is_class_or_function_definition(following)
|
||||
{
|
||||
match self.kind {
|
||||
@@ -195,13 +191,9 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
empty_line().fmt(f)?;
|
||||
}
|
||||
}
|
||||
} else if is_import_definition(preceding)
|
||||
&& (!is_import_definition(following) || comments.has_leading(following))
|
||||
{
|
||||
} else if is_import_definition(preceding) && !is_import_definition(following) {
|
||||
// Enforce _at least_ one empty line after an import statement (but allow up to
|
||||
// two at the top-level). In this context, "after an import statement" means that
|
||||
// that the previous node is an import, and the following node is an import _or_ has
|
||||
// a leading comment.
|
||||
// two at the top-level).
|
||||
match self.kind {
|
||||
SuiteKind::TopLevel => {
|
||||
match lines_after_ignoring_trivia(preceding.end(), source) {
|
||||
@@ -282,21 +274,16 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
// it then counts the lines between the statement and the trailing comment, which is
|
||||
// always 0. This is why it skips any trailing trivia (trivia that's on the same line)
|
||||
// and counts the lines after.
|
||||
lines_after(offset, source)
|
||||
lines_after_ignoring_trivia(offset, source)
|
||||
};
|
||||
|
||||
let end = comments
|
||||
.trailing(preceding)
|
||||
.last()
|
||||
.map_or(preceding.end(), |comment| comment.slice().end());
|
||||
|
||||
match node_level {
|
||||
NodeLevel::TopLevel => match count_lines(end) {
|
||||
NodeLevel::TopLevel => match count_lines(preceding.end()) {
|
||||
0 | 1 => hard_line_break().fmt(f)?,
|
||||
2 => empty_line().fmt(f)?,
|
||||
_ => write!(f, [empty_line(), empty_line()])?,
|
||||
},
|
||||
NodeLevel::CompoundStatement => match count_lines(end) {
|
||||
NodeLevel::CompoundStatement => match count_lines(preceding.end()) {
|
||||
0 | 1 => hard_line_break().fmt(f)?,
|
||||
_ => empty_line().fmt(f)?,
|
||||
},
|
||||
|
||||
@@ -162,7 +162,7 @@ def f():
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -1,29 +1,205 @@
|
||||
@@ -1,29 +1,182 @@
|
||||
+# This file doesn't use the standard decomposition.
|
||||
+# Decorator syntax test cases are separated by double # comments.
|
||||
+# Those before the 'output' comment are valid under the old syntax.
|
||||
@@ -179,7 +179,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator()
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -187,7 +186,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(arg)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -195,7 +193,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(kwarg=0)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -203,68 +200,15 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(*args)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(**kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(*args, **kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
+)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@dotted.decorator
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@dotted.decorator(arg)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@dotted.decorator(kwarg=0)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator()()
|
||||
+
|
||||
+@dotted.decorator(*args)
|
||||
+@decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
|
||||
@@ -272,8 +216,7 @@ def f():
|
||||
##
|
||||
|
||||
-@(decorator)
|
||||
+
|
||||
+@dotted.decorator(**kwargs)
|
||||
+@decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
|
||||
@@ -281,17 +224,7 @@ def f():
|
||||
##
|
||||
|
||||
-@sequence["decorator"]
|
||||
+
|
||||
+@dotted.decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator[List[str]]
|
||||
+
|
||||
+@dotted.decorator(
|
||||
+@decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
+)
|
||||
@@ -301,7 +234,59 @@ def f():
|
||||
+
|
||||
##
|
||||
|
||||
-@decorator[List[str]]
|
||||
+@dotted.decorator
|
||||
def f():
|
||||
...
|
||||
|
||||
+
|
||||
##
|
||||
|
||||
-@var := decorator
|
||||
+@dotted.decorator(arg)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+@dotted.decorator(kwarg=0)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+@dotted.decorator(*args)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+@dotted.decorator(**kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+@dotted.decorator(*args, **kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+@dotted.decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
+)
|
||||
+def f():
|
||||
+ ...
|
||||
+
|
||||
+
|
||||
+##
|
||||
+
|
||||
+@double.dotted.decorator
|
||||
+def f():
|
||||
@@ -310,7 +295,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(arg)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -318,7 +302,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(kwarg=0)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -326,7 +309,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(*args)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -334,7 +316,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(**kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -342,7 +323,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(*args, **kwargs)
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -350,7 +330,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@double.dotted.decorator(
|
||||
+ *args,
|
||||
+ **kwargs,
|
||||
@@ -361,7 +340,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@_(sequence["decorator"])
|
||||
+def f():
|
||||
+ ...
|
||||
@@ -369,7 +347,6 @@ def f():
|
||||
+
|
||||
+##
|
||||
+
|
||||
+
|
||||
+@eval("sequence['decorator']")
|
||||
def f():
|
||||
...
|
||||
@@ -394,7 +371,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator()
|
||||
def f():
|
||||
...
|
||||
@@ -402,7 +378,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(arg)
|
||||
def f():
|
||||
...
|
||||
@@ -410,7 +385,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(kwarg=0)
|
||||
def f():
|
||||
...
|
||||
@@ -418,7 +392,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(*args)
|
||||
def f():
|
||||
...
|
||||
@@ -426,7 +399,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -434,7 +406,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -442,7 +413,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@decorator(
|
||||
*args,
|
||||
**kwargs,
|
||||
@@ -453,7 +423,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator
|
||||
def f():
|
||||
...
|
||||
@@ -461,7 +430,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(arg)
|
||||
def f():
|
||||
...
|
||||
@@ -469,7 +437,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(kwarg=0)
|
||||
def f():
|
||||
...
|
||||
@@ -477,7 +444,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(*args)
|
||||
def f():
|
||||
...
|
||||
@@ -485,7 +451,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -493,7 +458,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -501,7 +465,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@dotted.decorator(
|
||||
*args,
|
||||
**kwargs,
|
||||
@@ -512,7 +475,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator
|
||||
def f():
|
||||
...
|
||||
@@ -520,7 +482,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(arg)
|
||||
def f():
|
||||
...
|
||||
@@ -528,7 +489,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(kwarg=0)
|
||||
def f():
|
||||
...
|
||||
@@ -536,7 +496,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(*args)
|
||||
def f():
|
||||
...
|
||||
@@ -544,7 +503,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(**kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -552,7 +510,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(*args, **kwargs)
|
||||
def f():
|
||||
...
|
||||
@@ -560,7 +517,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@double.dotted.decorator(
|
||||
*args,
|
||||
**kwargs,
|
||||
@@ -571,7 +527,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@_(sequence["decorator"])
|
||||
def f():
|
||||
...
|
||||
@@ -579,7 +534,6 @@ def f():
|
||||
|
||||
##
|
||||
|
||||
|
||||
@eval("sequence['decorator']")
|
||||
def f():
|
||||
...
|
||||
|
||||
@@ -156,7 +156,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite
|
||||
)
|
||||
|
||||
|
||||
@@ -108,11 +112,18 @@
|
||||
@@ -108,11 +112,20 @@
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
)
|
||||
|
||||
@@ -176,7 +176,10 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite
|
||||
+ ], # type: ignore
|
||||
)
|
||||
|
||||
aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type]
|
||||
-aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type]
|
||||
+aaaaaaaaaaaaa, bbbbbbbbb = map(
|
||||
+ list, map(itertools.chain.from_iterable, zip(*items))
|
||||
+) # type: ignore[arg-type]
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -310,7 +313,9 @@ call_to_some_function_asdf(
|
||||
], # type: ignore
|
||||
)
|
||||
|
||||
aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type]
|
||||
aaaaaaaaaaaaa, bbbbbbbbb = map(
|
||||
list, map(itertools.chain.from_iterable, zip(*items))
|
||||
) # type: ignore[arg-type]
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/empty_lines.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```py
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
# leading comment
|
||||
def f():
|
||||
NO = ''
|
||||
SPACE = ' '
|
||||
DOUBLESPACE = ' '
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent # trailing comment
|
||||
v = leaf.value
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT: # another trailing comment
|
||||
return DOUBLESPACE
|
||||
|
||||
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
|
||||
|
||||
return NO
|
||||
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.DOUBLESTAR:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.dictsetmaker,
|
||||
}:
|
||||
return NO
|
||||
|
||||
###############################################################################
|
||||
# SECTION BECAUSE SECTIONS
|
||||
###############################################################################
|
||||
|
||||
def g():
|
||||
NO = ''
|
||||
SPACE = ' '
|
||||
DOUBLESPACE = ' '
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent
|
||||
v = leaf.value
|
||||
|
||||
# Comment because comments
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT:
|
||||
return DOUBLESPACE
|
||||
|
||||
# Another comment because more comments
|
||||
assert p is not None, f'INTERNAL ERROR: hand-made leaf without parent: {leaf!r}'
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
# Start of the line or a bracketed expression.
|
||||
# More than one line for the comment.
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -49,7 +49,6 @@
|
||||
# SECTION BECAUSE SECTIONS
|
||||
###############################################################################
|
||||
|
||||
-
|
||||
def g():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
# leading comment
|
||||
def f():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
DOUBLESPACE = " "
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent # trailing comment
|
||||
v = leaf.value
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT: # another trailing comment
|
||||
return DOUBLESPACE
|
||||
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.DOUBLESTAR:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.dictsetmaker,
|
||||
}:
|
||||
return NO
|
||||
|
||||
|
||||
###############################################################################
|
||||
# SECTION BECAUSE SECTIONS
|
||||
###############################################################################
|
||||
|
||||
def g():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
DOUBLESPACE = " "
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent
|
||||
v = leaf.value
|
||||
|
||||
# Comment because comments
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT:
|
||||
return DOUBLESPACE
|
||||
|
||||
# Another comment because more comments
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
# Start of the line or a bracketed expression.
|
||||
# More than one line for the comment.
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```py
|
||||
"""Docstring."""
|
||||
|
||||
|
||||
# leading comment
|
||||
def f():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
DOUBLESPACE = " "
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent # trailing comment
|
||||
v = leaf.value
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT: # another trailing comment
|
||||
return DOUBLESPACE
|
||||
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
|
||||
elif prevp.type == token.DOUBLESTAR:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.dictsetmaker,
|
||||
}:
|
||||
return NO
|
||||
|
||||
|
||||
###############################################################################
|
||||
# SECTION BECAUSE SECTIONS
|
||||
###############################################################################
|
||||
|
||||
|
||||
def g():
|
||||
NO = ""
|
||||
SPACE = " "
|
||||
DOUBLESPACE = " "
|
||||
|
||||
t = leaf.type
|
||||
p = leaf.parent
|
||||
v = leaf.value
|
||||
|
||||
# Comment because comments
|
||||
|
||||
if t in ALWAYS_NO_SPACE:
|
||||
pass
|
||||
if t == token.COMMENT:
|
||||
return DOUBLESPACE
|
||||
|
||||
# Another comment because more comments
|
||||
assert p is not None, f"INTERNAL ERROR: hand-made leaf without parent: {leaf!r}"
|
||||
|
||||
prev = leaf.prev_sibling
|
||||
if not prev:
|
||||
prevp = preceding_leaf(p)
|
||||
|
||||
if not prevp or prevp.type in OPENING_BRACKETS:
|
||||
# Start of the line or a bracketed expression.
|
||||
# More than one line for the comment.
|
||||
return NO
|
||||
|
||||
if prevp.type == token.EQUAL:
|
||||
if prevp.parent and prevp.parent.type in {
|
||||
syms.typedargslist,
|
||||
syms.varargslist,
|
||||
syms.parameters,
|
||||
syms.arglist,
|
||||
syms.argument,
|
||||
}:
|
||||
return NO
|
||||
```
|
||||
|
||||
|
||||
@@ -300,6 +300,17 @@ last_call()
|
||||
) # note: no trailing comma pre-3.6
|
||||
call(*gidgets[:2])
|
||||
call(a, *gidgets[:2])
|
||||
@@ -142,7 +143,9 @@
|
||||
xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
|
||||
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
|
||||
)
|
||||
-xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
|
||||
+xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[
|
||||
+ ..., List[SomeClass]
|
||||
+] = classmethod( # type: ignore
|
||||
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
|
||||
)
|
||||
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod(
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -450,7 +461,9 @@ very_long_variable_name_filters: t.List[
|
||||
xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
|
||||
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
|
||||
)
|
||||
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod( # type: ignore
|
||||
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[
|
||||
..., List[SomeClass]
|
||||
] = classmethod( # type: ignore
|
||||
sync(async_xxxx_xxx_xxxx_xxxxx_xxxx_xxx.__func__)
|
||||
)
|
||||
xxxx_xxx_xxxx_xxxxx_xxxx_xxx: Callable[..., List[SomeClass]] = classmethod(
|
||||
|
||||
@@ -198,15 +198,7 @@ d={'a':1,
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -5,6 +5,7 @@
|
||||
from third_party import X, Y, Z
|
||||
|
||||
from library import some_connection, some_decorator
|
||||
+
|
||||
# fmt: off
|
||||
from third_party import (X,
|
||||
Y, Z)
|
||||
@@ -63,15 +64,15 @@
|
||||
@@ -63,15 +63,15 @@
|
||||
|
||||
something = {
|
||||
# fmt: off
|
||||
@@ -225,7 +217,7 @@ d={'a':1,
|
||||
# fmt: on
|
||||
goes + here,
|
||||
andhere,
|
||||
@@ -122,8 +123,10 @@
|
||||
@@ -122,8 +122,10 @@
|
||||
"""
|
||||
# fmt: off
|
||||
|
||||
@@ -237,7 +229,7 @@ d={'a':1,
|
||||
# fmt: on
|
||||
pass
|
||||
|
||||
@@ -138,7 +141,7 @@
|
||||
@@ -138,7 +140,7 @@
|
||||
now . considers . multiple . fmt . directives . within . one . prefix
|
||||
# fmt: on
|
||||
# fmt: off
|
||||
@@ -246,7 +238,7 @@ d={'a':1,
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -178,14 +181,18 @@
|
||||
@@ -178,14 +180,18 @@
|
||||
$
|
||||
""",
|
||||
# fmt: off
|
||||
@@ -279,7 +271,6 @@ import sys
|
||||
from third_party import X, Y, Z
|
||||
|
||||
from library import some_connection, some_decorator
|
||||
|
||||
# fmt: off
|
||||
from third_party import (X,
|
||||
Y, Z)
|
||||
|
||||
@@ -110,7 +110,15 @@ elif unformatted:
|
||||
},
|
||||
)
|
||||
|
||||
@@ -82,6 +81,6 @@
|
||||
@@ -74,7 +73,6 @@
|
||||
class Factory(t.Protocol):
|
||||
def this_will_be_formatted(self, **kwargs) -> Named:
|
||||
...
|
||||
-
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -82,6 +80,6 @@
|
||||
if x:
|
||||
return x
|
||||
# fmt: off
|
||||
@@ -198,7 +206,6 @@ class Named(t.Protocol):
|
||||
class Factory(t.Protocol):
|
||||
def this_will_be_formatted(self, **kwargs) -> Named:
|
||||
...
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```py
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -3,6 +3,7 @@
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
+
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```py
|
||||
# Regression test for https://github.com/psf/black/issues/3438
|
||||
|
||||
import ast
|
||||
import collections # fmt: skip
|
||||
import dataclasses
|
||||
# fmt: off
|
||||
import os
|
||||
# fmt: on
|
||||
import pathlib
|
||||
|
||||
import re # fmt: skip
|
||||
import secrets
|
||||
|
||||
# fmt: off
|
||||
import sys
|
||||
# fmt: on
|
||||
|
||||
import tempfile
|
||||
import zoneinfo
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/power_op_spacing.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```py
|
||||
def function(**kwargs):
|
||||
t = a**2 + b**3
|
||||
return t ** 2
|
||||
|
||||
|
||||
def function_replace_spaces(**kwargs):
|
||||
t = a **2 + b** 3 + c ** 4
|
||||
|
||||
|
||||
def function_dont_replace_spaces():
|
||||
{**a, **b, **c}
|
||||
|
||||
|
||||
a = 5**~4
|
||||
b = 5 ** f()
|
||||
c = -(5**2)
|
||||
d = 5 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5
|
||||
g = a.b**c.d
|
||||
h = 5 ** funcs.f()
|
||||
i = funcs.f() ** 5
|
||||
j = super().name ** 5
|
||||
k = [(2**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2**63], [1, 2**63])]
|
||||
n = count <= 10**5
|
||||
o = settings(max_examples=10**6)
|
||||
p = {(k, k**2): v**2 for k, v in pairs}
|
||||
q = [10**i for i in range(6)]
|
||||
r = x**y
|
||||
|
||||
a = 5.0**~4.0
|
||||
b = 5.0 ** f()
|
||||
c = -(5.0**2.0)
|
||||
d = 5.0 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5.0
|
||||
g = a.b**c.d
|
||||
h = 5.0 ** funcs.f()
|
||||
i = funcs.f() ** 5.0
|
||||
j = super().name ** 5.0
|
||||
k = [(2.0**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2.0**63.0], [1.0, 2**63.0])]
|
||||
n = count <= 10**5.0
|
||||
o = settings(max_examples=10**6.0)
|
||||
p = {(k, k**2): v**2.0 for k, v in pairs}
|
||||
q = [10.5**i for i in range(6)]
|
||||
|
||||
|
||||
# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
|
||||
if hasattr(view, "sum_of_weights"):
|
||||
return np.divide( # type: ignore[no-any-return]
|
||||
view.variance, # type: ignore[union-attr]
|
||||
view.sum_of_weights, # type: ignore[union-attr]
|
||||
out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
|
||||
where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
return np.divide(
|
||||
where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -55,9 +55,11 @@
|
||||
view.variance, # type: ignore[union-attr]
|
||||
view.sum_of_weights, # type: ignore[union-attr]
|
||||
out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
|
||||
- where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
+ where=view.sum_of_weights**2
|
||||
+ > view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
return np.divide(
|
||||
- where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore
|
||||
+ where=view.sum_of_weights_of_weight_long**2
|
||||
+ > view.sum_of_weights_squared, # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```py
|
||||
def function(**kwargs):
|
||||
t = a**2 + b**3
|
||||
return t**2
|
||||
|
||||
|
||||
def function_replace_spaces(**kwargs):
|
||||
t = a**2 + b**3 + c**4
|
||||
|
||||
|
||||
def function_dont_replace_spaces():
|
||||
{**a, **b, **c}
|
||||
|
||||
|
||||
a = 5**~4
|
||||
b = 5 ** f()
|
||||
c = -(5**2)
|
||||
d = 5 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5
|
||||
g = a.b**c.d
|
||||
h = 5 ** funcs.f()
|
||||
i = funcs.f() ** 5
|
||||
j = super().name ** 5
|
||||
k = [(2**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2**63], [1, 2**63])]
|
||||
n = count <= 10**5
|
||||
o = settings(max_examples=10**6)
|
||||
p = {(k, k**2): v**2 for k, v in pairs}
|
||||
q = [10**i for i in range(6)]
|
||||
r = x**y
|
||||
|
||||
a = 5.0**~4.0
|
||||
b = 5.0 ** f()
|
||||
c = -(5.0**2.0)
|
||||
d = 5.0 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5.0
|
||||
g = a.b**c.d
|
||||
h = 5.0 ** funcs.f()
|
||||
i = funcs.f() ** 5.0
|
||||
j = super().name ** 5.0
|
||||
k = [(2.0**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2.0**63.0], [1.0, 2**63.0])]
|
||||
n = count <= 10**5.0
|
||||
o = settings(max_examples=10**6.0)
|
||||
p = {(k, k**2): v**2.0 for k, v in pairs}
|
||||
q = [10.5**i for i in range(6)]
|
||||
|
||||
|
||||
# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
|
||||
if hasattr(view, "sum_of_weights"):
|
||||
return np.divide( # type: ignore[no-any-return]
|
||||
view.variance, # type: ignore[union-attr]
|
||||
view.sum_of_weights, # type: ignore[union-attr]
|
||||
out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
|
||||
where=view.sum_of_weights**2
|
||||
> view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
return np.divide(
|
||||
where=view.sum_of_weights_of_weight_long**2
|
||||
> view.sum_of_weights_squared, # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```py
|
||||
def function(**kwargs):
|
||||
t = a**2 + b**3
|
||||
return t**2
|
||||
|
||||
|
||||
def function_replace_spaces(**kwargs):
|
||||
t = a**2 + b**3 + c**4
|
||||
|
||||
|
||||
def function_dont_replace_spaces():
|
||||
{**a, **b, **c}
|
||||
|
||||
|
||||
a = 5**~4
|
||||
b = 5 ** f()
|
||||
c = -(5**2)
|
||||
d = 5 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5
|
||||
g = a.b**c.d
|
||||
h = 5 ** funcs.f()
|
||||
i = funcs.f() ** 5
|
||||
j = super().name ** 5
|
||||
k = [(2**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2**63], [1, 2**63])]
|
||||
n = count <= 10**5
|
||||
o = settings(max_examples=10**6)
|
||||
p = {(k, k**2): v**2 for k, v in pairs}
|
||||
q = [10**i for i in range(6)]
|
||||
r = x**y
|
||||
|
||||
a = 5.0**~4.0
|
||||
b = 5.0 ** f()
|
||||
c = -(5.0**2.0)
|
||||
d = 5.0 ** f["hi"]
|
||||
e = lazy(lambda **kwargs: 5)
|
||||
f = f() ** 5.0
|
||||
g = a.b**c.d
|
||||
h = 5.0 ** funcs.f()
|
||||
i = funcs.f() ** 5.0
|
||||
j = super().name ** 5.0
|
||||
k = [(2.0**idx, value) for idx, value in pairs]
|
||||
l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001)
|
||||
m = [([2.0**63.0], [1.0, 2**63.0])]
|
||||
n = count <= 10**5.0
|
||||
o = settings(max_examples=10**6.0)
|
||||
p = {(k, k**2): v**2.0 for k, v in pairs}
|
||||
q = [10.5**i for i in range(6)]
|
||||
|
||||
|
||||
# WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873)
|
||||
if hasattr(view, "sum_of_weights"):
|
||||
return np.divide( # type: ignore[no-any-return]
|
||||
view.variance, # type: ignore[union-attr]
|
||||
view.sum_of_weights, # type: ignore[union-attr]
|
||||
out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr]
|
||||
where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
return np.divide(
|
||||
where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -50,14 +50,17 @@ assert (
|
||||
) #
|
||||
|
||||
assert sort_by_dependency(
|
||||
@@ -25,9 +25,9 @@
|
||||
@@ -25,9 +25,11 @@
|
||||
class A:
|
||||
def foo(self):
|
||||
for _ in range(10):
|
||||
- aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc(
|
||||
+ aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
|
||||
xxxxxxxxxxxx
|
||||
- xxxxxxxxxxxx
|
||||
- ) # pylint: disable=no-member
|
||||
+ aaaaaaaaaaaaaaaaaaa = (
|
||||
+ bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
|
||||
+ xxxxxxxxxxxx
|
||||
+ )
|
||||
+ )
|
||||
|
||||
|
||||
@@ -94,8 +97,10 @@ importA
|
||||
class A:
|
||||
def foo(self):
|
||||
for _ in range(10):
|
||||
aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
|
||||
xxxxxxxxxxxx
|
||||
aaaaaaaaaaaaaaaaaaa = (
|
||||
bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member
|
||||
xxxxxxxxxxxx
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -39,30 +39,6 @@ result_f = (
|
||||
# comment
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}' # comment
|
||||
f'{2}'
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}'
|
||||
f'{2}' # comment
|
||||
)
|
||||
|
||||
(
|
||||
1, ( # comment
|
||||
f'{2}'
|
||||
)
|
||||
)
|
||||
|
||||
(
|
||||
(
|
||||
f'{1}'
|
||||
# comment
|
||||
),
|
||||
2
|
||||
)
|
||||
```
|
||||
|
||||
## Output
|
||||
@@ -100,30 +76,6 @@ result_f = (
|
||||
# comment
|
||||
""
|
||||
)
|
||||
|
||||
(
|
||||
f"{1}" # comment
|
||||
f"{2}"
|
||||
)
|
||||
|
||||
(
|
||||
f"{1}" f"{2}" # comment
|
||||
)
|
||||
|
||||
(
|
||||
1,
|
||||
( # comment
|
||||
f"{2}"
|
||||
),
|
||||
)
|
||||
|
||||
(
|
||||
(
|
||||
f"{1}"
|
||||
# comment
|
||||
),
|
||||
2,
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,61 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off
|
||||
---
|
||||
## Input
|
||||
```py
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted;
|
||||
```
|
||||
|
||||
## Outputs
|
||||
@@ -17,6 +72,63 @@ magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
```py
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
|
||||
@@ -30,6 +142,63 @@ magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
```py
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
|
||||
@@ -43,6 +212,63 @@ magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
```py
|
||||
def test():
|
||||
# fmt: off
|
||||
a_very_small_indent
|
||||
(
|
||||
not_fixed
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
more
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
def test():
|
||||
a_small_indent
|
||||
# fmt: off
|
||||
# fix under-indented comments
|
||||
(or_the_inner_expression +
|
||||
expressions
|
||||
)
|
||||
|
||||
if True:
|
||||
pass
|
||||
# fmt: on
|
||||
|
||||
|
||||
# fmt: off
|
||||
def test():
|
||||
pass
|
||||
|
||||
# It is necessary to indent comments because the following fmt: on comment because it otherwise becomes a trailing comment
|
||||
# of the `test` function if the "proper" indentation is larger than 2 spaces.
|
||||
# fmt: on
|
||||
|
||||
disabled + formatting;
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
|
||||
|
||||
def test():
|
||||
pass
|
||||
# fmt: off
|
||||
"""A multiline strings
|
||||
that should not get formatted"""
|
||||
|
||||
"A single quoted multiline \
|
||||
string"
|
||||
|
||||
disabled + formatting
|
||||
|
||||
|
||||
# fmt: on
|
||||
|
||||
formatted
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -45,8 +45,6 @@ not_fixed
|
||||
more
|
||||
else:
|
||||
other
|
||||
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
@@ -74,8 +72,6 @@ not_fixed
|
||||
more
|
||||
else:
|
||||
other
|
||||
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
@@ -103,8 +99,6 @@ not_fixed
|
||||
more
|
||||
else:
|
||||
other
|
||||
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user