Compare commits
9 Commits
preview-bi
...
dhruv/stri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30b25f13d7 | ||
|
|
235cfb7976 | ||
|
|
91ae81b565 | ||
|
|
d46c5d8ac8 | ||
|
|
20217e9bbd | ||
|
|
72bf1c2880 | ||
|
|
c47ff658e4 | ||
|
|
c3bba54b6b | ||
|
|
fe79798c12 |
61
CHANGELOG.md
61
CHANGELOG.md
@@ -1,5 +1,66 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.2
|
||||
|
||||
Highlights include:
|
||||
|
||||
- Initial support formatting f-strings (in `--preview`).
|
||||
- Support for overriding arbitrary configuration options via the CLI through an expanded `--config`
|
||||
argument (e.g., `--config "lint.isort.combine-as-imports=false"`).
|
||||
- Significant performance improvements in Ruff's lexer, parser, and lint rules.
|
||||
|
||||
### Preview features
|
||||
|
||||
- Implement minimal f-string formatting ([#9642](https://github.com/astral-sh/ruff/pull/9642))
|
||||
- \[`pycodestyle`\] Add blank line(s) rules (`E301`, `E302`, `E303`, `E304`, `E305`, `E306`) ([#9266](https://github.com/astral-sh/ruff/pull/9266))
|
||||
- \[`refurb`\] Implement `readlines_in_for` (`FURB129`) ([#9880](https://github.com/astral-sh/ruff/pull/9880))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`ruff`\] Ensure closing parentheses for multiline sequences are always on their own line (`RUF022`, `RUF023`) ([#9793](https://github.com/astral-sh/ruff/pull/9793))
|
||||
- \[`numpy`\] Add missing deprecation violations (`NPY002`) ([#9862](https://github.com/astral-sh/ruff/pull/9862))
|
||||
- \[`flake8-bandit`\] Detect `mark_safe` usages in decorators ([#9887](https://github.com/astral-sh/ruff/pull/9887))
|
||||
- \[`ruff`\] Expand `asyncio-dangling-task` (`RUF006`) to include `new_event_loop` ([#9976](https://github.com/astral-sh/ruff/pull/9976))
|
||||
- \[`flake8-pyi`\] Ignore 'unused' private type dicts in class scopes ([#9952](https://github.com/astral-sh/ruff/pull/9952))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Docstring formatting: Preserve tab indentation when using `indent-style=tabs` ([#9915](https://github.com/astral-sh/ruff/pull/9915))
|
||||
- Disable top-level docstring formatting for notebooks ([#9957](https://github.com/astral-sh/ruff/pull/9957))
|
||||
- Stabilize quote-style's `preserve` mode ([#9922](https://github.com/astral-sh/ruff/pull/9922))
|
||||
|
||||
### CLI
|
||||
|
||||
- Allow arbitrary configuration options to be overridden via the CLI ([#9599](https://github.com/astral-sh/ruff/pull/9599))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Make `show-settings` filters directory-agnostic ([#9866](https://github.com/astral-sh/ruff/pull/9866))
|
||||
- Respect duplicates when rewriting type aliases ([#9905](https://github.com/astral-sh/ruff/pull/9905))
|
||||
- Respect tuple assignments in typing analyzer ([#9969](https://github.com/astral-sh/ruff/pull/9969))
|
||||
- Use atomic write when persisting cache ([#9981](https://github.com/astral-sh/ruff/pull/9981))
|
||||
- Use non-parenthesized range for `DebugText` ([#9953](https://github.com/astral-sh/ruff/pull/9953))
|
||||
- \[`flake8-simplify`\] Avoid false positive with `async` for loops (`SIM113`) ([#9996](https://github.com/astral-sh/ruff/pull/9996))
|
||||
- \[`flake8-trio`\] Respect `async with` in `timeout-without-await` ([#9859](https://github.com/astral-sh/ruff/pull/9859))
|
||||
- \[`perflint`\] Catch a wider range of mutations in `PERF101` ([#9955](https://github.com/astral-sh/ruff/pull/9955))
|
||||
- \[`pycodestyle`\] Fix `E30X` panics on blank lines with trailing white spaces ([#9907](https://github.com/astral-sh/ruff/pull/9907))
|
||||
- \[`pydocstyle`\] Allow using `parameters` as a subsection header (`D405`) ([#9894](https://github.com/astral-sh/ruff/pull/9894))
|
||||
- \[`pydocstyle`\] Fix blank-line docstring rules for module-level docstrings ([#9878](https://github.com/astral-sh/ruff/pull/9878))
|
||||
- \[`pylint`\] Accept 0.0 and 1.0 as common magic values (`PLR2004`) ([#9964](https://github.com/astral-sh/ruff/pull/9964))
|
||||
- \[`pylint`\] Avoid suggesting set rewrites for non-hashable types ([#9956](https://github.com/astral-sh/ruff/pull/9956))
|
||||
- \[`ruff`\] Avoid false negatives with string literals inside of method calls (`RUF027`) ([#9865](https://github.com/astral-sh/ruff/pull/9865))
|
||||
- \[`ruff`\] Fix panic on with f-string detection (`RUF027`) ([#9990](https://github.com/astral-sh/ruff/pull/9990))
|
||||
- \[`ruff`\] Ignore builtins when detecting missing f-strings ([#9849](https://github.com/astral-sh/ruff/pull/9849))
|
||||
|
||||
### Performance
|
||||
|
||||
- Use `memchr` for string lexing ([#9888](https://github.com/astral-sh/ruff/pull/9888))
|
||||
- Use `memchr` for tab-indentation detection ([#9853](https://github.com/astral-sh/ruff/pull/9853))
|
||||
- Reduce `Result<Tok, LexicalError>` size by using `Box<str>` instead of `String` ([#9885](https://github.com/astral-sh/ruff/pull/9885))
|
||||
- Reduce size of `Expr` from 80 to 64 bytes ([#9900](https://github.com/astral-sh/ruff/pull/9900))
|
||||
- Improve trailing comma rule performance ([#9867](https://github.com/astral-sh/ruff/pull/9867))
|
||||
- Remove unnecessary string cloning from the parser ([#9884](https://github.com/astral-sh/ruff/pull/9884))
|
||||
|
||||
## 0.2.1
|
||||
|
||||
This release includes support for range formatting (i.e., the ability to format specific lines
|
||||
|
||||
@@ -39,7 +39,7 @@ For small changes (e.g., bug fixes), feel free to submit a PR.
|
||||
|
||||
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
|
||||
creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
|
||||
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the
|
||||
You can also join us on [**Discord**](https://discord.com/invite/astral-sh) to discuss your idea with the
|
||||
community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
||||
in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
||||
and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -1979,7 +1979,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2140,7 +2140,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2394,7 +2394,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
[](https://github.com/astral-sh/ruff/actions)
|
||||
|
||||
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
|
||||
[**Discord**](https://discord.com/invite/astral-sh) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
|
||||
|
||||
An extremely fast Python linter and code formatter, written in Rust.
|
||||
|
||||
@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.2.1
|
||||
rev: v0.2.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -341,14 +341,14 @@ For a complete enumeration of the supported rules, see [_Rules_](https://docs.as
|
||||
Contributions are welcome and highly appreciated. To get started, check out the
|
||||
[**contributing guidelines**](https://docs.astral.sh/ruff/contributing/).
|
||||
|
||||
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5).
|
||||
You can also join us on [**Discord**](https://discord.com/invite/astral-sh).
|
||||
|
||||
## Support
|
||||
|
||||
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues),
|
||||
or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new).
|
||||
|
||||
You can also ask for help on [**Discord**](https://discord.gg/c9MhzV8aU5).
|
||||
You can also ask for help on [**Discord**](https://discord.com/invite/astral-sh).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -193,3 +193,11 @@ def func():
|
||||
for y in range(5):
|
||||
g(x, idx)
|
||||
idx += 1
|
||||
|
||||
async def func():
|
||||
# OK (for loop is async)
|
||||
idx = 0
|
||||
|
||||
async for x in async_gen():
|
||||
g(x, idx)
|
||||
idx += 1
|
||||
|
||||
@@ -68,3 +68,7 @@ def method_calls():
|
||||
first = "Wendy"
|
||||
last = "Appleseed"
|
||||
value.method("{first} {last}") # RUF027
|
||||
|
||||
def format_specifiers():
|
||||
a = 4
|
||||
b = "{a:b} {a:^5}"
|
||||
|
||||
2
crates/ruff_linter/resources/test/fixtures/ruff/RUF027_2.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/ruff/RUF027_2.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# 测试eval函数,eval()函数用来执行一个字符串表达式,并返t表达式的值。另外,可以讲字符串转换成列表或元组或字典
|
||||
a = "{1: 'a', 2: 'b'}"
|
||||
@@ -2,10 +2,16 @@ use ruff_python_ast::StringLike;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_bandit, flake8_pyi};
|
||||
use crate::rules::{flake8_bandit, flake8_pyi, ruff};
|
||||
|
||||
/// Run lint rules over a [`StringLike`] syntax nodes.
|
||||
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
|
||||
if checker.any_enabled(&[
|
||||
Rule::AmbiguousUnicodeCharacterString,
|
||||
Rule::AmbiguousUnicodeCharacterDocstring,
|
||||
]) {
|
||||
ruff::rules::ambiguous_unicode_character_string(checker, string_like);
|
||||
}
|
||||
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
|
||||
flake8_bandit::rules::hardcoded_bind_all_interfaces(checker, string_like);
|
||||
}
|
||||
|
||||
@@ -6,17 +6,14 @@ use ruff_notebook::CellOffsets;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::directives::TodoComment;
|
||||
use crate::lex::docstring_detection::StateMachine;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pycodestyle::rules::BlankLinesChecker;
|
||||
use crate::rules::ruff::rules::Context;
|
||||
use crate::rules::{
|
||||
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
|
||||
flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
|
||||
@@ -66,31 +63,15 @@ pub(crate) fn check_tokens(
|
||||
pylint::rules::empty_comments(&mut diagnostics, indexer, locator);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::AmbiguousUnicodeCharacterString,
|
||||
Rule::AmbiguousUnicodeCharacterDocstring,
|
||||
Rule::AmbiguousUnicodeCharacterComment,
|
||||
]) {
|
||||
let mut state_machine = StateMachine::default();
|
||||
for &(ref tok, range) in tokens.iter().flatten() {
|
||||
let is_docstring = state_machine.consume(tok);
|
||||
let context = match tok {
|
||||
Tok::String { .. } => {
|
||||
if is_docstring {
|
||||
Context::Docstring
|
||||
} else {
|
||||
Context::String
|
||||
}
|
||||
}
|
||||
Tok::FStringMiddle { .. } => Context::String,
|
||||
Tok::Comment(_) => Context::Comment,
|
||||
_ => continue,
|
||||
};
|
||||
ruff::rules::ambiguous_unicode_character(
|
||||
if settings
|
||||
.rules
|
||||
.enabled(Rule::AmbiguousUnicodeCharacterComment)
|
||||
{
|
||||
for range in indexer.comment_ranges() {
|
||||
ruff::rules::ambiguous_unicode_character_comment(
|
||||
&mut diagnostics,
|
||||
locator,
|
||||
range,
|
||||
context,
|
||||
*range,
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,8 +256,6 @@ impl Rule {
|
||||
| Rule::MixedSpacesAndTabs
|
||||
| Rule::TrailingWhitespace => LintSource::PhysicalLines,
|
||||
Rule::AmbiguousUnicodeCharacterComment
|
||||
| Rule::AmbiguousUnicodeCharacterDocstring
|
||||
| Rule::AmbiguousUnicodeCharacterString
|
||||
| Rule::AvoidableEscapedQuote
|
||||
| Rule::BadQuotesDocstring
|
||||
| Rule::BadQuotesInlineString
|
||||
|
||||
@@ -49,6 +49,11 @@ impl Violation for EnumerateForLoop {
|
||||
|
||||
/// SIM113
|
||||
pub(crate) fn enumerate_for_loop(checker: &mut Checker, for_stmt: &ast::StmtFor) {
|
||||
// If the loop is async, abort.
|
||||
if for_stmt.is_async {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the loop contains a `continue`, abort.
|
||||
let mut visitor = LoopControlFlowVisitor::default();
|
||||
visitor.visit_body(&for_stmt.body);
|
||||
|
||||
@@ -87,12 +87,17 @@ impl Violation for IndentWithSpaces {
|
||||
/// """
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
|
||||
///
|
||||
/// [PEP 257]: https://peps.python.org/pep-0257/
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct UnderIndentation;
|
||||
|
||||
|
||||
@@ -27,10 +27,16 @@ use crate::docstrings::Docstring;
|
||||
/// """Return the pathname of the KOS root directory."""
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent quotes, making the rule redundant.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct TripleSingleQuotes {
|
||||
expected_quote: Quote,
|
||||
|
||||
@@ -48,6 +48,7 @@ mod tests {
|
||||
#[test_case(Rule::DefaultFactoryKwarg, Path::new("RUF026.py"))]
|
||||
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_0.py"))]
|
||||
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_1.py"))]
|
||||
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_2.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -4,9 +4,11 @@ use bitflags::bitflags;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::StringLike;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::ruff::rules::confusables::confusable;
|
||||
use crate::rules::ruff::rules::Context;
|
||||
@@ -171,16 +173,59 @@ impl Violation for AmbiguousUnicodeCharacterComment {
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF001, RUF002, RUF003
|
||||
pub(crate) fn ambiguous_unicode_character(
|
||||
/// RUF003
|
||||
pub(crate) fn ambiguous_unicode_character_comment(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
locator: &Locator,
|
||||
range: TextRange,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
let text = locator.slice(range);
|
||||
ambiguous_unicode_character(diagnostics, text, range, Context::Comment, settings);
|
||||
}
|
||||
|
||||
/// RUF001, RUF002
|
||||
pub(crate) fn ambiguous_unicode_character_string(checker: &mut Checker, string_like: StringLike) {
|
||||
let context = if checker.semantic().in_docstring() {
|
||||
Context::Docstring
|
||||
} else {
|
||||
Context::String
|
||||
};
|
||||
|
||||
match string_like {
|
||||
StringLike::StringLiteral(string_literal) => {
|
||||
for string in &string_literal.value {
|
||||
let text = checker.locator().slice(string);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
string.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
StringLike::FStringLiteral(f_string_literal) => {
|
||||
let text = checker.locator().slice(f_string_literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
f_string_literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
StringLike::BytesLiteral(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn ambiguous_unicode_character(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
text: &str,
|
||||
range: TextRange,
|
||||
context: Context,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
let text = locator.slice(range);
|
||||
|
||||
// Most of the time, we don't need to check for ambiguous unicode characters at all.
|
||||
if text.is_ascii() {
|
||||
return;
|
||||
|
||||
@@ -88,8 +88,10 @@ fn should_be_fstring(
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(ast::Expr::FString(ast::ExprFString { value, .. })) =
|
||||
parse_expression(&format!("f{}", locator.slice(literal.range())))
|
||||
let fstring_expr = format!("f{}", locator.slice(literal));
|
||||
|
||||
// Note: Range offsets for `value` are based on `fstring_expr`
|
||||
let Ok(ast::Expr::FString(ast::ExprFString { value, .. })) = parse_expression(&fstring_expr)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
@@ -159,7 +161,7 @@ fn should_be_fstring(
|
||||
has_name = true;
|
||||
}
|
||||
if let Some(spec) = &element.format_spec {
|
||||
let spec = locator.slice(spec.range());
|
||||
let spec = &fstring_expr[spec.range()];
|
||||
if FormatSpec::parse(spec).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -285,6 +285,8 @@ RUF027_0.py:70:18: RUF027 [*] Possible f-string without an `f` prefix
|
||||
69 | last = "Appleseed"
|
||||
70 | value.method("{first} {last}") # RUF027
|
||||
| ^^^^^^^^^^^^^^^^ RUF027
|
||||
71 |
|
||||
72 | def format_specifiers():
|
||||
|
|
||||
= help: Add `f` prefix
|
||||
|
||||
@@ -294,5 +296,24 @@ RUF027_0.py:70:18: RUF027 [*] Possible f-string without an `f` prefix
|
||||
69 69 | last = "Appleseed"
|
||||
70 |- value.method("{first} {last}") # RUF027
|
||||
70 |+ value.method(f"{first} {last}") # RUF027
|
||||
71 71 |
|
||||
72 72 | def format_specifiers():
|
||||
73 73 | a = 4
|
||||
|
||||
RUF027_0.py:74:9: RUF027 [*] Possible f-string without an `f` prefix
|
||||
|
|
||||
72 | def format_specifiers():
|
||||
73 | a = 4
|
||||
74 | b = "{a:b} {a:^5}"
|
||||
| ^^^^^^^^^^^^^^ RUF027
|
||||
|
|
||||
= help: Add `f` prefix
|
||||
|
||||
ℹ Unsafe fix
|
||||
71 71 |
|
||||
72 72 | def format_specifiers():
|
||||
73 73 | a = 4
|
||||
74 |- b = "{a:b} {a:^5}"
|
||||
74 |+ b = f"{a:b} {a:^5}"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
|
||||
@@ -24,8 +24,8 @@ pub enum ExpressionRef<'a> {
|
||||
Compare(&'a ast::ExprCompare),
|
||||
Call(&'a ast::ExprCall),
|
||||
FString(&'a ast::ExprFString),
|
||||
StringLiteral(&'a ast::ExprStringLiteral),
|
||||
BytesLiteral(&'a ast::ExprBytesLiteral),
|
||||
StringLiteral(&'a ast::ExprString),
|
||||
BytesLiteral(&'a ast::ExprBytes),
|
||||
NumberLiteral(&'a ast::ExprNumberLiteral),
|
||||
BooleanLiteral(&'a ast::ExprBooleanLiteral),
|
||||
NoneLiteral(&'a ast::ExprNoneLiteral),
|
||||
@@ -67,8 +67,8 @@ impl<'a> From<&'a Expr> for ExpressionRef<'a> {
|
||||
Expr::Compare(value) => ExpressionRef::Compare(value),
|
||||
Expr::Call(value) => ExpressionRef::Call(value),
|
||||
Expr::FString(value) => ExpressionRef::FString(value),
|
||||
Expr::StringLiteral(value) => ExpressionRef::StringLiteral(value),
|
||||
Expr::BytesLiteral(value) => ExpressionRef::BytesLiteral(value),
|
||||
Expr::String(value) => ExpressionRef::StringLiteral(value),
|
||||
Expr::Bytes(value) => ExpressionRef::BytesLiteral(value),
|
||||
Expr::NumberLiteral(value) => ExpressionRef::NumberLiteral(value),
|
||||
Expr::BooleanLiteral(value) => ExpressionRef::BooleanLiteral(value),
|
||||
Expr::NoneLiteral(value) => ExpressionRef::NoneLiteral(value),
|
||||
@@ -175,13 +175,13 @@ impl<'a> From<&'a ast::ExprFString> for ExpressionRef<'a> {
|
||||
Self::FString(value)
|
||||
}
|
||||
}
|
||||
impl<'a> From<&'a ast::ExprStringLiteral> for ExpressionRef<'a> {
|
||||
fn from(value: &'a ast::ExprStringLiteral) -> Self {
|
||||
impl<'a> From<&'a ast::ExprString> for ExpressionRef<'a> {
|
||||
fn from(value: &'a ast::ExprString) -> Self {
|
||||
Self::StringLiteral(value)
|
||||
}
|
||||
}
|
||||
impl<'a> From<&'a ast::ExprBytesLiteral> for ExpressionRef<'a> {
|
||||
fn from(value: &'a ast::ExprBytesLiteral) -> Self {
|
||||
impl<'a> From<&'a ast::ExprBytes> for ExpressionRef<'a> {
|
||||
fn from(value: &'a ast::ExprBytes) -> Self {
|
||||
Self::BytesLiteral(value)
|
||||
}
|
||||
}
|
||||
@@ -332,8 +332,8 @@ impl Ranged for ExpressionRef<'_> {
|
||||
/// reference instead of an owned value.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, is_macro::Is)]
|
||||
pub enum LiteralExpressionRef<'a> {
|
||||
StringLiteral(&'a ast::ExprStringLiteral),
|
||||
BytesLiteral(&'a ast::ExprBytesLiteral),
|
||||
StringLiteral(&'a ast::ExprString),
|
||||
BytesLiteral(&'a ast::ExprBytes),
|
||||
NumberLiteral(&'a ast::ExprNumberLiteral),
|
||||
BooleanLiteral(&'a ast::ExprBooleanLiteral),
|
||||
NoneLiteral(&'a ast::ExprNoneLiteral),
|
||||
@@ -399,19 +399,19 @@ impl LiteralExpressionRef<'_> {
|
||||
/// f-strings.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum StringLike<'a> {
|
||||
StringLiteral(&'a ast::ExprStringLiteral),
|
||||
BytesLiteral(&'a ast::ExprBytesLiteral),
|
||||
StringLiteral(&'a ast::ExprString),
|
||||
BytesLiteral(&'a ast::ExprBytes),
|
||||
FStringLiteral(&'a ast::FStringLiteralElement),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprStringLiteral> for StringLike<'a> {
|
||||
fn from(value: &'a ast::ExprStringLiteral) -> Self {
|
||||
impl<'a> From<&'a ast::ExprString> for StringLike<'a> {
|
||||
fn from(value: &'a ast::ExprString) -> Self {
|
||||
StringLike::StringLiteral(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprBytesLiteral> for StringLike<'a> {
|
||||
fn from(value: &'a ast::ExprBytesLiteral) -> Self {
|
||||
impl<'a> From<&'a ast::ExprBytes> for StringLike<'a> {
|
||||
fn from(value: &'a ast::ExprBytes) -> Self {
|
||||
StringLike::BytesLiteral(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ pub enum AnyNode {
|
||||
ExprCompare(ast::ExprCompare),
|
||||
ExprCall(ast::ExprCall),
|
||||
ExprFString(ast::ExprFString),
|
||||
ExprStringLiteral(ast::ExprStringLiteral),
|
||||
ExprBytesLiteral(ast::ExprBytesLiteral),
|
||||
ExprStringLiteral(ast::ExprString),
|
||||
ExprBytesLiteral(ast::ExprBytes),
|
||||
ExprNumberLiteral(ast::ExprNumberLiteral),
|
||||
ExprBooleanLiteral(ast::ExprBooleanLiteral),
|
||||
ExprNoneLiteral(ast::ExprNoneLiteral),
|
||||
@@ -238,8 +238,8 @@ impl AnyNode {
|
||||
AnyNode::ExprCompare(node) => Some(Expr::Compare(node)),
|
||||
AnyNode::ExprCall(node) => Some(Expr::Call(node)),
|
||||
AnyNode::ExprFString(node) => Some(Expr::FString(node)),
|
||||
AnyNode::ExprStringLiteral(node) => Some(Expr::StringLiteral(node)),
|
||||
AnyNode::ExprBytesLiteral(node) => Some(Expr::BytesLiteral(node)),
|
||||
AnyNode::ExprStringLiteral(node) => Some(Expr::String(node)),
|
||||
AnyNode::ExprBytesLiteral(node) => Some(Expr::Bytes(node)),
|
||||
AnyNode::ExprNumberLiteral(node) => Some(Expr::NumberLiteral(node)),
|
||||
AnyNode::ExprBooleanLiteral(node) => Some(Expr::BooleanLiteral(node)),
|
||||
AnyNode::ExprNoneLiteral(node) => Some(Expr::NoneLiteral(node)),
|
||||
@@ -2788,7 +2788,7 @@ impl AstNode for ast::ExprFString {
|
||||
|
||||
for f_string_part in value {
|
||||
match f_string_part {
|
||||
ast::FStringPart::Literal(string_literal) => {
|
||||
ast::FStringPart::String(string_literal) => {
|
||||
visitor.visit_string_literal(string_literal);
|
||||
}
|
||||
ast::FStringPart::FString(f_string) => {
|
||||
@@ -2798,7 +2798,7 @@ impl AstNode for ast::ExprFString {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl AstNode for ast::ExprStringLiteral {
|
||||
impl AstNode for ast::ExprString {
|
||||
fn cast(kind: AnyNode) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -2830,14 +2830,14 @@ impl AstNode for ast::ExprStringLiteral {
|
||||
where
|
||||
V: PreorderVisitor<'a> + ?Sized,
|
||||
{
|
||||
let ast::ExprStringLiteral { value, range: _ } = self;
|
||||
let ast::ExprString { value, range: _ } = self;
|
||||
|
||||
for string_literal in value {
|
||||
visitor.visit_string_literal(string_literal);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl AstNode for ast::ExprBytesLiteral {
|
||||
impl AstNode for ast::ExprBytes {
|
||||
fn cast(kind: AnyNode) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -2869,7 +2869,7 @@ impl AstNode for ast::ExprBytesLiteral {
|
||||
where
|
||||
V: PreorderVisitor<'a> + ?Sized,
|
||||
{
|
||||
let ast::ExprBytesLiteral { value, range: _ } = self;
|
||||
let ast::ExprBytes { value, range: _ } = self;
|
||||
|
||||
for bytes_literal in value {
|
||||
visitor.visit_bytes_literal(bytes_literal);
|
||||
@@ -4557,8 +4557,8 @@ impl From<Expr> for AnyNode {
|
||||
Expr::Compare(node) => AnyNode::ExprCompare(node),
|
||||
Expr::Call(node) => AnyNode::ExprCall(node),
|
||||
Expr::FString(node) => AnyNode::ExprFString(node),
|
||||
Expr::StringLiteral(node) => AnyNode::ExprStringLiteral(node),
|
||||
Expr::BytesLiteral(node) => AnyNode::ExprBytesLiteral(node),
|
||||
Expr::String(node) => AnyNode::ExprStringLiteral(node),
|
||||
Expr::Bytes(node) => AnyNode::ExprBytesLiteral(node),
|
||||
Expr::NumberLiteral(node) => AnyNode::ExprNumberLiteral(node),
|
||||
Expr::BooleanLiteral(node) => AnyNode::ExprBooleanLiteral(node),
|
||||
Expr::NoneLiteral(node) => AnyNode::ExprNoneLiteral(node),
|
||||
@@ -4910,14 +4910,14 @@ impl From<ast::ExprFString> for AnyNode {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ast::ExprStringLiteral> for AnyNode {
|
||||
fn from(node: ast::ExprStringLiteral) -> Self {
|
||||
impl From<ast::ExprString> for AnyNode {
|
||||
fn from(node: ast::ExprString) -> Self {
|
||||
AnyNode::ExprStringLiteral(node)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ast::ExprBytesLiteral> for AnyNode {
|
||||
fn from(node: ast::ExprBytesLiteral) -> Self {
|
||||
impl From<ast::ExprBytes> for AnyNode {
|
||||
fn from(node: ast::ExprBytes) -> Self {
|
||||
AnyNode::ExprBytesLiteral(node)
|
||||
}
|
||||
}
|
||||
@@ -5299,8 +5299,8 @@ pub enum AnyNodeRef<'a> {
|
||||
FStringLiteralElement(&'a ast::FStringLiteralElement),
|
||||
FStringFormatSpec(&'a ast::FStringFormatSpec),
|
||||
ExprFString(&'a ast::ExprFString),
|
||||
ExprStringLiteral(&'a ast::ExprStringLiteral),
|
||||
ExprBytesLiteral(&'a ast::ExprBytesLiteral),
|
||||
ExprStringLiteral(&'a ast::ExprString),
|
||||
ExprBytesLiteral(&'a ast::ExprBytes),
|
||||
ExprNumberLiteral(&'a ast::ExprNumberLiteral),
|
||||
ExprBooleanLiteral(&'a ast::ExprBooleanLiteral),
|
||||
ExprNoneLiteral(&'a ast::ExprNoneLiteral),
|
||||
@@ -6492,14 +6492,14 @@ impl<'a> From<&'a ast::ExprFString> for AnyNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprStringLiteral> for AnyNodeRef<'a> {
|
||||
fn from(node: &'a ast::ExprStringLiteral) -> Self {
|
||||
impl<'a> From<&'a ast::ExprString> for AnyNodeRef<'a> {
|
||||
fn from(node: &'a ast::ExprString) -> Self {
|
||||
AnyNodeRef::ExprStringLiteral(node)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprBytesLiteral> for AnyNodeRef<'a> {
|
||||
fn from(node: &'a ast::ExprBytesLiteral) -> Self {
|
||||
impl<'a> From<&'a ast::ExprBytes> for AnyNodeRef<'a> {
|
||||
fn from(node: &'a ast::ExprBytes) -> Self {
|
||||
AnyNodeRef::ExprBytesLiteral(node)
|
||||
}
|
||||
}
|
||||
@@ -6742,8 +6742,8 @@ impl<'a> From<&'a Expr> for AnyNodeRef<'a> {
|
||||
Expr::Compare(node) => AnyNodeRef::ExprCompare(node),
|
||||
Expr::Call(node) => AnyNodeRef::ExprCall(node),
|
||||
Expr::FString(node) => AnyNodeRef::ExprFString(node),
|
||||
Expr::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node),
|
||||
Expr::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node),
|
||||
Expr::String(node) => AnyNodeRef::ExprStringLiteral(node),
|
||||
Expr::Bytes(node) => AnyNodeRef::ExprBytesLiteral(node),
|
||||
Expr::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node),
|
||||
Expr::BooleanLiteral(node) => AnyNodeRef::ExprBooleanLiteral(node),
|
||||
Expr::NoneLiteral(node) => AnyNodeRef::ExprNoneLiteral(node),
|
||||
|
||||
@@ -594,9 +594,9 @@ pub enum Expr {
|
||||
#[is(name = "f_string_expr")]
|
||||
FString(ExprFString),
|
||||
#[is(name = "string_literal_expr")]
|
||||
StringLiteral(ExprStringLiteral),
|
||||
String(ExprString),
|
||||
#[is(name = "bytes_literal_expr")]
|
||||
BytesLiteral(ExprBytesLiteral),
|
||||
Bytes(ExprBytes),
|
||||
#[is(name = "number_literal_expr")]
|
||||
NumberLiteral(ExprNumberLiteral),
|
||||
#[is(name = "boolean_literal_expr")]
|
||||
@@ -633,8 +633,8 @@ impl Expr {
|
||||
pub fn is_literal_expr(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Expr::StringLiteral(_)
|
||||
| Expr::BytesLiteral(_)
|
||||
Expr::String(_)
|
||||
| Expr::Bytes(_)
|
||||
| Expr::NumberLiteral(_)
|
||||
| Expr::BooleanLiteral(_)
|
||||
| Expr::NoneLiteral(_)
|
||||
@@ -645,8 +645,8 @@ impl Expr {
|
||||
/// Returns [`LiteralExpressionRef`] if the expression is a literal expression.
|
||||
pub fn as_literal_expr(&self) -> Option<LiteralExpressionRef<'_>> {
|
||||
match self {
|
||||
Expr::StringLiteral(expr) => Some(LiteralExpressionRef::StringLiteral(expr)),
|
||||
Expr::BytesLiteral(expr) => Some(LiteralExpressionRef::BytesLiteral(expr)),
|
||||
Expr::String(expr) => Some(LiteralExpressionRef::StringLiteral(expr)),
|
||||
Expr::Bytes(expr) => Some(LiteralExpressionRef::BytesLiteral(expr)),
|
||||
Expr::NumberLiteral(expr) => Some(LiteralExpressionRef::NumberLiteral(expr)),
|
||||
Expr::BooleanLiteral(expr) => Some(LiteralExpressionRef::BooleanLiteral(expr)),
|
||||
Expr::NoneLiteral(expr) => Some(LiteralExpressionRef::NoneLiteral(expr)),
|
||||
@@ -1011,7 +1011,7 @@ pub struct DebugText {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ExprFString {
|
||||
pub range: TextRange,
|
||||
pub value: FStringValue,
|
||||
pub value: FStringExprValue,
|
||||
}
|
||||
|
||||
impl From<ExprFString> for Expr {
|
||||
@@ -1022,15 +1022,15 @@ impl From<ExprFString> for Expr {
|
||||
|
||||
/// The value representing an [`ExprFString`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FStringValue {
|
||||
inner: FStringValueInner,
|
||||
pub struct FStringExprValue {
|
||||
inner: FStringExprValueInner,
|
||||
}
|
||||
|
||||
impl FStringValue {
|
||||
impl FStringExprValue {
|
||||
/// Creates a new f-string with the given value.
|
||||
pub fn single(value: FString) -> Self {
|
||||
Self {
|
||||
inner: FStringValueInner::Single(FStringPart::FString(value)),
|
||||
inner: FStringExprValueInner::Single(FStringPart::FString(value)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,32 +1039,32 @@ impl FStringValue {
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `values` is less than 2. Use [`FStringValue::single`] instead.
|
||||
/// Panics if `values` is less than 2. Use [`FStringExprValue::single`] instead.
|
||||
pub fn concatenated(values: Vec<FStringPart>) -> Self {
|
||||
assert!(values.len() > 1);
|
||||
Self {
|
||||
inner: FStringValueInner::Concatenated(values),
|
||||
inner: FStringExprValueInner::Concatenated(values),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the f-string is implicitly concatenated, `false` otherwise.
|
||||
pub fn is_implicit_concatenated(&self) -> bool {
|
||||
matches!(self.inner, FStringValueInner::Concatenated(_))
|
||||
matches!(self.inner, FStringExprValueInner::Concatenated(_))
|
||||
}
|
||||
|
||||
/// Returns a slice of all the [`FStringPart`]s contained in this value.
|
||||
pub fn as_slice(&self) -> &[FStringPart] {
|
||||
match &self.inner {
|
||||
FStringValueInner::Single(part) => std::slice::from_ref(part),
|
||||
FStringValueInner::Concatenated(parts) => parts,
|
||||
FStringExprValueInner::Single(part) => std::slice::from_ref(part),
|
||||
FStringExprValueInner::Concatenated(parts) => parts,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of all the [`FStringPart`]s contained in this value.
|
||||
fn as_mut_slice(&mut self) -> &mut [FStringPart] {
|
||||
match &mut self.inner {
|
||||
FStringValueInner::Single(part) => std::slice::from_mut(part),
|
||||
FStringValueInner::Concatenated(parts) => parts,
|
||||
FStringExprValueInner::Single(part) => std::slice::from_mut(part),
|
||||
FStringExprValueInner::Concatenated(parts) => parts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1088,8 +1088,8 @@ impl FStringValue {
|
||||
/// ```
|
||||
///
|
||||
/// Here, the string literal parts returned would be `"foo"` and `"baz"`.
|
||||
pub fn literals(&self) -> impl Iterator<Item = &StringLiteral> {
|
||||
self.iter().filter_map(|part| part.as_literal())
|
||||
pub fn strings(&self) -> impl Iterator<Item = &StringLiteral> {
|
||||
self.iter().filter_map(|part| part.as_string())
|
||||
}
|
||||
|
||||
/// Returns an iterator over the [`FString`] parts contained in this value.
|
||||
@@ -1121,7 +1121,7 @@ impl FStringValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a FStringValue {
|
||||
impl<'a> IntoIterator for &'a FStringExprValue {
|
||||
type Item = &'a FStringPart;
|
||||
type IntoIter = Iter<'a, FStringPart>;
|
||||
|
||||
@@ -1140,7 +1140,7 @@ impl<'a> IntoIterator for &'a mut FStringValue {
|
||||
|
||||
/// An internal representation of [`FStringValue`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum FStringValueInner {
|
||||
enum FStringExprValueInner {
|
||||
/// A single f-string i.e., `f"foo"`.
|
||||
///
|
||||
/// This is always going to be `FStringPart::FString` variant which is
|
||||
@@ -1154,14 +1154,14 @@ enum FStringValueInner {
|
||||
/// An f-string part which is either a string literal or an f-string.
|
||||
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
|
||||
pub enum FStringPart {
|
||||
Literal(StringLiteral),
|
||||
String(StringLiteral),
|
||||
FString(FString),
|
||||
}
|
||||
|
||||
impl Ranged for FStringPart {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
FStringPart::Literal(string_literal) => string_literal.range(),
|
||||
FStringPart::String(string_literal) => string_literal.range(),
|
||||
FStringPart::FString(f_string) => f_string.range(),
|
||||
}
|
||||
}
|
||||
@@ -1184,7 +1184,7 @@ impl From<FString> for Expr {
|
||||
fn from(payload: FString) -> Self {
|
||||
ExprFString {
|
||||
range: payload.range,
|
||||
value: FStringValue::single(payload),
|
||||
value: FStringExprValue::single(payload),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -1208,34 +1208,34 @@ impl Ranged for FStringElement {
|
||||
/// An AST node that represents either a single string literal or an implicitly
|
||||
/// concatenated string literals.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ExprStringLiteral {
|
||||
pub struct ExprString {
|
||||
pub range: TextRange,
|
||||
pub value: StringLiteralValue,
|
||||
pub value: StringExprValue,
|
||||
}
|
||||
|
||||
impl From<ExprStringLiteral> for Expr {
|
||||
fn from(payload: ExprStringLiteral) -> Self {
|
||||
Expr::StringLiteral(payload)
|
||||
impl From<ExprString> for Expr {
|
||||
fn from(payload: ExprString) -> Self {
|
||||
Expr::String(payload)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for ExprStringLiteral {
|
||||
impl Ranged for ExprString {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
/// The value representing a [`ExprStringLiteral`].
|
||||
/// The value representing a [`ExprString`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct StringLiteralValue {
|
||||
inner: StringLiteralValueInner,
|
||||
pub struct StringExprValue {
|
||||
inner: StringExprValueInner,
|
||||
}
|
||||
|
||||
impl StringLiteralValue {
|
||||
impl StringExprValue {
|
||||
/// Creates a new single string literal with the given value.
|
||||
pub fn single(string: StringLiteral) -> Self {
|
||||
Self {
|
||||
inner: StringLiteralValueInner::Single(string),
|
||||
inner: StringExprValueInner::Single(string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1249,7 +1249,7 @@ impl StringLiteralValue {
|
||||
pub fn concatenated(strings: Vec<StringLiteral>) -> Self {
|
||||
assert!(strings.len() > 1);
|
||||
Self {
|
||||
inner: StringLiteralValueInner::Concatenated(ConcatenatedStringLiteral {
|
||||
inner: StringExprValueInner::Concatenated(ConcatenatedStringLiteral {
|
||||
strings,
|
||||
value: OnceCell::new(),
|
||||
}),
|
||||
@@ -1258,7 +1258,7 @@ impl StringLiteralValue {
|
||||
|
||||
/// Returns `true` if the string literal is implicitly concatenated.
|
||||
pub const fn is_implicit_concatenated(&self) -> bool {
|
||||
matches!(self.inner, StringLiteralValueInner::Concatenated(_))
|
||||
matches!(self.inner, StringExprValueInner::Concatenated(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if the string literal is a unicode string.
|
||||
@@ -1272,16 +1272,16 @@ impl StringLiteralValue {
|
||||
/// Returns a slice of all the [`StringLiteral`] parts contained in this value.
|
||||
pub fn as_slice(&self) -> &[StringLiteral] {
|
||||
match &self.inner {
|
||||
StringLiteralValueInner::Single(value) => std::slice::from_ref(value),
|
||||
StringLiteralValueInner::Concatenated(value) => value.strings.as_slice(),
|
||||
StringExprValueInner::Single(value) => std::slice::from_ref(value),
|
||||
StringExprValueInner::Concatenated(value) => value.strings.as_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of all the [`StringLiteral`] parts contained in this value.
|
||||
fn as_mut_slice(&mut self) -> &mut [StringLiteral] {
|
||||
match &mut self.inner {
|
||||
StringLiteralValueInner::Single(value) => std::slice::from_mut(value),
|
||||
StringLiteralValueInner::Concatenated(value) => value.strings.as_mut_slice(),
|
||||
StringExprValueInner::Single(value) => std::slice::from_mut(value),
|
||||
StringExprValueInner::Concatenated(value) => value.strings.as_mut_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1318,13 +1318,13 @@ impl StringLiteralValue {
|
||||
/// string value is implicitly concatenated.
|
||||
pub fn to_str(&self) -> &str {
|
||||
match &self.inner {
|
||||
StringLiteralValueInner::Single(value) => value.as_str(),
|
||||
StringLiteralValueInner::Concatenated(value) => value.to_str(),
|
||||
StringExprValueInner::Single(value) => value.as_str(),
|
||||
StringExprValueInner::Concatenated(value) => value.to_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a StringLiteralValue {
|
||||
impl<'a> IntoIterator for &'a StringExprValue {
|
||||
type Item = &'a StringLiteral;
|
||||
type IntoIter = Iter<'a, StringLiteral>;
|
||||
|
||||
@@ -1333,15 +1333,16 @@ impl<'a> IntoIterator for &'a StringLiteralValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a mut StringLiteralValue {
|
||||
impl<'a> IntoIterator for &'a mut StringExprValue {
|
||||
type Item = &'a mut StringLiteral;
|
||||
type IntoIter = IterMut<'a, StringLiteral>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for StringLiteralValue {
|
||||
impl PartialEq<str> for StringExprValue {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
if self.len() != other.len() {
|
||||
return false;
|
||||
@@ -1351,13 +1352,13 @@ impl PartialEq<str> for StringLiteralValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<String> for StringLiteralValue {
|
||||
impl PartialEq<String> for StringExprValue {
|
||||
fn eq(&self, other: &String) -> bool {
|
||||
self == other.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StringLiteralValue {
|
||||
impl fmt::Display for StringExprValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.to_str())
|
||||
}
|
||||
@@ -1365,7 +1366,7 @@ impl fmt::Display for StringLiteralValue {
|
||||
|
||||
/// An internal representation of [`StringLiteralValue`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum StringLiteralValueInner {
|
||||
enum StringExprValueInner {
|
||||
/// A single string literal i.e., `"foo"`.
|
||||
Single(StringLiteral),
|
||||
|
||||
@@ -1373,7 +1374,7 @@ enum StringLiteralValueInner {
|
||||
Concatenated(ConcatenatedStringLiteral),
|
||||
}
|
||||
|
||||
impl Default for StringLiteralValueInner {
|
||||
impl Default for StringExprValueInner {
|
||||
fn default() -> Self {
|
||||
Self::Single(StringLiteral::default())
|
||||
}
|
||||
@@ -1411,9 +1412,9 @@ impl StringLiteral {
|
||||
|
||||
impl From<StringLiteral> for Expr {
|
||||
fn from(payload: StringLiteral) -> Self {
|
||||
ExprStringLiteral {
|
||||
ExprString {
|
||||
range: payload.range,
|
||||
value: StringLiteralValue::single(payload),
|
||||
value: StringExprValue::single(payload),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -1464,18 +1465,18 @@ impl Debug for ConcatenatedStringLiteral {
|
||||
/// An AST node that represents either a single bytes literal or an implicitly
|
||||
/// concatenated bytes literals.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ExprBytesLiteral {
|
||||
pub struct ExprBytes {
|
||||
pub range: TextRange,
|
||||
pub value: BytesLiteralValue,
|
||||
pub value: BytesExprValue,
|
||||
}
|
||||
|
||||
impl From<ExprBytesLiteral> for Expr {
|
||||
fn from(payload: ExprBytesLiteral) -> Self {
|
||||
Expr::BytesLiteral(payload)
|
||||
impl From<ExprBytes> for Expr {
|
||||
fn from(payload: ExprBytes) -> Self {
|
||||
Expr::Bytes(payload)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for ExprBytesLiteral {
|
||||
impl Ranged for ExprBytes {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
@@ -1483,15 +1484,15 @@ impl Ranged for ExprBytesLiteral {
|
||||
|
||||
/// The value representing a [`ExprBytesLiteral`].
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct BytesLiteralValue {
|
||||
inner: BytesLiteralValueInner,
|
||||
pub struct BytesExprValue {
|
||||
inner: BytesExprValueInner,
|
||||
}
|
||||
|
||||
impl BytesLiteralValue {
|
||||
impl BytesExprValue {
|
||||
/// Creates a new single bytes literal with the given value.
|
||||
pub fn single(value: BytesLiteral) -> Self {
|
||||
Self {
|
||||
inner: BytesLiteralValueInner::Single(value),
|
||||
inner: BytesExprValueInner::Single(value),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1505,28 +1506,28 @@ impl BytesLiteralValue {
|
||||
pub fn concatenated(values: Vec<BytesLiteral>) -> Self {
|
||||
assert!(values.len() > 1);
|
||||
Self {
|
||||
inner: BytesLiteralValueInner::Concatenated(values),
|
||||
inner: BytesExprValueInner::Concatenated(values),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the bytes literal is implicitly concatenated.
|
||||
pub const fn is_implicit_concatenated(&self) -> bool {
|
||||
matches!(self.inner, BytesLiteralValueInner::Concatenated(_))
|
||||
matches!(self.inner, BytesExprValueInner::Concatenated(_))
|
||||
}
|
||||
|
||||
/// Returns a slice of all the [`BytesLiteral`] parts contained in this value.
|
||||
pub fn as_slice(&self) -> &[BytesLiteral] {
|
||||
match &self.inner {
|
||||
BytesLiteralValueInner::Single(value) => std::slice::from_ref(value),
|
||||
BytesLiteralValueInner::Concatenated(value) => value.as_slice(),
|
||||
BytesExprValueInner::Single(value) => std::slice::from_ref(value),
|
||||
BytesExprValueInner::Concatenated(value) => value.as_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of all the [`BytesLiteral`] parts contained in this value.
|
||||
fn as_mut_slice(&mut self) -> &mut [BytesLiteral] {
|
||||
match &mut self.inner {
|
||||
BytesLiteralValueInner::Single(value) => std::slice::from_mut(value),
|
||||
BytesLiteralValueInner::Concatenated(value) => value.as_mut_slice(),
|
||||
BytesExprValueInner::Single(value) => std::slice::from_mut(value),
|
||||
BytesExprValueInner::Concatenated(value) => value.as_mut_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1557,7 +1558,7 @@ impl BytesLiteralValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a BytesLiteralValue {
|
||||
impl<'a> IntoIterator for &'a BytesExprValue {
|
||||
type Item = &'a BytesLiteral;
|
||||
type IntoIter = Iter<'a, BytesLiteral>;
|
||||
|
||||
@@ -1566,15 +1567,16 @@ impl<'a> IntoIterator for &'a BytesLiteralValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a mut BytesLiteralValue {
|
||||
impl<'a> IntoIterator for &'a mut BytesExprValue {
|
||||
type Item = &'a mut BytesLiteral;
|
||||
type IntoIter = IterMut<'a, BytesLiteral>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<[u8]> for BytesLiteralValue {
|
||||
impl PartialEq<[u8]> for BytesExprValue {
|
||||
fn eq(&self, other: &[u8]) -> bool {
|
||||
if self.len() != other.len() {
|
||||
return false;
|
||||
@@ -1588,7 +1590,7 @@ impl PartialEq<[u8]> for BytesLiteralValue {
|
||||
|
||||
/// An internal representation of [`BytesLiteralValue`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum BytesLiteralValueInner {
|
||||
enum BytesExprValueInner {
|
||||
/// A single bytes literal i.e., `b"foo"`.
|
||||
Single(BytesLiteral),
|
||||
|
||||
@@ -1596,14 +1598,14 @@ enum BytesLiteralValueInner {
|
||||
Concatenated(Vec<BytesLiteral>),
|
||||
}
|
||||
|
||||
impl Default for BytesLiteralValueInner {
|
||||
impl Default for BytesExprValueInner {
|
||||
fn default() -> Self {
|
||||
Self::Single(BytesLiteral::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// An AST node that represents a single bytes literal which is part of an
|
||||
/// [`ExprBytesLiteral`].
|
||||
/// [`ExprBytes`] node.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct BytesLiteral {
|
||||
pub range: TextRange,
|
||||
@@ -1633,9 +1635,9 @@ impl BytesLiteral {
|
||||
|
||||
impl From<BytesLiteral> for Expr {
|
||||
fn from(payload: BytesLiteral) -> Self {
|
||||
ExprBytesLiteral {
|
||||
ExprBytes {
|
||||
range: payload.range,
|
||||
value: BytesLiteralValue::single(payload),
|
||||
value: BytesExprValue::single(payload),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -3708,8 +3710,8 @@ impl Ranged for crate::Expr {
|
||||
Self::Compare(node) => node.range(),
|
||||
Self::Call(node) => node.range(),
|
||||
Self::FString(node) => node.range(),
|
||||
Self::StringLiteral(node) => node.range(),
|
||||
Self::BytesLiteral(node) => node.range(),
|
||||
Self::String(node) => node.range(),
|
||||
Self::Bytes(node) => node.range(),
|
||||
Self::NumberLiteral(node) => node.range(),
|
||||
Self::BooleanLiteral(node) => node.range(),
|
||||
Self::NoneLiteral(node) => node.range(),
|
||||
|
||||
8
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.options.json
vendored
Normal file
8
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.options.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"preview": "enabled"
|
||||
},
|
||||
{
|
||||
"preview": "disabled"
|
||||
}
|
||||
]
|
||||
@@ -30,22 +30,22 @@ result_f = (
|
||||
# an expression inside a formatted value
|
||||
(
|
||||
f'{1}'
|
||||
# comment
|
||||
# comment 1
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}' # comment
|
||||
f'{1}' # comment 2
|
||||
f'{2}'
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}'
|
||||
f'{2}' # comment
|
||||
f'{2}' # comment 3
|
||||
)
|
||||
|
||||
(
|
||||
1, ( # comment
|
||||
1, ( # comment 4
|
||||
f'{2}'
|
||||
)
|
||||
)
|
||||
@@ -53,7 +53,7 @@ result_f = (
|
||||
(
|
||||
(
|
||||
f'{1}'
|
||||
# comment
|
||||
# comment 5
|
||||
),
|
||||
2
|
||||
)
|
||||
@@ -62,3 +62,221 @@ result_f = (
|
||||
x = f'''a{""}b'''
|
||||
y = f'''c{1}d"""e'''
|
||||
z = f'''a{""}b''' f'''c{1}d"""e'''
|
||||
|
||||
# F-String formatting test cases (Preview)
|
||||
|
||||
# Simple expression with a mix of debug expression and comments.
|
||||
x = f"{a}"
|
||||
x = f"{
|
||||
a = }"
|
||||
x = f"{ # comment 6
|
||||
a }"
|
||||
x = f"{ # comment 7
|
||||
a = }"
|
||||
|
||||
# Remove the parentheses as adding them doesn't make then fit within the line length limit.
|
||||
# This is similar to how we format it before f-string formatting.
|
||||
aaaaaaaaaaa = (
|
||||
f"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd } cccccccccc"
|
||||
)
|
||||
# Here, we would use the best fit layout to put the f-string indented on the next line
|
||||
# similar to the next example.
|
||||
aaaaaaaaaaa = f"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
|
||||
aaaaaaaaaaa = (
|
||||
f"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
|
||||
)
|
||||
|
||||
# This should never add the optional parentheses because even after adding them, the
|
||||
# f-string exceeds the line length limit.
|
||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||
|
||||
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
|
||||
# whether to split at the first or second expression. This should work similarly to the
|
||||
# assignment statement formatting where we split from right to left in preview mode.
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
|
||||
# The above example won't split but when we start introducing line breaks:
|
||||
x = f"aaaaaaaaaaaa {
|
||||
bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb
|
||||
} cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc {
|
||||
ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd
|
||||
} eeeeeeeeeeeeee"
|
||||
|
||||
# But, in case comments are present, we would split at the expression containing the
|
||||
# comments:
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb # comment 10
|
||||
} cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb
|
||||
} cccccccccccccccccccc { # comment 11
|
||||
ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
|
||||
# Here, the expression part itself starts with a curly brace so we need to add an extra
|
||||
# space between the opening curly brace and the expression.
|
||||
x = f"{ {'x': 1, 'y': 2} }"
|
||||
# Although the extra space isn't required before the ending curly brace, we add it for
|
||||
# consistency.
|
||||
x = f"{ {'x': 1, 'y': 2}}"
|
||||
x = f"{ {'x': 1, 'y': 2} = }"
|
||||
x = f"{ # comment 12
|
||||
{'x': 1, 'y': 2} }"
|
||||
x = f"{ # comment 13
|
||||
{'x': 1, 'y': 2} = }"
|
||||
|
||||
# But, in this case, we would split the expression itself because it exceeds the line
|
||||
# length limit so we need not add the extra space.
|
||||
xxxxxxx = f"{
|
||||
{'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'}
|
||||
}"
|
||||
# And, split the expression itself because it exceeds the line length.
|
||||
xxxxxxx = f"{
|
||||
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
|
||||
}"
|
||||
|
||||
# Quotes
|
||||
f"foo 'bar' {x}"
|
||||
f"foo \"bar\" {x}"
|
||||
f'foo "bar" {x}'
|
||||
f'foo \'bar\' {x}'
|
||||
f"foo {"bar"}"
|
||||
f"foo {'\'bar\''}"
|
||||
|
||||
# Here, the formatter will remove the escapes which is correct because they aren't allowed
|
||||
# pre 3.12. This means we can assume that the f-string is used in the context of 3.12.
|
||||
f"foo {'\"bar\"'}"
|
||||
|
||||
|
||||
# Triple-quoted strings
|
||||
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
||||
f"""test {'inner'}"""
|
||||
f"""test {"inner"}"""
|
||||
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
||||
f"""test {'''inner'''}"""
|
||||
|
||||
# Magic trailing comma
|
||||
#
|
||||
# The expression formatting will result in breaking it across multiple lines with a
|
||||
# trailing comma but as the expression isn't already broken, we will remove all the line
|
||||
# breaks which results in the trailing comma being present. This test case makes sure
|
||||
# that the trailing comma is removed as well.
|
||||
f"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa"
|
||||
|
||||
# And, if the trailing comma is already present, we still need to remove it.
|
||||
f"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee',]} aaaaaaa"
|
||||
|
||||
# Keep this Multiline by breaking it at the square brackets.
|
||||
f"""aaaaaa {[
|
||||
xxxxxxxx,
|
||||
yyyyyyyy,
|
||||
]} ccc"""
|
||||
|
||||
# Add the magic trailing comma because the elements don't fit within the line length limit
|
||||
# when collapsed.
|
||||
f"aaaaaa {[
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
yyyyyyyyyyyy
|
||||
]} ccccccc"
|
||||
|
||||
# Remove the parenthese because they aren't required
|
||||
xxxxxxxxxxxxxxx = (
|
||||
f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb {
|
||||
xxxxxxxxxxx # comment 14
|
||||
+ yyyyyyyyyy
|
||||
} dddddddddd"
|
||||
)
|
||||
|
||||
# Comments
|
||||
|
||||
# No comments should be dropped!
|
||||
f"{ # comment 15
|
||||
# comment 16
|
||||
foo # comment 17
|
||||
# comment 18
|
||||
}" # comment 19
|
||||
# comment 20
|
||||
|
||||
# Conversion flags
|
||||
#
|
||||
# This is not a valid Python code because of the additional whitespace between the `!`
|
||||
# and conversion type. But, our parser isn't strict about this. This should probably be
|
||||
# removed once we have a strict parser.
|
||||
x = f"aaaaaaaaa { x ! r }"
|
||||
|
||||
# Even in the case of debug expresions, we only need to preserve the whitespace within
|
||||
# the expression part of the replacement field.
|
||||
x = f"aaaaaaaaa { x = ! r }"
|
||||
|
||||
# Combine conversion flags with format specifiers
|
||||
x = f"{x = ! s
|
||||
:>0
|
||||
|
||||
}"
|
||||
# This is interesting. There can be a comment after the format specifier but only if it's
|
||||
# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details.
|
||||
# We'll format is as trailing comments.
|
||||
x = f"{x !s
|
||||
:>0
|
||||
# comment 21
|
||||
}"
|
||||
|
||||
x = f"""
|
||||
{ # comment 22
|
||||
x = :.0{y # comment 23
|
||||
}f}"""
|
||||
|
||||
# Here, the debug expression is in a nested f-string so we should start preserving
|
||||
# whitespaces from that point onwards. This means we should format the outer f-string.
|
||||
x = f"""{"foo " + # comment 24
|
||||
f"{ x =
|
||||
|
||||
}" # comment 25
|
||||
}
|
||||
"""
|
||||
|
||||
# Mix of various features.
|
||||
f"{ # comment 26
|
||||
foo # after foo
|
||||
:>{
|
||||
x # after x
|
||||
}
|
||||
# comment 27
|
||||
# comment 28
|
||||
} woah {x}"
|
||||
|
||||
# Indentation
|
||||
|
||||
# What should be the indentation?
|
||||
# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590
|
||||
if indent0:
|
||||
if indent1:
|
||||
if indent2:
|
||||
foo = f"""hello world
|
||||
hello {
|
||||
f"aaaaaaa {
|
||||
[
|
||||
'aaaaaaaaaaaaaaaaaaaaa',
|
||||
'bbbbbbbbbbbbbbbbbbbbb',
|
||||
'ccccccccccccccccccccc',
|
||||
'ddddddddddddddddddddd'
|
||||
]
|
||||
} bbbbbbbb" +
|
||||
[
|
||||
'aaaaaaaaaaaaaaaaaaaaa',
|
||||
'bbbbbbbbbbbbbbbbbbbbb',
|
||||
'ccccccccccccccccccccc',
|
||||
'ddddddddddddddddddddd'
|
||||
]
|
||||
} --------
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"target_version": "py312"
|
||||
}
|
||||
]
|
||||
6
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_py312.py
vendored
Normal file
6
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_py312.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# This file contains test cases only for cases where the logic tests for whether
|
||||
# the target version is 3.12 or later. A user can have 3.12 syntax even if the target
|
||||
# version isn't set.
|
||||
|
||||
# Quotes re-use
|
||||
f"{'a'}"
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_formatter::{write, Argument, Arguments};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::context::{NodeLevel, WithNodeLevel};
|
||||
use crate::context::{FStringState, NodeLevel, WithNodeLevel};
|
||||
use crate::other::commas::has_magic_trailing_comma;
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -206,6 +206,16 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
|
||||
|
||||
pub(crate) fn finish(&mut self) -> FormatResult<()> {
|
||||
self.result.and_then(|()| {
|
||||
// If the formatter is inside an f-string expression element, and the layout
|
||||
// is flat, then we don't need to add a trailing comma.
|
||||
if let FStringState::InsideExpressionElement(context) =
|
||||
self.fmt.context().f_string_state()
|
||||
{
|
||||
if context.layout().is_flat() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last_end) = self.entries.position() {
|
||||
let magic_trailing_comma = has_magic_trailing_comma(
|
||||
TextRange::new(last_end, self.sequence_end),
|
||||
|
||||
@@ -289,6 +289,28 @@ fn handle_enclosed_comment<'a>(
|
||||
}
|
||||
}
|
||||
AnyNodeRef::FString(fstring) => CommentPlacement::dangling(fstring, comment),
|
||||
AnyNodeRef::FStringExpressionElement(_) => {
|
||||
// Handle comments after the format specifier (should be rare):
|
||||
//
|
||||
// ```python
|
||||
// f"literal {
|
||||
// expr:.3f
|
||||
// # comment
|
||||
// }"
|
||||
// ```
|
||||
//
|
||||
// This is a valid comment placement.
|
||||
if matches!(
|
||||
comment.preceding_node(),
|
||||
Some(
|
||||
AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_)
|
||||
)
|
||||
) {
|
||||
CommentPlacement::trailing(comment.enclosing_node(), comment)
|
||||
} else {
|
||||
handle_bracketed_end_of_line_comment(comment, locator)
|
||||
}
|
||||
}
|
||||
AnyNodeRef::ExprList(_)
|
||||
| AnyNodeRef::ExprSet(_)
|
||||
| AnyNodeRef::ExprListComp(_)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::comments::Comments;
|
||||
use crate::other::f_string::FStringContext;
|
||||
use crate::string::QuoteChar;
|
||||
use crate::PyFormatOptions;
|
||||
use ruff_formatter::{Buffer, FormatContext, GroupId, IndentWidth, SourceCode};
|
||||
@@ -22,6 +23,8 @@ pub struct PyFormatContext<'a> {
|
||||
/// quote style that is inverted from the one here in order to ensure that
|
||||
/// the formatted Python code will be valid.
|
||||
docstring: Option<QuoteChar>,
|
||||
/// The state of the formatter with respect to f-strings.
|
||||
f_string_state: FStringState,
|
||||
}
|
||||
|
||||
impl<'a> PyFormatContext<'a> {
|
||||
@@ -33,6 +36,7 @@ impl<'a> PyFormatContext<'a> {
|
||||
node_level: NodeLevel::TopLevel(TopLevelStatementPosition::Other),
|
||||
indent_level: IndentLevel::new(0),
|
||||
docstring: None,
|
||||
f_string_state: FStringState::Outside,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +90,14 @@ impl<'a> PyFormatContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn f_string_state(&self) -> FStringState {
|
||||
self.f_string_state
|
||||
}
|
||||
|
||||
pub(crate) fn set_f_string_state(&mut self, f_string_state: FStringState) {
|
||||
self.f_string_state = f_string_state;
|
||||
}
|
||||
|
||||
/// Returns `true` if preview mode is enabled.
|
||||
pub(crate) const fn is_preview(&self) -> bool {
|
||||
self.options.preview().is_enabled()
|
||||
@@ -115,6 +127,18 @@ impl Debug for PyFormatContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub(crate) enum FStringState {
|
||||
/// The formatter is inside an f-string expression element i.e., between the
|
||||
/// curly brace in `f"foo {x}"`.
|
||||
///
|
||||
/// The containing `FStringContext` is the surrounding f-string context.
|
||||
InsideExpressionElement(FStringContext),
|
||||
/// The formatter is outside an f-string.
|
||||
#[default]
|
||||
Outside,
|
||||
}
|
||||
|
||||
/// The position of a top-level statement in the module.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
|
||||
pub(crate) enum TopLevelStatementPosition {
|
||||
@@ -332,3 +356,65 @@ where
|
||||
.set_indent_level(self.saved_level);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WithFStringState<'a, B, D>
|
||||
where
|
||||
D: DerefMut<Target = B>,
|
||||
B: Buffer<Context = PyFormatContext<'a>>,
|
||||
{
|
||||
buffer: D,
|
||||
saved_location: FStringState,
|
||||
}
|
||||
|
||||
impl<'a, B, D> WithFStringState<'a, B, D>
|
||||
where
|
||||
D: DerefMut<Target = B>,
|
||||
B: Buffer<Context = PyFormatContext<'a>>,
|
||||
{
|
||||
pub(crate) fn new(expr_location: FStringState, mut buffer: D) -> Self {
|
||||
let context = buffer.state_mut().context_mut();
|
||||
let saved_location = context.f_string_state();
|
||||
|
||||
context.set_f_string_state(expr_location);
|
||||
|
||||
Self {
|
||||
buffer,
|
||||
saved_location,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B, D> Deref for WithFStringState<'a, B, D>
|
||||
where
|
||||
D: DerefMut<Target = B>,
|
||||
B: Buffer<Context = PyFormatContext<'a>>,
|
||||
{
|
||||
type Target = B;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B, D> DerefMut for WithFStringState<'a, B, D>
|
||||
where
|
||||
D: DerefMut<Target = B>,
|
||||
B: Buffer<Context = PyFormatContext<'a>>,
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B, D> Drop for WithFStringState<'a, B, D>
|
||||
where
|
||||
D: DerefMut<Target = B>,
|
||||
B: Buffer<Context = PyFormatContext<'a>>,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
self.buffer
|
||||
.state_mut()
|
||||
.context_mut()
|
||||
.set_f_string_state(self.saved_location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,24 @@ impl NeedsParentheses for ExprFString {
|
||||
) -> OptionalParentheses {
|
||||
if self.value.is_implicit_concatenated() {
|
||||
OptionalParentheses::Multiline
|
||||
// TODO(dhruvmanila): Ideally what we want here is a new variant which
|
||||
// is something like:
|
||||
// - If the expression fits by just adding the parentheses, then add them and
|
||||
// avoid breaking the f-string expression. So,
|
||||
// ```
|
||||
// xxxxxxxxx = (
|
||||
// f"aaaaaaaaaaaa { xxxxxxx + yyyyyyyy } bbbbbbbbbbbbb"
|
||||
// )
|
||||
// ```
|
||||
// - But, if the expression is too long to fit even with parentheses, then
|
||||
// don't add the parentheses and instead break the expression at `soft_line_break`.
|
||||
// ```
|
||||
// xxxxxxxxx = f"aaaaaaaaaaaa {
|
||||
// xxxxxxxxx + yyyyyyyyyy
|
||||
// } bbbbbbbbbbbbb"
|
||||
// ```
|
||||
// This isn't decided yet, refer to the relevant discussion:
|
||||
// https://github.com/astral-sh/ruff/discussions/9785
|
||||
} else if AnyString::FString(self).is_multiline(context.source()) {
|
||||
OptionalParentheses::Never
|
||||
} else {
|
||||
|
||||
@@ -466,3 +466,12 @@ pub enum PythonVersion {
|
||||
Py311,
|
||||
Py312,
|
||||
}
|
||||
|
||||
impl PythonVersion {
|
||||
/// Return `true` if the current version supports [PEP 701].
|
||||
///
|
||||
/// [PEP 701]: https://peps.python.org/pep-0701/
|
||||
pub fn supports_pep_701(self) -> bool {
|
||||
self >= Self::Py312
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::FString;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::string::{Quoting, StringNormalizer, StringPart};
|
||||
use crate::preview::is_f_string_formatting_enabled;
|
||||
use crate::string::{Quoting, StringNormalizer, StringPart, StringPrefix, StringQuotes};
|
||||
|
||||
use super::f_string_element::FormatFStringElement;
|
||||
|
||||
/// Formats an f-string which is part of a larger f-string expression.
|
||||
///
|
||||
@@ -25,25 +30,126 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let locator = f.context().locator();
|
||||
|
||||
let result = StringNormalizer::from_context(f.context())
|
||||
let string = StringPart::from_source(self.value.range(), &locator);
|
||||
|
||||
let normalizer = StringNormalizer::from_context(f.context())
|
||||
.with_quoting(self.quoting)
|
||||
.with_preferred_quote_style(f.options().quote_style())
|
||||
.normalize(
|
||||
&StringPart::from_source(self.value.range(), &locator),
|
||||
&locator,
|
||||
.with_preferred_quote_style(f.options().quote_style());
|
||||
|
||||
// If f-string formatting is disabled (not in preview), then we will
|
||||
// fall back to the previous behavior of normalizing the f-string.
|
||||
if !is_f_string_formatting_enabled(f.context()) {
|
||||
let result = normalizer.normalize(&string, &locator).fmt(f);
|
||||
let comments = f.context().comments();
|
||||
self.value.elements.iter().for_each(|value| {
|
||||
comments.mark_verbatim_node_comments_formatted(value.into());
|
||||
// Above method doesn't mark the trailing comments of the f-string elements
|
||||
// as formatted, so we need to do it manually. For example,
|
||||
//
|
||||
// ```python
|
||||
// f"""foo {
|
||||
// x:.3f
|
||||
// # comment
|
||||
// }"""
|
||||
// ```
|
||||
for trailing_comment in comments.trailing(value) {
|
||||
trailing_comment.mark_formatted();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
let quotes = normalizer.choose_quotes(&string, &locator);
|
||||
|
||||
let context = FStringContext::new(
|
||||
string.prefix(),
|
||||
quotes,
|
||||
FStringLayout::from_f_string(self.value, &locator),
|
||||
);
|
||||
|
||||
// Starting prefix and quote
|
||||
write!(f, [string.prefix(), quotes])?;
|
||||
|
||||
f.join()
|
||||
.entries(
|
||||
self.value
|
||||
.elements
|
||||
.iter()
|
||||
.map(|element| FormatFStringElement::new(element, context)),
|
||||
)
|
||||
.fmt(f);
|
||||
.finish()?;
|
||||
|
||||
// TODO(dhruvmanila): With PEP 701, comments can be inside f-strings.
|
||||
// This is to mark all of those comments as formatted but we need to
|
||||
// figure out how to handle them. Note that this needs to be done only
|
||||
// after the f-string is formatted, so only for all the non-formatted
|
||||
// comments.
|
||||
let comments = f.context().comments();
|
||||
self.value.elements.iter().for_each(|value| {
|
||||
comments.mark_verbatim_node_comments_formatted(value.into());
|
||||
});
|
||||
|
||||
result
|
||||
// Ending quote
|
||||
quotes.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct FStringContext {
|
||||
prefix: StringPrefix,
|
||||
quotes: StringQuotes,
|
||||
layout: FStringLayout,
|
||||
}
|
||||
|
||||
impl FStringContext {
|
||||
const fn new(prefix: StringPrefix, quotes: StringQuotes, layout: FStringLayout) -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
quotes,
|
||||
layout,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn quotes(self) -> StringQuotes {
|
||||
self.quotes
|
||||
}
|
||||
|
||||
pub(crate) const fn prefix(self) -> StringPrefix {
|
||||
self.prefix
|
||||
}
|
||||
|
||||
pub(crate) const fn layout(self) -> FStringLayout {
|
||||
self.layout
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum FStringLayout {
|
||||
/// Original f-string is flat.
|
||||
/// Don't break expressions to keep the string flat.
|
||||
Flat,
|
||||
/// Original f-string has multiline expressions in the replacement fields.
|
||||
/// Allow breaking expressions across multiple lines.
|
||||
Multiline,
|
||||
}
|
||||
|
||||
impl FStringLayout {
|
||||
fn from_f_string(f_string: &FString, locator: &Locator) -> Self {
|
||||
// Heuristic: Allow breaking the f-string expressions across multiple lines
|
||||
// only if there already is at least one multiline expression. This puts the
|
||||
// control in the hands of the user to decide if they want to break the
|
||||
// f-string expressions across multiple lines or not. This is similar to
|
||||
// how Prettier does it for template literals in JavaScript.
|
||||
//
|
||||
// If it's single quoted f-string and it contains a multiline expression, then we
|
||||
// assume that the target version of Python supports it (3.12+). If there are comments
|
||||
// used in any of the expression of the f-string, then it's always going to be multiline
|
||||
// and we assume that the target version of Python supports it (3.12+).
|
||||
//
|
||||
// Reference: https://prettier.io/docs/en/next/rationale.html#template-literals
|
||||
if f_string
|
||||
.elements
|
||||
.iter()
|
||||
.filter_map(|element| element.as_expression())
|
||||
.any(|expr| memchr::memchr2(b'\n', b'\r', locator.slice(expr).as_bytes()).is_some())
|
||||
{
|
||||
Self::Multiline
|
||||
} else {
|
||||
Self::Flat
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_flat(self) -> bool {
|
||||
matches!(self, Self::Flat)
|
||||
}
|
||||
}
|
||||
|
||||
244
crates/ruff_python_formatter/src/other/f_string_element.rs
Normal file
244
crates/ruff_python_formatter/src/other/f_string_element.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ruff_formatter::{format_args, write, Buffer, RemoveSoftLinesBuffer};
|
||||
use ruff_python_ast::{
|
||||
ConversionFlag, Expr, FStringElement, FStringExpressionElement, FStringLiteralElement,
|
||||
};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::{dangling_open_parenthesis_comments, trailing_comments};
|
||||
use crate::context::{FStringState, NodeLevel, WithFStringState, WithNodeLevel};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_hex_codes_in_unicode_sequences_enabled;
|
||||
use crate::string::normalize_string;
|
||||
use crate::verbatim::verbatim_text;
|
||||
|
||||
use super::f_string::FStringContext;
|
||||
|
||||
/// Formats an f-string element which is either a literal or a formatted expression.
|
||||
///
|
||||
/// This delegates the actual formatting to the appropriate formatter.
|
||||
pub(crate) struct FormatFStringElement<'a> {
|
||||
element: &'a FStringElement,
|
||||
context: FStringContext,
|
||||
}
|
||||
|
||||
impl<'a> FormatFStringElement<'a> {
|
||||
pub(crate) fn new(element: &'a FStringElement, context: FStringContext) -> Self {
|
||||
Self { element, context }
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatFStringElement<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
match self.element {
|
||||
FStringElement::Literal(string_literal) => {
|
||||
FormatFStringLiteralElement::new(string_literal, self.context).fmt(f)
|
||||
}
|
||||
FStringElement::Expression(expression) => {
|
||||
FormatFStringExpressionElement::new(expression, self.context).fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an f-string literal element.
|
||||
pub(crate) struct FormatFStringLiteralElement<'a> {
|
||||
element: &'a FStringLiteralElement,
|
||||
context: FStringContext,
|
||||
}
|
||||
|
||||
impl<'a> FormatFStringLiteralElement<'a> {
|
||||
pub(crate) fn new(element: &'a FStringLiteralElement, context: FStringContext) -> Self {
|
||||
Self { element, context }
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let literal_content = f.context().locator().slice(self.element.range());
|
||||
let normalized = normalize_string(
|
||||
literal_content,
|
||||
self.context.quotes(),
|
||||
self.context.prefix(),
|
||||
is_hex_codes_in_unicode_sequences_enabled(f.context()),
|
||||
);
|
||||
match &normalized {
|
||||
Cow::Borrowed(_) => source_text_slice(self.element.range()).fmt(f),
|
||||
Cow::Owned(normalized) => text(normalized).fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an f-string expression element.
|
||||
pub(crate) struct FormatFStringExpressionElement<'a> {
|
||||
element: &'a FStringExpressionElement,
|
||||
context: FStringContext,
|
||||
}
|
||||
|
||||
impl<'a> FormatFStringExpressionElement<'a> {
|
||||
pub(crate) fn new(element: &'a FStringExpressionElement, context: FStringContext) -> Self {
|
||||
Self { element, context }
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatFStringExpressionElement<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let FStringExpressionElement {
|
||||
expression,
|
||||
debug_text,
|
||||
conversion,
|
||||
format_spec,
|
||||
..
|
||||
} = self.element;
|
||||
|
||||
if let Some(debug_text) = debug_text {
|
||||
token("{").fmt(f)?;
|
||||
|
||||
let comments = f.context().comments();
|
||||
|
||||
// If the element has a debug text, preserve the same formatting as
|
||||
// in the source code (`verbatim`). This requires us to mark all of
|
||||
// the surrounding comments as formatted.
|
||||
comments.mark_verbatim_node_comments_formatted(self.element.into());
|
||||
|
||||
// Above method doesn't mark the leading and trailing comments of the element.
|
||||
// There can't be any leading comments for an expression element, but there
|
||||
// can be trailing comments. For example,
|
||||
//
|
||||
// ```python
|
||||
// f"""foo {
|
||||
// x:.3f
|
||||
// # trailing comment
|
||||
// }"""
|
||||
// ```
|
||||
for trailing_comment in comments.trailing(self.element) {
|
||||
trailing_comment.mark_formatted();
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
text(&debug_text.leading),
|
||||
verbatim_text(&**expression),
|
||||
text(&debug_text.trailing),
|
||||
]
|
||||
)?;
|
||||
|
||||
// Even if debug text is present, any whitespace between the
|
||||
// conversion flag and the format spec doesn't need to be preserved.
|
||||
match conversion {
|
||||
ConversionFlag::Str => text("!s").fmt(f)?,
|
||||
ConversionFlag::Ascii => text("!a").fmt(f)?,
|
||||
ConversionFlag::Repr => text("!r").fmt(f)?,
|
||||
ConversionFlag::None => (),
|
||||
}
|
||||
|
||||
if let Some(format_spec) = format_spec.as_deref() {
|
||||
write!(f, [token(":"), verbatim_text(format_spec)])?;
|
||||
}
|
||||
|
||||
token("}").fmt(f)
|
||||
} else {
|
||||
let comments = f.context().comments().clone();
|
||||
let dangling_item_comments = comments.dangling(self.element);
|
||||
|
||||
let item = format_with(|f| {
|
||||
let bracket_spacing = match expression.as_ref() {
|
||||
// If an expression starts with a `{`, we need to add a space before the
|
||||
// curly brace to avoid turning it into a literal curly with `{{`.
|
||||
//
|
||||
// For example,
|
||||
// ```python
|
||||
// f"{ {'x': 1, 'y': 2} }"
|
||||
// # ^ ^
|
||||
// ```
|
||||
//
|
||||
// We need to preserve the space highlighted by `^`. The whitespace
|
||||
// before the closing curly brace is not strictly necessary, but it's
|
||||
// added to maintain consistency.
|
||||
Expr::Dict(_) | Expr::DictComp(_) | Expr::Set(_) | Expr::SetComp(_) => {
|
||||
Some(format_with(|f| {
|
||||
if self.context.layout().is_flat() {
|
||||
space().fmt(f)
|
||||
} else {
|
||||
soft_line_break_or_space().fmt(f)
|
||||
}
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Update the context to be inside the f-string expression element.
|
||||
let f = &mut WithFStringState::new(
|
||||
FStringState::InsideExpressionElement(self.context),
|
||||
f,
|
||||
);
|
||||
|
||||
write!(f, [bracket_spacing, expression.format()])?;
|
||||
|
||||
// Conversion comes first, then the format spec.
|
||||
match conversion {
|
||||
ConversionFlag::Str => text("!s").fmt(f)?,
|
||||
ConversionFlag::Ascii => text("!a").fmt(f)?,
|
||||
ConversionFlag::Repr => text("!r").fmt(f)?,
|
||||
ConversionFlag::None => (),
|
||||
}
|
||||
|
||||
if let Some(format_spec) = format_spec.as_deref() {
|
||||
token(":").fmt(f)?;
|
||||
|
||||
f.join()
|
||||
.entries(
|
||||
format_spec
|
||||
.elements
|
||||
.iter()
|
||||
.map(|element| FormatFStringElement::new(element, self.context)),
|
||||
)
|
||||
.finish()?;
|
||||
|
||||
// These trailing comments can only occur if the format specifier is
|
||||
// present. For example,
|
||||
//
|
||||
// ```python
|
||||
// f"{
|
||||
// x:.3f
|
||||
// # comment
|
||||
// }"
|
||||
// ```
|
||||
//
|
||||
// Any other trailing comments are attached to the expression itself.
|
||||
trailing_comments(comments.trailing(self.element)).fmt(f)?;
|
||||
}
|
||||
|
||||
bracket_spacing.fmt(f)
|
||||
});
|
||||
|
||||
let open_parenthesis_comments = if dangling_item_comments.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(dangling_open_parenthesis_comments(dangling_item_comments))
|
||||
};
|
||||
|
||||
token("{").fmt(f)?;
|
||||
|
||||
{
|
||||
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
|
||||
|
||||
if self.context.layout().is_flat() {
|
||||
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);
|
||||
|
||||
write!(buffer, [open_parenthesis_comments, item])?;
|
||||
} else {
|
||||
group(&format_args![
|
||||
open_parenthesis_comments,
|
||||
soft_block_indent(&item)
|
||||
])
|
||||
.fmt(&mut f)?;
|
||||
}
|
||||
}
|
||||
|
||||
token("}").fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub(crate) mod decorator;
|
||||
pub(crate) mod elif_else_clause;
|
||||
pub(crate) mod except_handler_except_handler;
|
||||
pub(crate) mod f_string;
|
||||
pub(crate) mod f_string_element;
|
||||
pub(crate) mod f_string_part;
|
||||
pub(crate) mod identifier;
|
||||
pub(crate) mod keyword;
|
||||
|
||||
@@ -81,3 +81,8 @@ pub(crate) const fn is_multiline_string_handling_enabled(context: &PyFormatConte
|
||||
pub(crate) const fn is_format_module_docstring_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`f-string formatting`](https://github.com/astral-sh/ruff/issues/7594) preview style is enabled.
|
||||
pub(crate) fn is_f_string_formatting_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
212
crates/ruff_python_formatter/src/string/any.rs
Normal file
212
crates/ruff_python_formatter/src/string/any.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use memchr::memchr2;
|
||||
|
||||
use ruff_python_ast::{
|
||||
self as ast, AnyNodeRef, Expr, ExprBytesLiteral, ExprFString, ExprStringLiteral, ExpressionRef,
|
||||
StringLiteral,
|
||||
};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
|
||||
use crate::expression::expr_f_string::f_string_quoting;
|
||||
use crate::other::f_string::FormatFString;
|
||||
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
||||
use crate::prelude::*;
|
||||
use crate::string::{Quoting, StringPrefix, StringQuotes};
|
||||
|
||||
/// Represents any kind of string expression. This could be either a string,
|
||||
/// bytes or f-string.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum AnyString<'a> {
|
||||
String(&'a ExprStringLiteral),
|
||||
Bytes(&'a ExprBytesLiteral),
|
||||
FString(&'a ExprFString),
|
||||
}
|
||||
|
||||
impl<'a> AnyString<'a> {
|
||||
/// Creates a new [`AnyString`] from the given [`Expr`].
|
||||
///
|
||||
/// Returns `None` if the expression is not either a string, bytes or f-string.
|
||||
pub(crate) fn from_expression(expression: &'a Expr) -> Option<AnyString<'a>> {
|
||||
match expression {
|
||||
Expr::StringLiteral(string) => Some(AnyString::String(string)),
|
||||
Expr::BytesLiteral(bytes) => Some(AnyString::Bytes(bytes)),
|
||||
Expr::FString(fstring) => Some(AnyString::FString(fstring)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the string is implicitly concatenated.
|
||||
pub(crate) fn is_implicit_concatenated(self) -> bool {
|
||||
match self {
|
||||
Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(),
|
||||
Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(),
|
||||
Self::FString(ExprFString { value, .. }) => value.is_implicit_concatenated(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the quoting to be used for this string.
|
||||
pub(super) fn quoting(self, locator: &Locator<'_>) -> Quoting {
|
||||
match self {
|
||||
Self::String(_) | Self::Bytes(_) => Quoting::CanChange,
|
||||
Self::FString(f_string) => f_string_quoting(f_string, locator),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a vector of all the [`AnyStringPart`] of this string.
|
||||
pub(super) fn parts(self, quoting: Quoting) -> AnyStringPartsIter<'a> {
|
||||
match self {
|
||||
Self::String(ExprStringLiteral { value, .. }) => {
|
||||
AnyStringPartsIter::String(value.iter())
|
||||
}
|
||||
Self::Bytes(ExprBytesLiteral { value, .. }) => AnyStringPartsIter::Bytes(value.iter()),
|
||||
Self::FString(ExprFString { value, .. }) => {
|
||||
AnyStringPartsIter::FString(value.iter(), quoting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_multiline(self, source: &str) -> bool {
|
||||
match self {
|
||||
AnyString::String(_) | AnyString::Bytes(_) => {
|
||||
let contents = &source[self.range()];
|
||||
let prefix = StringPrefix::parse(contents);
|
||||
let quotes = StringQuotes::parse(
|
||||
&contents[TextRange::new(prefix.text_len(), contents.text_len())],
|
||||
);
|
||||
|
||||
quotes.is_some_and(StringQuotes::is_triple)
|
||||
&& memchr2(b'\n', b'\r', contents.as_bytes()).is_some()
|
||||
}
|
||||
AnyString::FString(fstring) => {
|
||||
memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for AnyString<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Self::String(expr) => expr.range(),
|
||||
Self::Bytes(expr) => expr.range(),
|
||||
Self::FString(expr) => expr.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&AnyString<'a>> for AnyNodeRef<'a> {
|
||||
fn from(value: &AnyString<'a>) -> Self {
|
||||
match value {
|
||||
AnyString::String(expr) => AnyNodeRef::ExprStringLiteral(expr),
|
||||
AnyString::Bytes(expr) => AnyNodeRef::ExprBytesLiteral(expr),
|
||||
AnyString::FString(expr) => AnyNodeRef::ExprFString(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AnyString<'a>> for AnyNodeRef<'a> {
|
||||
fn from(value: AnyString<'a>) -> Self {
|
||||
AnyNodeRef::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&AnyString<'a>> for ExpressionRef<'a> {
|
||||
fn from(value: &AnyString<'a>) -> Self {
|
||||
match value {
|
||||
AnyString::String(expr) => ExpressionRef::StringLiteral(expr),
|
||||
AnyString::Bytes(expr) => ExpressionRef::BytesLiteral(expr),
|
||||
AnyString::FString(expr) => ExpressionRef::FString(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum AnyStringPartsIter<'a> {
|
||||
String(std::slice::Iter<'a, StringLiteral>),
|
||||
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
|
||||
FString(std::slice::Iter<'a, ast::FStringPart>, Quoting),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AnyStringPartsIter<'a> {
|
||||
type Item = AnyStringPart<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let part = match self {
|
||||
Self::String(inner) => {
|
||||
let part = inner.next()?;
|
||||
AnyStringPart::String {
|
||||
part,
|
||||
layout: StringLiteralKind::String,
|
||||
}
|
||||
}
|
||||
Self::Bytes(inner) => AnyStringPart::Bytes(inner.next()?),
|
||||
Self::FString(inner, quoting) => {
|
||||
let part = inner.next()?;
|
||||
match part {
|
||||
ast::FStringPart::Literal(string_literal) => AnyStringPart::String {
|
||||
part: string_literal,
|
||||
layout: StringLiteralKind::InImplicitlyConcatenatedFString(*quoting),
|
||||
},
|
||||
ast::FStringPart::FString(f_string) => AnyStringPart::FString {
|
||||
part: f_string,
|
||||
quoting: *quoting,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(part)
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for AnyStringPartsIter<'_> {}
|
||||
|
||||
/// Represents any kind of string which is part of an implicitly concatenated
|
||||
/// string. This could be either a string, bytes or f-string.
|
||||
///
|
||||
/// This is constructed from the [`AnyString::parts`] method on [`AnyString`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) enum AnyStringPart<'a> {
|
||||
String {
|
||||
part: &'a ast::StringLiteral,
|
||||
layout: StringLiteralKind,
|
||||
},
|
||||
Bytes(&'a ast::BytesLiteral),
|
||||
FString {
|
||||
part: &'a ast::FString,
|
||||
quoting: Quoting,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<&AnyStringPart<'a>> for AnyNodeRef<'a> {
|
||||
fn from(value: &AnyStringPart<'a>) -> Self {
|
||||
match value {
|
||||
AnyStringPart::String { part, .. } => AnyNodeRef::StringLiteral(part),
|
||||
AnyStringPart::Bytes(part) => AnyNodeRef::BytesLiteral(part),
|
||||
AnyStringPart::FString { part, .. } => AnyNodeRef::FString(part),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for AnyStringPart<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Self::String { part, .. } => part.range(),
|
||||
Self::Bytes(part) => part.range(),
|
||||
Self::FString { part, .. } => part.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for AnyStringPart<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
match self {
|
||||
AnyStringPart::String { part, layout } => {
|
||||
FormatStringLiteral::new(part, *layout).fmt(f)
|
||||
}
|
||||
AnyStringPart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
|
||||
AnyStringPart::FString { part, quoting } => FormatFString::new(part, *quoting).fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ use super::{NormalizedString, QuoteChar};
|
||||
/// `indent-width * spaces` to tabs because doing so could break ASCII art and other docstrings
|
||||
/// that use spaces for alignment.
|
||||
pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let docstring = &normalized.text;
|
||||
let docstring = &normalized.text();
|
||||
|
||||
// Black doesn't change the indentation of docstrings that contain an escaped newline
|
||||
if contains_unescaped_newline(docstring) {
|
||||
@@ -125,7 +125,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
||||
let mut lines = docstring.split('\n').peekable();
|
||||
|
||||
// Start the string
|
||||
write!(f, [normalized.prefix, normalized.quotes])?;
|
||||
write!(f, [normalized.prefix(), normalized.quotes()])?;
|
||||
// We track where in the source docstring we are (in source code byte offsets)
|
||||
let mut offset = normalized.start();
|
||||
|
||||
@@ -141,7 +141,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
||||
|
||||
// Edge case: The first line is `""" "content`, so we need to insert chaperone space that keep
|
||||
// inner quotes and closing quotes from getting to close to avoid `""""content`
|
||||
if trim_both.starts_with(normalized.quotes.quote_char.as_char()) {
|
||||
if trim_both.starts_with(normalized.quotes().quote_char.as_char()) {
|
||||
space().fmt(f)?;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
||||
{
|
||||
space().fmt(f)?;
|
||||
}
|
||||
normalized.quotes.fmt(f)?;
|
||||
normalized.quotes().fmt(f)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
||||
offset,
|
||||
stripped_indentation,
|
||||
already_normalized,
|
||||
quote_char: normalized.quotes.quote_char,
|
||||
quote_char: normalized.quotes().quote_char,
|
||||
code_example: CodeExample::default(),
|
||||
}
|
||||
.add_iter(lines)?;
|
||||
@@ -207,7 +207,7 @@ pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> Form
|
||||
space().fmt(f)?;
|
||||
}
|
||||
|
||||
write!(f, [normalized.quotes])
|
||||
write!(f, [normalized.quotes()])
|
||||
}
|
||||
|
||||
fn contains_unescaped_newline(haystack: &str) -> bool {
|
||||
@@ -1569,7 +1569,7 @@ fn docstring_format_source(
|
||||
/// that avoids `content""""` and `content\"""`. This does only applies to un-escaped backslashes,
|
||||
/// so `content\\ """` doesn't need a space while `content\\\ """` does.
|
||||
fn needs_chaperone_space(normalized: &NormalizedString, trim_end: &str) -> bool {
|
||||
trim_end.ends_with(normalized.quotes.quote_char.as_char())
|
||||
trim_end.ends_with(normalized.quotes().quote_char.as_char())
|
||||
|| trim_end.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use memchr::memchr2;
|
||||
|
||||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::{
|
||||
self as ast, Expr, ExprBytesLiteral, ExprFString, ExprStringLiteral, ExpressionRef,
|
||||
};
|
||||
use ruff_python_ast::{AnyNodeRef, StringLiteral};
|
||||
pub(crate) use any::AnyString;
|
||||
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
|
||||
use ruff_formatter::format_args;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::comments::{leading_comments, trailing_comments};
|
||||
use crate::expression::expr_f_string::f_string_quoting;
|
||||
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
|
||||
use crate::other::f_string::FormatFString;
|
||||
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_hex_codes_in_unicode_sequences_enabled;
|
||||
use crate::QuoteStyle;
|
||||
|
||||
mod any;
|
||||
pub(crate) mod docstring;
|
||||
mod normalize;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub(crate) enum Quoting {
|
||||
@@ -30,202 +22,6 @@ pub(crate) enum Quoting {
|
||||
Preserve,
|
||||
}
|
||||
|
||||
/// Represents any kind of string expression. This could be either a string,
|
||||
/// bytes or f-string.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum AnyString<'a> {
|
||||
String(&'a ExprStringLiteral),
|
||||
Bytes(&'a ExprBytesLiteral),
|
||||
FString(&'a ExprFString),
|
||||
}
|
||||
|
||||
impl<'a> AnyString<'a> {
|
||||
/// Creates a new [`AnyString`] from the given [`Expr`].
|
||||
///
|
||||
/// Returns `None` if the expression is not either a string, bytes or f-string.
|
||||
pub(crate) fn from_expression(expression: &'a Expr) -> Option<AnyString<'a>> {
|
||||
match expression {
|
||||
Expr::StringLiteral(string) => Some(AnyString::String(string)),
|
||||
Expr::BytesLiteral(bytes) => Some(AnyString::Bytes(bytes)),
|
||||
Expr::FString(fstring) => Some(AnyString::FString(fstring)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the string is implicitly concatenated.
|
||||
pub(crate) fn is_implicit_concatenated(self) -> bool {
|
||||
match self {
|
||||
Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(),
|
||||
Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(),
|
||||
Self::FString(ExprFString { value, .. }) => value.is_implicit_concatenated(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the quoting to be used for this string.
|
||||
fn quoting(self, locator: &Locator<'_>) -> Quoting {
|
||||
match self {
|
||||
Self::String(_) | Self::Bytes(_) => Quoting::CanChange,
|
||||
Self::FString(f_string) => f_string_quoting(f_string, locator),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a vector of all the [`AnyStringPart`] of this string.
|
||||
fn parts(self, quoting: Quoting) -> AnyStringPartsIter<'a> {
|
||||
match self {
|
||||
Self::String(ExprStringLiteral { value, .. }) => {
|
||||
AnyStringPartsIter::String(value.iter())
|
||||
}
|
||||
Self::Bytes(ExprBytesLiteral { value, .. }) => AnyStringPartsIter::Bytes(value.iter()),
|
||||
Self::FString(ExprFString { value, .. }) => {
|
||||
AnyStringPartsIter::FString(value.iter(), quoting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_multiline(self, source: &str) -> bool {
|
||||
match self {
|
||||
AnyString::String(_) | AnyString::Bytes(_) => {
|
||||
let contents = &source[self.range()];
|
||||
let prefix = StringPrefix::parse(contents);
|
||||
let quotes = StringQuotes::parse(
|
||||
&contents[TextRange::new(prefix.text_len(), contents.text_len())],
|
||||
);
|
||||
|
||||
quotes.is_some_and(StringQuotes::is_triple)
|
||||
&& memchr2(b'\n', b'\r', contents.as_bytes()).is_some()
|
||||
}
|
||||
AnyString::FString(fstring) => {
|
||||
memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for AnyString<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Self::String(expr) => expr.range(),
|
||||
Self::Bytes(expr) => expr.range(),
|
||||
Self::FString(expr) => expr.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&AnyString<'a>> for AnyNodeRef<'a> {
|
||||
fn from(value: &AnyString<'a>) -> Self {
|
||||
match value {
|
||||
AnyString::String(expr) => AnyNodeRef::ExprStringLiteral(expr),
|
||||
AnyString::Bytes(expr) => AnyNodeRef::ExprBytesLiteral(expr),
|
||||
AnyString::FString(expr) => AnyNodeRef::ExprFString(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AnyString<'a>> for AnyNodeRef<'a> {
|
||||
fn from(value: AnyString<'a>) -> Self {
|
||||
AnyNodeRef::from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&AnyString<'a>> for ExpressionRef<'a> {
|
||||
fn from(value: &AnyString<'a>) -> Self {
|
||||
match value {
|
||||
AnyString::String(expr) => ExpressionRef::StringLiteral(expr),
|
||||
AnyString::Bytes(expr) => ExpressionRef::BytesLiteral(expr),
|
||||
AnyString::FString(expr) => ExpressionRef::FString(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AnyStringPartsIter<'a> {
|
||||
String(std::slice::Iter<'a, StringLiteral>),
|
||||
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
|
||||
FString(std::slice::Iter<'a, ast::FStringPart>, Quoting),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AnyStringPartsIter<'a> {
|
||||
type Item = AnyStringPart<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let part = match self {
|
||||
Self::String(inner) => {
|
||||
let part = inner.next()?;
|
||||
AnyStringPart::String {
|
||||
part,
|
||||
layout: StringLiteralKind::String,
|
||||
}
|
||||
}
|
||||
Self::Bytes(inner) => AnyStringPart::Bytes(inner.next()?),
|
||||
Self::FString(inner, quoting) => {
|
||||
let part = inner.next()?;
|
||||
match part {
|
||||
ast::FStringPart::Literal(string_literal) => AnyStringPart::String {
|
||||
part: string_literal,
|
||||
layout: StringLiteralKind::InImplicitlyConcatenatedFString(*quoting),
|
||||
},
|
||||
ast::FStringPart::FString(f_string) => AnyStringPart::FString {
|
||||
part: f_string,
|
||||
quoting: *quoting,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(part)
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for AnyStringPartsIter<'_> {}
|
||||
|
||||
/// Represents any kind of string which is part of an implicitly concatenated
|
||||
/// string. This could be either a string, bytes or f-string.
|
||||
///
|
||||
/// This is constructed from the [`AnyString::parts`] method on [`AnyString`].
|
||||
#[derive(Clone, Debug)]
|
||||
enum AnyStringPart<'a> {
|
||||
String {
|
||||
part: &'a ast::StringLiteral,
|
||||
layout: StringLiteralKind,
|
||||
},
|
||||
Bytes(&'a ast::BytesLiteral),
|
||||
FString {
|
||||
part: &'a ast::FString,
|
||||
quoting: Quoting,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<&AnyStringPart<'a>> for AnyNodeRef<'a> {
|
||||
fn from(value: &AnyStringPart<'a>) -> Self {
|
||||
match value {
|
||||
AnyStringPart::String { part, .. } => AnyNodeRef::StringLiteral(part),
|
||||
AnyStringPart::Bytes(part) => AnyNodeRef::BytesLiteral(part),
|
||||
AnyStringPart::FString { part, .. } => AnyNodeRef::FString(part),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for AnyStringPart<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Self::String { part, .. } => part.range(),
|
||||
Self::Bytes(part) => part.range(),
|
||||
Self::FString { part, .. } => part.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for AnyStringPart<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
match self {
|
||||
AnyStringPart::String { part, layout } => {
|
||||
FormatStringLiteral::new(part, *layout).fmt(f)
|
||||
}
|
||||
AnyStringPart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
|
||||
AnyStringPart::FString { part, quoting } => FormatFString::new(part, *quoting).fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats any implicitly concatenated string. This could be any valid combination
|
||||
/// of string, bytes or f-string literals.
|
||||
pub(crate) struct FormatStringContinuation<'a> {
|
||||
@@ -308,167 +104,6 @@ impl StringPart {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StringNormalizer {
|
||||
quoting: Quoting,
|
||||
preferred_quote_style: QuoteStyle,
|
||||
parent_docstring_quote_char: Option<QuoteChar>,
|
||||
normalize_hex: bool,
|
||||
}
|
||||
|
||||
impl StringNormalizer {
|
||||
pub(crate) fn from_context(context: &PyFormatContext<'_>) -> Self {
|
||||
Self {
|
||||
quoting: Quoting::default(),
|
||||
preferred_quote_style: QuoteStyle::default(),
|
||||
parent_docstring_quote_char: context.docstring(),
|
||||
normalize_hex: is_hex_codes_in_unicode_sequences_enabled(context),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_preferred_quote_style(mut self, quote_style: QuoteStyle) -> Self {
|
||||
self.preferred_quote_style = quote_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_quoting(mut self, quoting: Quoting) -> Self {
|
||||
self.quoting = quoting;
|
||||
self
|
||||
}
|
||||
|
||||
/// Computes the strings preferred quotes.
|
||||
pub(crate) fn choose_quotes(&self, string: &StringPart, locator: &Locator) -> StringQuotes {
|
||||
// Per PEP 8, always prefer double quotes for triple-quoted strings.
|
||||
// Except when using quote-style-preserve.
|
||||
let preferred_style = if string.quotes().triple {
|
||||
// ... unless we're formatting a code snippet inside a docstring,
|
||||
// then we specifically want to invert our quote style to avoid
|
||||
// writing out invalid Python.
|
||||
//
|
||||
// It's worth pointing out that we can actually wind up being
|
||||
// somewhat out of sync with PEP8 in this case. Consider this
|
||||
// example:
|
||||
//
|
||||
// def foo():
|
||||
// '''
|
||||
// Something.
|
||||
//
|
||||
// >>> """tricksy"""
|
||||
// '''
|
||||
// pass
|
||||
//
|
||||
// Ideally, this would be reformatted as:
|
||||
//
|
||||
// def foo():
|
||||
// """
|
||||
// Something.
|
||||
//
|
||||
// >>> '''tricksy'''
|
||||
// """
|
||||
// pass
|
||||
//
|
||||
// But the logic here results in the original quoting being
|
||||
// preserved. This is because the quoting style of the outer
|
||||
// docstring is determined, in part, by looking at its contents. In
|
||||
// this case, it notices that it contains a `"""` and thus infers
|
||||
// that using `'''` would overall read better because it avoids
|
||||
// the need to escape the interior `"""`. Except... in this case,
|
||||
// the `"""` is actually part of a code snippet that could get
|
||||
// reformatted to using a different quoting style itself.
|
||||
//
|
||||
// Fixing this would, I believe, require some fairly seismic
|
||||
// changes to how formatting strings works. Namely, we would need
|
||||
// to look for code snippets before normalizing the docstring, and
|
||||
// then figure out the quoting style more holistically by looking
|
||||
// at the various kinds of quotes used in the code snippets and
|
||||
// what reformatting them might look like.
|
||||
//
|
||||
// Overall this is a bit of a corner case and just inverting the
|
||||
// style from what the parent ultimately decided upon works, even
|
||||
// if it doesn't have perfect alignment with PEP8.
|
||||
if let Some(quote) = self.parent_docstring_quote_char {
|
||||
QuoteStyle::from(quote.invert())
|
||||
} else if self.preferred_quote_style.is_preserve() {
|
||||
QuoteStyle::Preserve
|
||||
} else {
|
||||
QuoteStyle::Double
|
||||
}
|
||||
} else {
|
||||
self.preferred_quote_style
|
||||
};
|
||||
|
||||
match self.quoting {
|
||||
Quoting::Preserve => string.quotes(),
|
||||
Quoting::CanChange => {
|
||||
if let Some(preferred_quote) = QuoteChar::from_style(preferred_style) {
|
||||
let raw_content = locator.slice(string.content_range());
|
||||
if string.prefix().is_raw_string() {
|
||||
choose_quotes_for_raw_string(raw_content, string.quotes(), preferred_quote)
|
||||
} else {
|
||||
choose_quotes_impl(raw_content, string.quotes(), preferred_quote)
|
||||
}
|
||||
} else {
|
||||
string.quotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the strings preferred quotes and normalizes its content.
|
||||
pub(crate) fn normalize<'a>(
|
||||
&self,
|
||||
string: &StringPart,
|
||||
locator: &'a Locator,
|
||||
) -> NormalizedString<'a> {
|
||||
let raw_content = locator.slice(string.content_range());
|
||||
|
||||
let quotes = self.choose_quotes(string, locator);
|
||||
|
||||
let normalized = normalize_string(raw_content, quotes, string.prefix(), self.normalize_hex);
|
||||
|
||||
NormalizedString {
|
||||
prefix: string.prefix(),
|
||||
content_range: string.content_range(),
|
||||
text: normalized,
|
||||
quotes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct NormalizedString<'a> {
|
||||
prefix: StringPrefix,
|
||||
|
||||
/// The quotes of the normalized string (preferred quotes)
|
||||
quotes: StringQuotes,
|
||||
|
||||
/// The range of the string's content in the source (minus prefix and quotes).
|
||||
content_range: TextRange,
|
||||
|
||||
/// The normalized text
|
||||
text: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Ranged for NormalizedString<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.content_range
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for NormalizedString<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
write!(f, [self.prefix, self.quotes])?;
|
||||
match &self.text {
|
||||
Cow::Borrowed(_) => {
|
||||
source_text_slice(self.range()).fmt(f)?;
|
||||
}
|
||||
Cow::Owned(normalized) => {
|
||||
text(normalized).fmt(f)?;
|
||||
}
|
||||
}
|
||||
self.quotes.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct StringPrefix: u8 {
|
||||
@@ -549,175 +184,6 @@ impl Format<PyFormatContext<'_>> for StringPrefix {
|
||||
}
|
||||
}
|
||||
|
||||
/// Choose the appropriate quote style for a raw string.
|
||||
///
|
||||
/// The preferred quote style is chosen unless the string contains unescaped quotes of the
|
||||
/// preferred style. For example, `r"foo"` is chosen over `r'foo'` if the preferred quote
|
||||
/// style is double quotes.
|
||||
fn choose_quotes_for_raw_string(
|
||||
input: &str,
|
||||
quotes: StringQuotes,
|
||||
preferred_quote: QuoteChar,
|
||||
) -> StringQuotes {
|
||||
let preferred_quote_char = preferred_quote.as_char();
|
||||
let mut chars = input.chars().peekable();
|
||||
let contains_unescaped_configured_quotes = loop {
|
||||
match chars.next() {
|
||||
Some('\\') => {
|
||||
// Ignore escaped characters
|
||||
chars.next();
|
||||
}
|
||||
// `"` or `'`
|
||||
Some(c) if c == preferred_quote_char => {
|
||||
if !quotes.triple {
|
||||
break true;
|
||||
}
|
||||
|
||||
match chars.peek() {
|
||||
// We can't turn `r'''\""'''` into `r"""\"""""`, this would confuse the parser
|
||||
// about where the closing triple quotes start
|
||||
None => break true,
|
||||
Some(next) if *next == preferred_quote_char => {
|
||||
// `""` or `''`
|
||||
chars.next();
|
||||
|
||||
// We can't turn `r'''""'''` into `r""""""""`, nor can we have
|
||||
// `"""` or `'''` respectively inside the string
|
||||
if chars.peek().is_none() || chars.peek() == Some(&preferred_quote_char) {
|
||||
break true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(_) => continue,
|
||||
None => break false,
|
||||
}
|
||||
};
|
||||
|
||||
StringQuotes {
|
||||
triple: quotes.triple,
|
||||
quote_char: if contains_unescaped_configured_quotes {
|
||||
quotes.quote_char
|
||||
} else {
|
||||
preferred_quote
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Choose the appropriate quote style for a string.
|
||||
///
|
||||
/// For single quoted strings, the preferred quote style is used, unless the alternative quote style
|
||||
/// would require fewer escapes.
|
||||
///
|
||||
/// For triple quoted strings, the preferred quote style is always used, unless the string contains
|
||||
/// a triplet of the quote character (e.g., if double quotes are preferred, double quotes will be
|
||||
/// used unless the string contains `"""`).
|
||||
fn choose_quotes_impl(
|
||||
input: &str,
|
||||
quotes: StringQuotes,
|
||||
preferred_quote: QuoteChar,
|
||||
) -> StringQuotes {
|
||||
let quote = if quotes.triple {
|
||||
// True if the string contains a triple quote sequence of the configured quote style.
|
||||
let mut uses_triple_quotes = false;
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
let preferred_quote_char = preferred_quote.as_char();
|
||||
match c {
|
||||
'\\' => {
|
||||
if matches!(chars.peek(), Some('"' | '\\')) {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
// `"` or `'`
|
||||
c if c == preferred_quote_char => {
|
||||
match chars.peek().copied() {
|
||||
Some(c) if c == preferred_quote_char => {
|
||||
// `""` or `''`
|
||||
chars.next();
|
||||
|
||||
match chars.peek().copied() {
|
||||
Some(c) if c == preferred_quote_char => {
|
||||
// `"""` or `'''`
|
||||
chars.next();
|
||||
uses_triple_quotes = true;
|
||||
break;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
// Handle `''' ""'''`. At this point we have consumed both
|
||||
// double quotes, so on the next iteration the iterator is empty
|
||||
// and we'd miss the string ending with a preferred quote
|
||||
uses_triple_quotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
// A single quote char, this is ok
|
||||
}
|
||||
None => {
|
||||
// Trailing quote at the end of the comment
|
||||
uses_triple_quotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
if uses_triple_quotes {
|
||||
// String contains a triple quote sequence of the configured quote style.
|
||||
// Keep the existing quote style.
|
||||
quotes.quote_char
|
||||
} else {
|
||||
preferred_quote
|
||||
}
|
||||
} else {
|
||||
let mut single_quotes = 0u32;
|
||||
let mut double_quotes = 0u32;
|
||||
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'\'' => {
|
||||
single_quotes += 1;
|
||||
}
|
||||
|
||||
'"' => {
|
||||
double_quotes += 1;
|
||||
}
|
||||
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
match preferred_quote {
|
||||
QuoteChar::Single => {
|
||||
if single_quotes > double_quotes {
|
||||
QuoteChar::Double
|
||||
} else {
|
||||
QuoteChar::Single
|
||||
}
|
||||
}
|
||||
QuoteChar::Double => {
|
||||
if double_quotes > single_quotes {
|
||||
QuoteChar::Single
|
||||
} else {
|
||||
QuoteChar::Double
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
StringQuotes {
|
||||
triple: quotes.triple,
|
||||
quote_char: quote,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct StringQuotes {
|
||||
triple: bool,
|
||||
@@ -821,269 +287,3 @@ impl TryFrom<char> for QuoteChar {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input`
|
||||
/// with the provided [`StringQuotes`] style.
|
||||
///
|
||||
/// Returns the normalized string and whether it contains new lines.
|
||||
pub(crate) fn normalize_string(
|
||||
input: &str,
|
||||
quotes: StringQuotes,
|
||||
prefix: StringPrefix,
|
||||
normalize_hex: bool,
|
||||
) -> Cow<str> {
|
||||
// The normalized string if `input` is not yet normalized.
|
||||
// `output` must remain empty if `input` is already normalized.
|
||||
let mut output = String::new();
|
||||
// Tracks the last index of `input` that has been written to `output`.
|
||||
// If `last_index` is `0` at the end, then the input is already normalized and can be returned as is.
|
||||
let mut last_index = 0;
|
||||
|
||||
let quote = quotes.quote_char;
|
||||
let preferred_quote = quote.as_char();
|
||||
let opposite_quote = quote.invert().as_char();
|
||||
|
||||
let mut chars = input.char_indices().peekable();
|
||||
|
||||
let is_raw = prefix.is_raw_string();
|
||||
let is_fstring = prefix.is_fstring();
|
||||
let mut formatted_value_nesting = 0u32;
|
||||
|
||||
while let Some((index, c)) = chars.next() {
|
||||
if is_fstring && matches!(c, '{' | '}') {
|
||||
if chars.peek().copied().is_some_and(|(_, next)| next == c) {
|
||||
// Skip over the second character of the double braces
|
||||
chars.next();
|
||||
} else if c == '{' {
|
||||
formatted_value_nesting += 1;
|
||||
} else {
|
||||
// Safe to assume that `c == '}'` here because of the matched pattern above
|
||||
formatted_value_nesting = formatted_value_nesting.saturating_sub(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if c == '\r' {
|
||||
output.push_str(&input[last_index..index]);
|
||||
|
||||
// Skip over the '\r' character, keep the `\n`
|
||||
if chars.peek().copied().is_some_and(|(_, next)| next == '\n') {
|
||||
chars.next();
|
||||
}
|
||||
// Replace the `\r` with a `\n`
|
||||
else {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
last_index = index + '\r'.len_utf8();
|
||||
} else if !is_raw {
|
||||
if c == '\\' {
|
||||
if let Some((_, next)) = chars.clone().next() {
|
||||
if next == '\\' {
|
||||
// Skip over escaped backslashes
|
||||
chars.next();
|
||||
} else if normalize_hex {
|
||||
if let Some(normalised) = UnicodeEscape::new(next, !prefix.is_byte())
|
||||
.and_then(|escape| {
|
||||
escape.normalize(&input[index + c.len_utf8() + next.len_utf8()..])
|
||||
})
|
||||
{
|
||||
// Length of the `\` plus the length of the escape sequence character (`u` | `U` | `x`)
|
||||
let escape_start_len = '\\'.len_utf8() + next.len_utf8();
|
||||
let escape_start_offset = index + escape_start_len;
|
||||
if let Cow::Owned(normalised) = &normalised {
|
||||
output.push_str(&input[last_index..escape_start_offset]);
|
||||
output.push_str(normalised);
|
||||
last_index = escape_start_offset + normalised.len();
|
||||
};
|
||||
|
||||
// Move the `chars` iterator passed the escape sequence.
|
||||
// Simply reassigning `chars` doesn't work because the indices` would
|
||||
// then be off.
|
||||
for _ in 0..next.len_utf8() + normalised.len() {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !quotes.triple {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if next == opposite_quote && formatted_value_nesting == 0 {
|
||||
// Remove the escape by ending before the backslash and starting again with the quote
|
||||
chars.next();
|
||||
output.push_str(&input[last_index..index]);
|
||||
last_index = index + '\\'.len_utf8();
|
||||
} else if next == preferred_quote {
|
||||
// Quote is already escaped, skip over it.
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !quotes.triple && c == preferred_quote && formatted_value_nesting == 0 {
|
||||
// Escape the quote
|
||||
output.push_str(&input[last_index..index]);
|
||||
output.push('\\');
|
||||
output.push(c);
|
||||
last_index = index + preferred_quote.len_utf8();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let normalized = if last_index == 0 {
|
||||
Cow::Borrowed(input)
|
||||
} else {
|
||||
output.push_str(&input[last_index..]);
|
||||
Cow::Owned(output)
|
||||
};
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum UnicodeEscape {
|
||||
/// A hex escape sequence of either 2 (`\x`), 4 (`\u`) or 8 (`\U`) hex characters.
|
||||
Hex(usize),
|
||||
|
||||
/// An escaped unicode name (`\N{name}`)
|
||||
CharacterName,
|
||||
}
|
||||
|
||||
impl UnicodeEscape {
|
||||
fn new(first: char, allow_unicode: bool) -> Option<UnicodeEscape> {
|
||||
Some(match first {
|
||||
'x' => UnicodeEscape::Hex(2),
|
||||
'u' if allow_unicode => UnicodeEscape::Hex(4),
|
||||
'U' if allow_unicode => UnicodeEscape::Hex(8),
|
||||
'N' if allow_unicode => UnicodeEscape::CharacterName,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Normalises `\u..`, `\U..`, `\x..` and `\N{..}` escape sequences to:
|
||||
///
|
||||
/// * `\u`, `\U'` and `\x`: To use lower case for the characters `a-f`.
|
||||
/// * `\N`: To use uppercase letters
|
||||
fn normalize(self, input: &str) -> Option<Cow<str>> {
|
||||
let mut normalised = String::new();
|
||||
|
||||
let len = match self {
|
||||
UnicodeEscape::Hex(len) => {
|
||||
// It's not a valid escape sequence if the input string has fewer characters
|
||||
// left than required by the escape sequence.
|
||||
if input.len() < len {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (index, c) in input.char_indices().take(len) {
|
||||
match c {
|
||||
'0'..='9' | 'a'..='f' => {
|
||||
if !normalised.is_empty() {
|
||||
normalised.push(c);
|
||||
}
|
||||
}
|
||||
'A'..='F' => {
|
||||
if normalised.is_empty() {
|
||||
normalised.reserve(len);
|
||||
normalised.push_str(&input[..index]);
|
||||
normalised.push(c.to_ascii_lowercase());
|
||||
} else {
|
||||
normalised.push(c.to_ascii_lowercase());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// not a valid escape sequence
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
len
|
||||
}
|
||||
UnicodeEscape::CharacterName => {
|
||||
let mut char_indices = input.char_indices();
|
||||
|
||||
if !matches!(char_indices.next(), Some((_, '{'))) {
|
||||
return None;
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some((index, c)) = char_indices.next() {
|
||||
match c {
|
||||
'}' => {
|
||||
if !normalised.is_empty() {
|
||||
normalised.push('}');
|
||||
}
|
||||
|
||||
// Name must be at least two characters long.
|
||||
if index < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
break index + '}'.len_utf8();
|
||||
}
|
||||
'0'..='9' | 'A'..='Z' | ' ' | '-' => {
|
||||
if !normalised.is_empty() {
|
||||
normalised.push(c);
|
||||
}
|
||||
}
|
||||
'a'..='z' => {
|
||||
if normalised.is_empty() {
|
||||
normalised.reserve(c.len_utf8() + '}'.len_utf8());
|
||||
normalised.push_str(&input[..index]);
|
||||
normalised.push(c.to_ascii_uppercase());
|
||||
} else {
|
||||
normalised.push(c.to_ascii_uppercase());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Seems like an invalid escape sequence, don't normalise it.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unterminated escape sequence, don't normalise it.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(if normalised.is_empty() {
|
||||
Cow::Borrowed(&input[..len])
|
||||
} else {
|
||||
Cow::Owned(normalised)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::string::{normalize_string, QuoteChar, StringPrefix, StringQuotes, UnicodeEscape};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[test]
|
||||
fn normalize_32_escape() {
|
||||
let escape_sequence = UnicodeEscape::new('U', true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(Cow::Owned("0001f60e".to_string())),
|
||||
escape_sequence.normalize("0001F60E")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_hex_in_byte_string() {
|
||||
let input = r"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
|
||||
|
||||
let normalized = normalize_string(
|
||||
input,
|
||||
StringQuotes {
|
||||
triple: false,
|
||||
quote_char: QuoteChar::Double,
|
||||
},
|
||||
StringPrefix::BYTE,
|
||||
true,
|
||||
);
|
||||
|
||||
assert_eq!(r"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", &normalized);
|
||||
}
|
||||
}
|
||||
|
||||
655
crates/ruff_python_formatter/src/string/normalize.rs
Normal file
655
crates/ruff_python_formatter/src/string/normalize.rs
Normal file
@@ -0,0 +1,655 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ruff_formatter::FormatContext;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::context::FStringState;
|
||||
use crate::options::PythonVersion;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_hex_codes_in_unicode_sequences_enabled;
|
||||
use crate::string::{QuoteChar, Quoting, StringPart, StringPrefix, StringQuotes};
|
||||
use crate::QuoteStyle;
|
||||
|
||||
pub(crate) struct StringNormalizer {
|
||||
quoting: Quoting,
|
||||
preferred_quote_style: QuoteStyle,
|
||||
parent_docstring_quote_char: Option<QuoteChar>,
|
||||
f_string_state: FStringState,
|
||||
target_version: PythonVersion,
|
||||
normalize_hex: bool,
|
||||
}
|
||||
|
||||
impl StringNormalizer {
|
||||
pub(crate) fn from_context(context: &PyFormatContext<'_>) -> Self {
|
||||
Self {
|
||||
quoting: Quoting::default(),
|
||||
preferred_quote_style: QuoteStyle::default(),
|
||||
parent_docstring_quote_char: context.docstring(),
|
||||
f_string_state: context.f_string_state(),
|
||||
target_version: context.options().target_version(),
|
||||
normalize_hex: is_hex_codes_in_unicode_sequences_enabled(context),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_preferred_quote_style(mut self, quote_style: QuoteStyle) -> Self {
|
||||
self.preferred_quote_style = quote_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_quoting(mut self, quoting: Quoting) -> Self {
|
||||
self.quoting = quoting;
|
||||
self
|
||||
}
|
||||
|
||||
/// Computes the strings preferred quotes.
|
||||
pub(crate) fn choose_quotes(&self, string: &StringPart, locator: &Locator) -> StringQuotes {
|
||||
// Per PEP 8, always prefer double quotes for triple-quoted strings.
|
||||
// Except when using quote-style-preserve.
|
||||
let preferred_style = if string.quotes().triple {
|
||||
// ... unless we're formatting a code snippet inside a docstring,
|
||||
// then we specifically want to invert our quote style to avoid
|
||||
// writing out invalid Python.
|
||||
//
|
||||
// It's worth pointing out that we can actually wind up being
|
||||
// somewhat out of sync with PEP8 in this case. Consider this
|
||||
// example:
|
||||
//
|
||||
// def foo():
|
||||
// '''
|
||||
// Something.
|
||||
//
|
||||
// >>> """tricksy"""
|
||||
// '''
|
||||
// pass
|
||||
//
|
||||
// Ideally, this would be reformatted as:
|
||||
//
|
||||
// def foo():
|
||||
// """
|
||||
// Something.
|
||||
//
|
||||
// >>> '''tricksy'''
|
||||
// """
|
||||
// pass
|
||||
//
|
||||
// But the logic here results in the original quoting being
|
||||
// preserved. This is because the quoting style of the outer
|
||||
// docstring is determined, in part, by looking at its contents. In
|
||||
// this case, it notices that it contains a `"""` and thus infers
|
||||
// that using `'''` would overall read better because it avoids
|
||||
// the need to escape the interior `"""`. Except... in this case,
|
||||
// the `"""` is actually part of a code snippet that could get
|
||||
// reformatted to using a different quoting style itself.
|
||||
//
|
||||
// Fixing this would, I believe, require some fairly seismic
|
||||
// changes to how formatting strings works. Namely, we would need
|
||||
// to look for code snippets before normalizing the docstring, and
|
||||
// then figure out the quoting style more holistically by looking
|
||||
// at the various kinds of quotes used in the code snippets and
|
||||
// what reformatting them might look like.
|
||||
//
|
||||
// Overall this is a bit of a corner case and just inverting the
|
||||
// style from what the parent ultimately decided upon works, even
|
||||
// if it doesn't have perfect alignment with PEP8.
|
||||
if let Some(quote) = self.parent_docstring_quote_char {
|
||||
QuoteStyle::from(quote.invert())
|
||||
} else if self.preferred_quote_style.is_preserve() {
|
||||
QuoteStyle::Preserve
|
||||
} else {
|
||||
QuoteStyle::Double
|
||||
}
|
||||
} else {
|
||||
self.preferred_quote_style
|
||||
};
|
||||
|
||||
let quoting = if let FStringState::InsideExpressionElement(context) = self.f_string_state {
|
||||
// If we're inside an f-string, we need to make sure to preserve the
|
||||
// existing quotes unless we're inside a triple-quoted f-string and
|
||||
// the inner string itself isn't triple-quoted. For example:
|
||||
//
|
||||
// ```python
|
||||
// f"""outer {"inner"}""" # Valid
|
||||
// f"""outer {"""inner"""}""" # Invalid
|
||||
// ```
|
||||
//
|
||||
// Or, if the target version supports PEP 701.
|
||||
//
|
||||
// The reason to preserve the quotes is based on the assumption that
|
||||
// the original f-string is valid in terms of quoting, and we don't
|
||||
// want to change that to make it invalid.
|
||||
if (context.quotes().is_triple() && !string.quotes().is_triple())
|
||||
|| self.target_version.supports_pep_701()
|
||||
{
|
||||
self.quoting
|
||||
} else {
|
||||
Quoting::Preserve
|
||||
}
|
||||
} else {
|
||||
self.quoting
|
||||
};
|
||||
|
||||
match quoting {
|
||||
Quoting::Preserve => string.quotes(),
|
||||
Quoting::CanChange => {
|
||||
if let Some(preferred_quote) = QuoteChar::from_style(preferred_style) {
|
||||
let raw_content = locator.slice(string.content_range());
|
||||
if string.prefix().is_raw_string() {
|
||||
choose_quotes_for_raw_string(raw_content, string.quotes(), preferred_quote)
|
||||
} else {
|
||||
choose_quotes_impl(raw_content, string.quotes(), preferred_quote)
|
||||
}
|
||||
} else {
|
||||
string.quotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the strings preferred quotes and normalizes its content.
|
||||
pub(crate) fn normalize<'a>(
|
||||
&self,
|
||||
string: &StringPart,
|
||||
locator: &'a Locator,
|
||||
) -> NormalizedString<'a> {
|
||||
let raw_content = locator.slice(string.content_range());
|
||||
|
||||
let quotes = self.choose_quotes(string, locator);
|
||||
|
||||
let normalized = normalize_string(raw_content, quotes, string.prefix(), self.normalize_hex);
|
||||
|
||||
NormalizedString {
|
||||
prefix: string.prefix(),
|
||||
content_range: string.content_range(),
|
||||
text: normalized,
|
||||
quotes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct NormalizedString<'a> {
|
||||
prefix: crate::string::StringPrefix,
|
||||
|
||||
/// The quotes of the normalized string (preferred quotes)
|
||||
quotes: StringQuotes,
|
||||
|
||||
/// The range of the string's content in the source (minus prefix and quotes).
|
||||
content_range: TextRange,
|
||||
|
||||
/// The normalized text
|
||||
text: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> NormalizedString<'a> {
|
||||
pub(crate) fn text(&self) -> &Cow<'a, str> {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub(crate) fn quotes(&self) -> StringQuotes {
|
||||
self.quotes
|
||||
}
|
||||
|
||||
pub(crate) fn prefix(&self) -> StringPrefix {
|
||||
self.prefix
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for NormalizedString<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.content_range
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for NormalizedString<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
ruff_formatter::write!(f, [self.prefix, self.quotes])?;
|
||||
match &self.text {
|
||||
Cow::Borrowed(_) => {
|
||||
source_text_slice(self.range()).fmt(f)?;
|
||||
}
|
||||
Cow::Owned(normalized) => {
|
||||
text(normalized).fmt(f)?;
|
||||
}
|
||||
}
|
||||
self.quotes.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Choose the appropriate quote style for a raw string.
|
||||
///
|
||||
/// The preferred quote style is chosen unless the string contains unescaped quotes of the
|
||||
/// preferred style. For example, `r"foo"` is chosen over `r'foo'` if the preferred quote
|
||||
/// style is double quotes.
|
||||
fn choose_quotes_for_raw_string(
|
||||
input: &str,
|
||||
quotes: StringQuotes,
|
||||
preferred_quote: QuoteChar,
|
||||
) -> StringQuotes {
|
||||
let preferred_quote_char = preferred_quote.as_char();
|
||||
let mut chars = input.chars().peekable();
|
||||
let contains_unescaped_configured_quotes = loop {
|
||||
match chars.next() {
|
||||
Some('\\') => {
|
||||
// Ignore escaped characters
|
||||
chars.next();
|
||||
}
|
||||
// `"` or `'`
|
||||
Some(c) if c == preferred_quote_char => {
|
||||
if !quotes.triple {
|
||||
break true;
|
||||
}
|
||||
|
||||
match chars.peek() {
|
||||
// We can't turn `r'''\""'''` into `r"""\"""""`, this would confuse the parser
|
||||
// about where the closing triple quotes start
|
||||
None => break true,
|
||||
Some(next) if *next == preferred_quote_char => {
|
||||
// `""` or `''`
|
||||
chars.next();
|
||||
|
||||
// We can't turn `r'''""'''` into `r""""""""`, nor can we have
|
||||
// `"""` or `'''` respectively inside the string
|
||||
if chars.peek().is_none() || chars.peek() == Some(&preferred_quote_char) {
|
||||
break true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(_) => continue,
|
||||
None => break false,
|
||||
}
|
||||
};
|
||||
|
||||
StringQuotes {
|
||||
triple: quotes.triple,
|
||||
quote_char: if contains_unescaped_configured_quotes {
|
||||
quotes.quote_char
|
||||
} else {
|
||||
preferred_quote
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Choose the appropriate quote style for a string.
|
||||
///
|
||||
/// For single quoted strings, the preferred quote style is used, unless the alternative quote style
|
||||
/// would require fewer escapes.
|
||||
///
|
||||
/// For triple quoted strings, the preferred quote style is always used, unless the string contains
|
||||
/// a triplet of the quote character (e.g., if double quotes are preferred, double quotes will be
|
||||
/// used unless the string contains `"""`).
|
||||
fn choose_quotes_impl(
|
||||
input: &str,
|
||||
quotes: StringQuotes,
|
||||
preferred_quote: QuoteChar,
|
||||
) -> StringQuotes {
|
||||
let quote = if quotes.triple {
|
||||
// True if the string contains a triple quote sequence of the configured quote style.
|
||||
let mut uses_triple_quotes = false;
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
let preferred_quote_char = preferred_quote.as_char();
|
||||
match c {
|
||||
'\\' => {
|
||||
if matches!(chars.peek(), Some('"' | '\\')) {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
// `"` or `'`
|
||||
c if c == preferred_quote_char => {
|
||||
match chars.peek().copied() {
|
||||
Some(c) if c == preferred_quote_char => {
|
||||
// `""` or `''`
|
||||
chars.next();
|
||||
|
||||
match chars.peek().copied() {
|
||||
Some(c) if c == preferred_quote_char => {
|
||||
// `"""` or `'''`
|
||||
chars.next();
|
||||
uses_triple_quotes = true;
|
||||
break;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
// Handle `''' ""'''`. At this point we have consumed both
|
||||
// double quotes, so on the next iteration the iterator is empty
|
||||
// and we'd miss the string ending with a preferred quote
|
||||
uses_triple_quotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
// A single quote char, this is ok
|
||||
}
|
||||
None => {
|
||||
// Trailing quote at the end of the comment
|
||||
uses_triple_quotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
if uses_triple_quotes {
|
||||
// String contains a triple quote sequence of the configured quote style.
|
||||
// Keep the existing quote style.
|
||||
quotes.quote_char
|
||||
} else {
|
||||
preferred_quote
|
||||
}
|
||||
} else {
|
||||
let mut single_quotes = 0u32;
|
||||
let mut double_quotes = 0u32;
|
||||
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'\'' => {
|
||||
single_quotes += 1;
|
||||
}
|
||||
|
||||
'"' => {
|
||||
double_quotes += 1;
|
||||
}
|
||||
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
match preferred_quote {
|
||||
QuoteChar::Single => {
|
||||
if single_quotes > double_quotes {
|
||||
QuoteChar::Double
|
||||
} else {
|
||||
QuoteChar::Single
|
||||
}
|
||||
}
|
||||
QuoteChar::Double => {
|
||||
if double_quotes > single_quotes {
|
||||
QuoteChar::Single
|
||||
} else {
|
||||
QuoteChar::Double
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
StringQuotes {
|
||||
triple: quotes.triple,
|
||||
quote_char: quote,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the necessary quote escapes and removes unnecessary escape sequences when quoting `input`
|
||||
/// with the provided [`StringQuotes`] style.
|
||||
///
|
||||
/// Returns the normalized string and whether it contains new lines.
|
||||
pub(crate) fn normalize_string(
|
||||
input: &str,
|
||||
quotes: StringQuotes,
|
||||
prefix: StringPrefix,
|
||||
normalize_hex: bool,
|
||||
) -> Cow<str> {
|
||||
// The normalized string if `input` is not yet normalized.
|
||||
// `output` must remain empty if `input` is already normalized.
|
||||
let mut output = String::new();
|
||||
// Tracks the last index of `input` that has been written to `output`.
|
||||
// If `last_index` is `0` at the end, then the input is already normalized and can be returned as is.
|
||||
let mut last_index = 0;
|
||||
|
||||
let quote = quotes.quote_char;
|
||||
let preferred_quote = quote.as_char();
|
||||
let opposite_quote = quote.invert().as_char();
|
||||
|
||||
let mut chars = input.char_indices().peekable();
|
||||
|
||||
let is_raw = prefix.is_raw_string();
|
||||
let is_fstring = prefix.is_fstring();
|
||||
let mut formatted_value_nesting = 0u32;
|
||||
|
||||
while let Some((index, c)) = chars.next() {
|
||||
if is_fstring && matches!(c, '{' | '}') {
|
||||
if chars.peek().copied().is_some_and(|(_, next)| next == c) {
|
||||
// Skip over the second character of the double braces
|
||||
chars.next();
|
||||
} else if c == '{' {
|
||||
formatted_value_nesting += 1;
|
||||
} else {
|
||||
// Safe to assume that `c == '}'` here because of the matched pattern above
|
||||
formatted_value_nesting = formatted_value_nesting.saturating_sub(1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if c == '\r' {
|
||||
output.push_str(&input[last_index..index]);
|
||||
|
||||
// Skip over the '\r' character, keep the `\n`
|
||||
if chars.peek().copied().is_some_and(|(_, next)| next == '\n') {
|
||||
chars.next();
|
||||
}
|
||||
// Replace the `\r` with a `\n`
|
||||
else {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
last_index = index + '\r'.len_utf8();
|
||||
} else if !is_raw {
|
||||
if c == '\\' {
|
||||
if let Some((_, next)) = chars.clone().next() {
|
||||
if next == '\\' {
|
||||
// Skip over escaped backslashes
|
||||
chars.next();
|
||||
} else if normalize_hex {
|
||||
if let Some(normalised) = UnicodeEscape::new(next, !prefix.is_byte())
|
||||
.and_then(|escape| {
|
||||
escape.normalize(&input[index + c.len_utf8() + next.len_utf8()..])
|
||||
})
|
||||
{
|
||||
// Length of the `\` plus the length of the escape sequence character (`u` | `U` | `x`)
|
||||
let escape_start_len = '\\'.len_utf8() + next.len_utf8();
|
||||
let escape_start_offset = index + escape_start_len;
|
||||
if let Cow::Owned(normalised) = &normalised {
|
||||
output.push_str(&input[last_index..escape_start_offset]);
|
||||
output.push_str(normalised);
|
||||
last_index = escape_start_offset + normalised.len();
|
||||
};
|
||||
|
||||
// Move the `chars` iterator passed the escape sequence.
|
||||
// Simply reassigning `chars` doesn't work because the indices` would
|
||||
// then be off.
|
||||
for _ in 0..next.len_utf8() + normalised.len() {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !quotes.triple {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if next == opposite_quote && formatted_value_nesting == 0 {
|
||||
// Remove the escape by ending before the backslash and starting again with the quote
|
||||
chars.next();
|
||||
output.push_str(&input[last_index..index]);
|
||||
last_index = index + '\\'.len_utf8();
|
||||
} else if next == preferred_quote {
|
||||
// Quote is already escaped, skip over it.
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !quotes.triple && c == preferred_quote && formatted_value_nesting == 0 {
|
||||
// Escape the quote
|
||||
output.push_str(&input[last_index..index]);
|
||||
output.push('\\');
|
||||
output.push(c);
|
||||
last_index = index + preferred_quote.len_utf8();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let normalized = if last_index == 0 {
|
||||
Cow::Borrowed(input)
|
||||
} else {
|
||||
output.push_str(&input[last_index..]);
|
||||
Cow::Owned(output)
|
||||
};
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum UnicodeEscape {
|
||||
/// A hex escape sequence of either 2 (`\x`), 4 (`\u`) or 8 (`\U`) hex characters.
|
||||
Hex(usize),
|
||||
|
||||
/// An escaped unicode name (`\N{name}`)
|
||||
CharacterName,
|
||||
}
|
||||
|
||||
impl UnicodeEscape {
|
||||
fn new(first: char, allow_unicode: bool) -> Option<UnicodeEscape> {
|
||||
Some(match first {
|
||||
'x' => UnicodeEscape::Hex(2),
|
||||
'u' if allow_unicode => UnicodeEscape::Hex(4),
|
||||
'U' if allow_unicode => UnicodeEscape::Hex(8),
|
||||
'N' if allow_unicode => UnicodeEscape::CharacterName,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Normalises `\u..`, `\U..`, `\x..` and `\N{..}` escape sequences to:
|
||||
///
|
||||
/// * `\u`, `\U'` and `\x`: To use lower case for the characters `a-f`.
|
||||
/// * `\N`: To use uppercase letters
|
||||
fn normalize(self, input: &str) -> Option<Cow<str>> {
|
||||
let mut normalised = String::new();
|
||||
|
||||
let len = match self {
|
||||
UnicodeEscape::Hex(len) => {
|
||||
// It's not a valid escape sequence if the input string has fewer characters
|
||||
// left than required by the escape sequence.
|
||||
if input.len() < len {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (index, c) in input.char_indices().take(len) {
|
||||
match c {
|
||||
'0'..='9' | 'a'..='f' => {
|
||||
if !normalised.is_empty() {
|
||||
normalised.push(c);
|
||||
}
|
||||
}
|
||||
'A'..='F' => {
|
||||
if normalised.is_empty() {
|
||||
normalised.reserve(len);
|
||||
normalised.push_str(&input[..index]);
|
||||
normalised.push(c.to_ascii_lowercase());
|
||||
} else {
|
||||
normalised.push(c.to_ascii_lowercase());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// not a valid escape sequence
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
len
|
||||
}
|
||||
UnicodeEscape::CharacterName => {
|
||||
let mut char_indices = input.char_indices();
|
||||
|
||||
if !matches!(char_indices.next(), Some((_, '{'))) {
|
||||
return None;
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some((index, c)) = char_indices.next() {
|
||||
match c {
|
||||
'}' => {
|
||||
if !normalised.is_empty() {
|
||||
normalised.push('}');
|
||||
}
|
||||
|
||||
// Name must be at least two characters long.
|
||||
if index < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
break index + '}'.len_utf8();
|
||||
}
|
||||
'0'..='9' | 'A'..='Z' | ' ' | '-' => {
|
||||
if !normalised.is_empty() {
|
||||
normalised.push(c);
|
||||
}
|
||||
}
|
||||
'a'..='z' => {
|
||||
if normalised.is_empty() {
|
||||
normalised.reserve(c.len_utf8() + '}'.len_utf8());
|
||||
normalised.push_str(&input[..index]);
|
||||
normalised.push(c.to_ascii_uppercase());
|
||||
} else {
|
||||
normalised.push(c.to_ascii_uppercase());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Seems like an invalid escape sequence, don't normalise it.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Unterminated escape sequence, don't normalise it.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(if normalised.is_empty() {
|
||||
Cow::Borrowed(&input[..len])
|
||||
} else {
|
||||
Cow::Owned(normalised)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::string::{QuoteChar, StringPrefix, StringQuotes};
|
||||
|
||||
use super::{normalize_string, UnicodeEscape};
|
||||
|
||||
#[test]
|
||||
fn normalize_32_escape() {
|
||||
let escape_sequence = UnicodeEscape::new('U', true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(Cow::Owned("0001f60e".to_string())),
|
||||
escape_sequence.normalize("0001F60E")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_hex_in_byte_string() {
|
||||
let input = r"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
|
||||
|
||||
let normalized = normalize_string(
|
||||
input,
|
||||
StringQuotes {
|
||||
triple: false,
|
||||
quote_char: QuoteChar::Double,
|
||||
},
|
||||
StringPrefix::BYTE,
|
||||
true,
|
||||
);
|
||||
|
||||
assert_eq!(r"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", &normalized);
|
||||
}
|
||||
}
|
||||
@@ -873,11 +873,11 @@ impl Ranged for LogicalLine {
|
||||
}
|
||||
}
|
||||
|
||||
struct VerbatimText {
|
||||
pub(crate) struct VerbatimText {
|
||||
verbatim_range: TextRange,
|
||||
}
|
||||
|
||||
fn verbatim_text<T>(item: T) -> VerbatimText
|
||||
pub(crate) fn verbatim_text<T>(item: T) -> VerbatimText
|
||||
where
|
||||
T: Ranged,
|
||||
{
|
||||
|
||||
@@ -902,7 +902,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||
)
|
||||
|
||||
dict_with_lambda_values = {
|
||||
@@ -524,61 +383,54 @@
|
||||
@@ -524,65 +383,58 @@
|
||||
|
||||
# Complex string concatenations with a method call in the middle.
|
||||
code = (
|
||||
@@ -941,7 +941,7 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||
log.info(
|
||||
- "Skipping:"
|
||||
- f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}"
|
||||
+ f'Skipping: {desc["db_id"]} {foo("bar",x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
|
||||
+ f'Skipping: {desc["db_id"]} {foo("bar", x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
|
||||
)
|
||||
|
||||
log.info(
|
||||
@@ -981,6 +981,18 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share
|
||||
)
|
||||
|
||||
log.info(
|
||||
- f"""Skipping: {"a" == 'b'} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}"""
|
||||
+ f"""Skipping: {"a" == "b"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}"""
|
||||
)
|
||||
|
||||
log.info(
|
||||
@@ -590,5 +442,5 @@
|
||||
)
|
||||
|
||||
log.info(
|
||||
- f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"""
|
||||
+ f"""Skipping: {"a" == "b"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}"""
|
||||
)
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
@@ -1394,7 +1406,7 @@ log.info(
|
||||
)
|
||||
|
||||
log.info(
|
||||
f'Skipping: {desc["db_id"]} {foo("bar",x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
|
||||
f'Skipping: {desc["db_id"]} {foo("bar", x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
|
||||
)
|
||||
|
||||
log.info(
|
||||
@@ -1422,7 +1434,7 @@ log.info(
|
||||
)
|
||||
|
||||
log.info(
|
||||
f"""Skipping: {"a" == 'b'} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}"""
|
||||
f"""Skipping: {"a" == "b"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}"""
|
||||
)
|
||||
|
||||
log.info(
|
||||
@@ -1430,7 +1442,7 @@ log.info(
|
||||
)
|
||||
|
||||
log.info(
|
||||
f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"""
|
||||
f"""Skipping: {"a" == "b"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}"""
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -832,7 +832,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||
|
||||
some_commented_string = ( # This comment stays at the top.
|
||||
"This string is long but not so long that it needs hahahah toooooo be so greatttt"
|
||||
@@ -279,36 +280,25 @@
|
||||
@@ -279,37 +280,26 @@
|
||||
)
|
||||
|
||||
lpar_and_rpar_have_comments = func_call( # LPAR Comment
|
||||
@@ -852,31 +852,32 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||
- f" {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||
-)
|
||||
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||
+
|
||||
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||
|
||||
-cmd_fstring = (
|
||||
- "sudo -E deluge-console info --detailed --sort-reverse=time_added"
|
||||
- f" {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||
-)
|
||||
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'"
|
||||
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {{'' if ID is None else ID}} | perl -nE 'print if /^{field}:/'"
|
||||
|
||||
-cmd_fstring = (
|
||||
- "sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is"
|
||||
- f" None else ID}} | perl -nE 'print if /^{field}:/'"
|
||||
-)
|
||||
+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {{'' if ID is None else ID}} | perl -nE 'print if /^{field}:/'"
|
||||
|
||||
+fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}."
|
||||
+
|
||||
|
||||
fstring = (
|
||||
- "This string really doesn't need to be an {{fstring}}, but this one most"
|
||||
- f" certainly, absolutely {does}."
|
||||
+ f"We have to remember to escape {braces}." " Like {these}." f" But not {this}."
|
||||
)
|
||||
-
|
||||
-fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}."
|
||||
|
||||
-fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}."
|
||||
-
|
||||
|
||||
class A:
|
||||
class B:
|
||||
@@ -364,10 +354,7 @@
|
||||
def foo():
|
||||
if not hasattr(module, name):
|
||||
@@ -979,7 +980,13 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||
)
|
||||
|
||||
# The parens should NOT be removed in this case.
|
||||
@@ -518,88 +494,78 @@
|
||||
@@ -513,93 +489,83 @@
|
||||
|
||||
|
||||
temp_msg = (
|
||||
- f"{f'{humanize_number(pos)}.': <{pound_len+2}} "
|
||||
+ f"{f'{humanize_number(pos)}.': <{pound_len + 2}} "
|
||||
f"{balance: <{bal_len + 5}} "
|
||||
f"<<{author.display_name}>>\n"
|
||||
)
|
||||
|
||||
@@ -1103,7 +1110,13 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry:
|
||||
"6. Click on Create Credential at the top."
|
||||
'7. At the top click the link for "API key".'
|
||||
"8. No application restrictions are needed. Click Create at the bottom."
|
||||
@@ -613,55 +579,40 @@
|
||||
@@ -608,60 +574,45 @@
|
||||
|
||||
# It shouldn't matter if the string prefixes are capitalized.
|
||||
temp_msg = (
|
||||
- f"{F'{humanize_number(pos)}.': <{pound_len+2}} "
|
||||
+ f"{f'{humanize_number(pos)}.': <{pound_len + 2}} "
|
||||
f"{balance: <{bal_len + 5}} "
|
||||
f"<<{author.display_name}>>\n"
|
||||
)
|
||||
|
||||
@@ -1688,7 +1701,7 @@ class X:
|
||||
|
||||
|
||||
temp_msg = (
|
||||
f"{f'{humanize_number(pos)}.': <{pound_len+2}} "
|
||||
f"{f'{humanize_number(pos)}.': <{pound_len + 2}} "
|
||||
f"{balance: <{bal_len + 5}} "
|
||||
f"<<{author.display_name}>>\n"
|
||||
)
|
||||
@@ -1773,7 +1786,7 @@ message = (
|
||||
|
||||
# It shouldn't matter if the string prefixes are capitalized.
|
||||
temp_msg = (
|
||||
f"{F'{humanize_number(pos)}.': <{pound_len+2}} "
|
||||
f"{f'{humanize_number(pos)}.': <{pound_len + 2}} "
|
||||
f"{balance: <{bal_len + 5}} "
|
||||
f"<<{author.display_name}>>\n"
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring_py312.py
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
# This file contains test cases only for cases where the logic tests for whether
|
||||
# the target version is 3.12 or later. A user can have 3.12 syntax even if the target
|
||||
# version isn't set.
|
||||
|
||||
# Quotes re-use
|
||||
f"{'a'}"
|
||||
```
|
||||
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = Py312
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
# This file contains test cases only for cases where the logic tests for whether
|
||||
# the target version is 3.12 or later. A user can have 3.12 syntax even if the target
|
||||
# version isn't set.
|
||||
|
||||
# Quotes re-use
|
||||
f"{'a'}"
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -3,4 +3,4 @@
|
||||
# version isn't set.
|
||||
|
||||
# Quotes re-use
|
||||
-f"{'a'}"
|
||||
+f"{"a"}"
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -14,7 +14,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.2.1
|
||||
rev: v0.2.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -27,7 +27,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.2.1
|
||||
rev: v0.2.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -41,7 +41,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.2.1
|
||||
rev: v0.2.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ruff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "scripts"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = ""
|
||||
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user