mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-09 21:28:04 +00:00

Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
As of [this cpython PR](https://github.com/python/cpython/pull/135996), it is not allowed to concatenate t-strings with non-t-strings, implicitly or explicitly. Expressions such as `"foo" t"{bar}"` are now syntax errors. This PR updates some AST nodes and parsing to reflect this change. The structural change is that `TStringPart` is no longer needed, since, as in the case of `BytesStringLiteral`, the only possibilities are that we have a single `TString` or a vector of such (representing an implicit concatenation of t-strings). This removes a level of nesting from many AST expressions (which is what all the snapshot changes reflect), and simplifies some logic in the implementation of visitors, for example. The other change of note is in the parser. When we meet an implicit concatenation of string-like literals, we now count the number of t-string literals. If these do not exhaust the total number of implicitly concatenated pieces, then we emit a syntax error. To recover from this syntax error, we encode any t-string pieces as _invalid_ string literals (which means we flag them as invalid, record their range, and record the value as `""`). Note that if at least one of the pieces is an f-string we prefer to parse the entire string as an f-string; otherwise we parse it as a string. This logic is exactly the same as how we currently treat `BytesStringLiteral` parsing and error recovery - and carries with it the same pros and cons. Finally, note that I have not implemented any changes in the implementation of the formatter. As far as I can tell, none are needed. I did change a few of the fixtures so that we are always concatenating t-strings with t-strings.
2055 lines
67 KiB
Rust
2055 lines
67 KiB
Rust
//! Generate Python source code from an abstract syntax tree (AST).
|
|
|
|
use std::fmt::Write;
|
|
use std::ops::Deref;
|
|
|
|
use ruff_python_ast::{
|
|
self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp,
|
|
Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator,
|
|
Parameter, Parameters, Pattern, Singleton, Stmt, StringFlags, Suite, TypeParam,
|
|
TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem,
|
|
};
|
|
use ruff_python_ast::{ParameterWithDefault, TypeParams};
|
|
use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape};
|
|
use ruff_source_file::LineEnding;
|
|
|
|
use super::stylist::{Indentation, Stylist};
|
|
|
|
mod precedence {
|
|
pub(crate) const NAMED_EXPR: u8 = 1;
|
|
pub(crate) const ASSIGN: u8 = 3;
|
|
pub(crate) const ANN_ASSIGN: u8 = 5;
|
|
pub(crate) const AUG_ASSIGN: u8 = 5;
|
|
pub(crate) const EXPR: u8 = 5;
|
|
pub(crate) const YIELD: u8 = 7;
|
|
pub(crate) const YIELD_FROM: u8 = 7;
|
|
pub(crate) const IF: u8 = 9;
|
|
pub(crate) const FOR: u8 = 9;
|
|
pub(crate) const WHILE: u8 = 9;
|
|
pub(crate) const RETURN: u8 = 11;
|
|
pub(crate) const SLICE: u8 = 13;
|
|
pub(crate) const SUBSCRIPT: u8 = 13;
|
|
pub(crate) const COMPREHENSION_TARGET: u8 = 19;
|
|
pub(crate) const TUPLE: u8 = 19;
|
|
pub(crate) const FORMATTED_VALUE: u8 = 19;
|
|
pub(crate) const COMMA: u8 = 21;
|
|
pub(crate) const ASSERT: u8 = 23;
|
|
pub(crate) const COMPREHENSION_ELEMENT: u8 = 27;
|
|
pub(crate) const LAMBDA: u8 = 27;
|
|
pub(crate) const IF_EXP: u8 = 27;
|
|
pub(crate) const COMPREHENSION: u8 = 29;
|
|
pub(crate) const OR: u8 = 31;
|
|
pub(crate) const AND: u8 = 33;
|
|
pub(crate) const NOT: u8 = 35;
|
|
pub(crate) const CMP: u8 = 37;
|
|
pub(crate) const BIT_OR: u8 = 39;
|
|
pub(crate) const BIT_XOR: u8 = 41;
|
|
pub(crate) const BIT_AND: u8 = 43;
|
|
pub(crate) const LSHIFT: u8 = 45;
|
|
pub(crate) const RSHIFT: u8 = 45;
|
|
pub(crate) const ADD: u8 = 47;
|
|
pub(crate) const SUB: u8 = 47;
|
|
pub(crate) const MULT: u8 = 49;
|
|
pub(crate) const DIV: u8 = 49;
|
|
pub(crate) const MOD: u8 = 49;
|
|
pub(crate) const FLOORDIV: u8 = 49;
|
|
pub(crate) const MAT_MULT: u8 = 49;
|
|
pub(crate) const INVERT: u8 = 53;
|
|
pub(crate) const UADD: u8 = 53;
|
|
pub(crate) const USUB: u8 = 53;
|
|
pub(crate) const POW: u8 = 55;
|
|
pub(crate) const AWAIT: u8 = 57;
|
|
pub(crate) const MAX: u8 = 63;
|
|
}
|
|
|
|
pub struct Generator<'a> {
|
|
/// The indentation style to use.
|
|
indent: &'a Indentation,
|
|
/// The line ending to use.
|
|
line_ending: LineEnding,
|
|
buffer: String,
|
|
indent_depth: usize,
|
|
num_newlines: usize,
|
|
initial: bool,
|
|
}
|
|
|
|
impl<'a> From<&'a Stylist<'a>> for Generator<'a> {
|
|
fn from(stylist: &'a Stylist<'a>) -> Self {
|
|
Self {
|
|
indent: stylist.indentation(),
|
|
line_ending: stylist.line_ending(),
|
|
buffer: String::new(),
|
|
indent_depth: 0,
|
|
num_newlines: 0,
|
|
initial: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Generator<'a> {
|
|
pub const fn new(indent: &'a Indentation, line_ending: LineEnding) -> Self {
|
|
Self {
|
|
// Style preferences.
|
|
indent,
|
|
line_ending,
|
|
// Internal state.
|
|
buffer: String::new(),
|
|
indent_depth: 0,
|
|
num_newlines: 0,
|
|
initial: true,
|
|
}
|
|
}
|
|
|
|
/// Generate source code from a [`Stmt`].
|
|
pub fn stmt(mut self, stmt: &Stmt) -> String {
|
|
self.unparse_stmt(stmt);
|
|
self.generate()
|
|
}
|
|
|
|
/// Generate source code from an [`Expr`].
|
|
pub fn expr(mut self, expr: &Expr) -> String {
|
|
self.unparse_expr(expr, 0);
|
|
self.generate()
|
|
}
|
|
|
|
fn newline(&mut self) {
|
|
if !self.initial {
|
|
self.num_newlines = std::cmp::max(self.num_newlines, 1);
|
|
}
|
|
}
|
|
|
|
fn newlines(&mut self, extra: usize) {
|
|
if !self.initial {
|
|
self.num_newlines = std::cmp::max(self.num_newlines, 1 + extra);
|
|
}
|
|
}
|
|
|
|
fn body(&mut self, stmts: &[Stmt]) {
|
|
self.indent_depth = self.indent_depth.saturating_add(1);
|
|
for stmt in stmts {
|
|
self.unparse_stmt(stmt);
|
|
}
|
|
self.indent_depth = self.indent_depth.saturating_sub(1);
|
|
}
|
|
|
|
fn p(&mut self, s: &str) {
|
|
if self.num_newlines > 0 {
|
|
for _ in 0..self.num_newlines {
|
|
self.buffer += &self.line_ending;
|
|
}
|
|
self.num_newlines = 0;
|
|
}
|
|
self.buffer += s;
|
|
}
|
|
|
|
fn p_id(&mut self, s: &Identifier) {
|
|
self.p(s.as_str());
|
|
}
|
|
|
|
fn p_bytes_repr(&mut self, s: &[u8], flags: BytesLiteralFlags) {
|
|
// raw bytes are interpreted without escapes and should all be ascii (it's a python syntax
|
|
// error otherwise), but if this assumption is violated, a `Utf8Error` will be returned from
|
|
// `p_raw_bytes`, and we should fall back on the normal escaping behavior instead of
|
|
// panicking
|
|
if flags.prefix().is_raw() {
|
|
if let Ok(s) = std::str::from_utf8(s) {
|
|
write!(self.buffer, "{}", flags.display_contents(s))
|
|
.expect("Writing to a String buffer should never fail");
|
|
return;
|
|
}
|
|
}
|
|
let escape = AsciiEscape::with_preferred_quote(s, flags.quote_style());
|
|
if let Some(len) = escape.layout().len {
|
|
self.buffer.reserve(len);
|
|
}
|
|
escape
|
|
.bytes_repr(flags.triple_quotes())
|
|
.write(&mut self.buffer)
|
|
.expect("Writing to a String buffer should never fail");
|
|
}
|
|
|
|
fn p_str_repr(&mut self, s: &str, flags: impl Into<AnyStringFlags>) {
|
|
let flags = flags.into();
|
|
if flags.prefix().is_raw() {
|
|
write!(self.buffer, "{}", flags.display_contents(s))
|
|
.expect("Writing to a String buffer should never fail");
|
|
return;
|
|
}
|
|
self.p(flags.prefix().as_str());
|
|
let escape = UnicodeEscape::with_preferred_quote(s, flags.quote_style());
|
|
if let Some(len) = escape.layout().len {
|
|
self.buffer.reserve(len);
|
|
}
|
|
escape
|
|
.str_repr(flags.triple_quotes())
|
|
.write(&mut self.buffer)
|
|
.expect("Writing to a String buffer should never fail");
|
|
}
|
|
|
|
fn p_if(&mut self, cond: bool, s: &str) {
|
|
if cond {
|
|
self.p(s);
|
|
}
|
|
}
|
|
|
|
fn p_delim(&mut self, first: &mut bool, s: &str) {
|
|
self.p_if(!std::mem::take(first), s);
|
|
}
|
|
|
|
pub(crate) fn generate(self) -> String {
|
|
self.buffer
|
|
}
|
|
|
|
pub fn unparse_suite(&mut self, suite: &Suite) {
|
|
for stmt in suite {
|
|
self.unparse_stmt(stmt);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn unparse_stmt(&mut self, ast: &Stmt) {
|
|
macro_rules! statement {
|
|
($body:block) => {{
|
|
self.newline();
|
|
self.p(&self.indent.deref().repeat(self.indent_depth));
|
|
$body
|
|
self.initial = false;
|
|
}};
|
|
}
|
|
|
|
match ast {
|
|
Stmt::FunctionDef(ast::StmtFunctionDef {
|
|
is_async,
|
|
name,
|
|
parameters,
|
|
body,
|
|
returns,
|
|
decorator_list,
|
|
type_params,
|
|
..
|
|
}) => {
|
|
self.newlines(if self.indent_depth == 0 { 2 } else { 1 });
|
|
for decorator in decorator_list {
|
|
statement!({
|
|
self.p("@");
|
|
self.unparse_expr(&decorator.expression, precedence::MAX);
|
|
});
|
|
}
|
|
statement!({
|
|
if *is_async {
|
|
self.p("async ");
|
|
}
|
|
self.p("def ");
|
|
self.p_id(name);
|
|
if let Some(type_params) = type_params {
|
|
self.unparse_type_params(type_params);
|
|
}
|
|
self.p("(");
|
|
self.unparse_parameters(parameters);
|
|
self.p(")");
|
|
if let Some(returns) = returns {
|
|
self.p(" -> ");
|
|
self.unparse_expr(returns, precedence::MAX);
|
|
}
|
|
self.p(":");
|
|
});
|
|
self.body(body);
|
|
if self.indent_depth == 0 {
|
|
self.newlines(2);
|
|
}
|
|
}
|
|
Stmt::ClassDef(ast::StmtClassDef {
|
|
name,
|
|
arguments,
|
|
body,
|
|
decorator_list,
|
|
type_params,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.newlines(if self.indent_depth == 0 { 2 } else { 1 });
|
|
for decorator in decorator_list {
|
|
statement!({
|
|
self.p("@");
|
|
self.unparse_expr(&decorator.expression, precedence::MAX);
|
|
});
|
|
}
|
|
statement!({
|
|
self.p("class ");
|
|
self.p_id(name);
|
|
if let Some(type_params) = type_params {
|
|
self.unparse_type_params(type_params);
|
|
}
|
|
if let Some(arguments) = arguments {
|
|
self.p("(");
|
|
let mut first = true;
|
|
for arg_or_keyword in arguments.arguments_source_order() {
|
|
match arg_or_keyword {
|
|
ArgOrKeyword::Arg(arg) => {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_expr(arg, precedence::MAX);
|
|
}
|
|
ArgOrKeyword::Keyword(keyword) => {
|
|
self.p_delim(&mut first, ", ");
|
|
if let Some(arg) = &keyword.arg {
|
|
self.p_id(arg);
|
|
self.p("=");
|
|
} else {
|
|
self.p("**");
|
|
}
|
|
self.unparse_expr(&keyword.value, precedence::MAX);
|
|
}
|
|
}
|
|
}
|
|
self.p(")");
|
|
}
|
|
self.p(":");
|
|
});
|
|
self.body(body);
|
|
if self.indent_depth == 0 {
|
|
self.newlines(2);
|
|
}
|
|
}
|
|
Stmt::Return(ast::StmtReturn {
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
if let Some(expr) = value {
|
|
self.p("return ");
|
|
self.unparse_expr(expr, precedence::RETURN);
|
|
} else {
|
|
self.p("return");
|
|
}
|
|
});
|
|
}
|
|
Stmt::Delete(ast::StmtDelete {
|
|
targets,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("del ");
|
|
let mut first = true;
|
|
for expr in targets {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_expr(expr, precedence::COMMA);
|
|
}
|
|
});
|
|
}
|
|
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
|
|
statement!({
|
|
for target in targets {
|
|
self.unparse_expr(target, precedence::ASSIGN);
|
|
self.p(" = ");
|
|
}
|
|
self.unparse_expr(value, precedence::ASSIGN);
|
|
});
|
|
}
|
|
Stmt::AugAssign(ast::StmtAugAssign {
|
|
target,
|
|
op,
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.unparse_expr(target, precedence::AUG_ASSIGN);
|
|
self.p(" ");
|
|
self.p(match op {
|
|
Operator::Add => "+",
|
|
Operator::Sub => "-",
|
|
Operator::Mult => "*",
|
|
Operator::MatMult => "@",
|
|
Operator::Div => "/",
|
|
Operator::Mod => "%",
|
|
Operator::Pow => "**",
|
|
Operator::LShift => "<<",
|
|
Operator::RShift => ">>",
|
|
Operator::BitOr => "|",
|
|
Operator::BitXor => "^",
|
|
Operator::BitAnd => "&",
|
|
Operator::FloorDiv => "//",
|
|
});
|
|
self.p("= ");
|
|
self.unparse_expr(value, precedence::AUG_ASSIGN);
|
|
});
|
|
}
|
|
Stmt::AnnAssign(ast::StmtAnnAssign {
|
|
target,
|
|
annotation,
|
|
value,
|
|
simple,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
let need_parens = matches!(target.as_ref(), Expr::Name(_)) && !simple;
|
|
self.p_if(need_parens, "(");
|
|
self.unparse_expr(target, precedence::ANN_ASSIGN);
|
|
self.p_if(need_parens, ")");
|
|
self.p(": ");
|
|
self.unparse_expr(annotation, precedence::COMMA);
|
|
if let Some(value) = value {
|
|
self.p(" = ");
|
|
self.unparse_expr(value, precedence::COMMA);
|
|
}
|
|
});
|
|
}
|
|
Stmt::For(ast::StmtFor {
|
|
is_async,
|
|
target,
|
|
iter,
|
|
body,
|
|
orelse,
|
|
..
|
|
}) => {
|
|
statement!({
|
|
if *is_async {
|
|
self.p("async ");
|
|
}
|
|
self.p("for ");
|
|
self.unparse_expr(target, precedence::FOR);
|
|
self.p(" in ");
|
|
self.unparse_expr(iter, precedence::MAX);
|
|
self.p(":");
|
|
});
|
|
self.body(body);
|
|
if !orelse.is_empty() {
|
|
statement!({
|
|
self.p("else:");
|
|
});
|
|
self.body(orelse);
|
|
}
|
|
}
|
|
Stmt::While(ast::StmtWhile {
|
|
test,
|
|
body,
|
|
orelse,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("while ");
|
|
self.unparse_expr(test, precedence::WHILE);
|
|
self.p(":");
|
|
});
|
|
self.body(body);
|
|
if !orelse.is_empty() {
|
|
statement!({
|
|
self.p("else:");
|
|
});
|
|
self.body(orelse);
|
|
}
|
|
}
|
|
Stmt::If(ast::StmtIf {
|
|
test,
|
|
body,
|
|
elif_else_clauses,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("if ");
|
|
self.unparse_expr(test, precedence::IF);
|
|
self.p(":");
|
|
});
|
|
self.body(body);
|
|
|
|
for clause in elif_else_clauses {
|
|
if let Some(test) = &clause.test {
|
|
statement!({
|
|
self.p("elif ");
|
|
self.unparse_expr(test, precedence::IF);
|
|
self.p(":");
|
|
});
|
|
} else {
|
|
statement!({
|
|
self.p("else:");
|
|
});
|
|
}
|
|
self.body(&clause.body);
|
|
}
|
|
}
|
|
Stmt::With(ast::StmtWith {
|
|
is_async,
|
|
items,
|
|
body,
|
|
..
|
|
}) => {
|
|
statement!({
|
|
if *is_async {
|
|
self.p("async ");
|
|
}
|
|
self.p("with ");
|
|
let mut first = true;
|
|
for item in items {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_with_item(item);
|
|
}
|
|
self.p(":");
|
|
});
|
|
self.body(body);
|
|
}
|
|
Stmt::Match(ast::StmtMatch {
|
|
subject,
|
|
cases,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("match ");
|
|
self.unparse_expr(subject, precedence::MAX);
|
|
self.p(":");
|
|
});
|
|
for case in cases {
|
|
self.indent_depth = self.indent_depth.saturating_add(1);
|
|
statement!({
|
|
self.unparse_match_case(case);
|
|
});
|
|
self.indent_depth = self.indent_depth.saturating_sub(1);
|
|
}
|
|
}
|
|
Stmt::TypeAlias(ast::StmtTypeAlias {
|
|
name,
|
|
range: _,
|
|
node_index: _,
|
|
type_params,
|
|
value,
|
|
}) => {
|
|
statement!({
|
|
self.p("type ");
|
|
self.unparse_expr(name, precedence::MAX);
|
|
if let Some(type_params) = type_params {
|
|
self.unparse_type_params(type_params);
|
|
}
|
|
self.p(" = ");
|
|
self.unparse_expr(value, precedence::ASSIGN);
|
|
});
|
|
}
|
|
Stmt::Raise(ast::StmtRaise {
|
|
exc,
|
|
cause,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("raise");
|
|
if let Some(exc) = exc {
|
|
self.p(" ");
|
|
self.unparse_expr(exc, precedence::MAX);
|
|
}
|
|
if let Some(cause) = cause {
|
|
self.p(" from ");
|
|
self.unparse_expr(cause, precedence::MAX);
|
|
}
|
|
});
|
|
}
|
|
Stmt::Try(ast::StmtTry {
|
|
body,
|
|
handlers,
|
|
orelse,
|
|
finalbody,
|
|
is_star,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("try:");
|
|
});
|
|
self.body(body);
|
|
|
|
for handler in handlers {
|
|
statement!({
|
|
self.unparse_except_handler(handler, *is_star);
|
|
});
|
|
}
|
|
|
|
if !orelse.is_empty() {
|
|
statement!({
|
|
self.p("else:");
|
|
});
|
|
self.body(orelse);
|
|
}
|
|
if !finalbody.is_empty() {
|
|
statement!({
|
|
self.p("finally:");
|
|
});
|
|
self.body(finalbody);
|
|
}
|
|
}
|
|
Stmt::Assert(ast::StmtAssert {
|
|
test,
|
|
msg,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("assert ");
|
|
self.unparse_expr(test, precedence::ASSERT);
|
|
if let Some(msg) = msg {
|
|
self.p(", ");
|
|
self.unparse_expr(msg, precedence::ASSERT);
|
|
}
|
|
});
|
|
}
|
|
Stmt::Import(ast::StmtImport {
|
|
names,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("import ");
|
|
let mut first = true;
|
|
for alias in names {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_alias(alias);
|
|
}
|
|
});
|
|
}
|
|
Stmt::ImportFrom(ast::StmtImportFrom {
|
|
module,
|
|
names,
|
|
level,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("from ");
|
|
if *level > 0 {
|
|
for _ in 0..*level {
|
|
self.p(".");
|
|
}
|
|
}
|
|
if let Some(module) = module {
|
|
self.p_id(module);
|
|
}
|
|
self.p(" import ");
|
|
let mut first = true;
|
|
for alias in names {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_alias(alias);
|
|
}
|
|
});
|
|
}
|
|
Stmt::Global(ast::StmtGlobal {
|
|
names,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("global ");
|
|
let mut first = true;
|
|
for name in names {
|
|
self.p_delim(&mut first, ", ");
|
|
self.p_id(name);
|
|
}
|
|
});
|
|
}
|
|
Stmt::Nonlocal(ast::StmtNonlocal {
|
|
names,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.p("nonlocal ");
|
|
let mut first = true;
|
|
for name in names {
|
|
self.p_delim(&mut first, ", ");
|
|
self.p_id(name);
|
|
}
|
|
});
|
|
}
|
|
Stmt::Expr(ast::StmtExpr {
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
statement!({
|
|
self.unparse_expr(value, precedence::EXPR);
|
|
});
|
|
}
|
|
Stmt::Pass(_) => {
|
|
statement!({
|
|
self.p("pass");
|
|
});
|
|
}
|
|
Stmt::Break(_) => {
|
|
statement!({
|
|
self.p("break");
|
|
});
|
|
}
|
|
Stmt::Continue(_) => {
|
|
statement!({
|
|
self.p("continue");
|
|
});
|
|
}
|
|
Stmt::IpyEscapeCommand(ast::StmtIpyEscapeCommand { kind, value, .. }) => {
|
|
statement!({
|
|
self.p(&format!("{kind}{value}"));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unparse_except_handler(&mut self, ast: &ExceptHandler, star: bool) {
|
|
match ast {
|
|
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
|
|
type_,
|
|
name,
|
|
body,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.p("except");
|
|
if star {
|
|
self.p("*");
|
|
}
|
|
if let Some(type_) = type_ {
|
|
self.p(" ");
|
|
self.unparse_expr(type_, precedence::MAX);
|
|
}
|
|
if let Some(name) = name {
|
|
self.p(" as ");
|
|
self.p_id(name);
|
|
}
|
|
self.p(":");
|
|
self.body(body);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unparse_pattern(&mut self, ast: &Pattern) {
|
|
match ast {
|
|
Pattern::MatchValue(ast::PatternMatchValue {
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.unparse_expr(value, precedence::MAX);
|
|
}
|
|
Pattern::MatchSingleton(ast::PatternMatchSingleton {
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.unparse_singleton(*value);
|
|
}
|
|
Pattern::MatchSequence(ast::PatternMatchSequence {
|
|
patterns,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.p("[");
|
|
let mut first = true;
|
|
for pattern in patterns {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_pattern(pattern);
|
|
}
|
|
self.p("]");
|
|
}
|
|
Pattern::MatchMapping(ast::PatternMatchMapping {
|
|
keys,
|
|
patterns,
|
|
rest,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.p("{");
|
|
let mut first = true;
|
|
for (key, pattern) in keys.iter().zip(patterns) {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_expr(key, precedence::MAX);
|
|
self.p(": ");
|
|
self.unparse_pattern(pattern);
|
|
}
|
|
if let Some(rest) = rest {
|
|
self.p_delim(&mut first, ", ");
|
|
self.p("**");
|
|
self.p_id(rest);
|
|
}
|
|
self.p("}");
|
|
}
|
|
Pattern::MatchClass(_) => {}
|
|
Pattern::MatchStar(ast::PatternMatchStar {
|
|
name,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.p("*");
|
|
if let Some(name) = name {
|
|
self.p_id(name);
|
|
} else {
|
|
self.p("_");
|
|
}
|
|
}
|
|
Pattern::MatchAs(ast::PatternMatchAs {
|
|
pattern,
|
|
name,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
if let Some(pattern) = pattern {
|
|
self.unparse_pattern(pattern);
|
|
self.p(" as ");
|
|
}
|
|
if let Some(name) = name {
|
|
self.p_id(name);
|
|
} else {
|
|
self.p("_");
|
|
}
|
|
}
|
|
Pattern::MatchOr(ast::PatternMatchOr {
|
|
patterns,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
let mut first = true;
|
|
for pattern in patterns {
|
|
self.p_delim(&mut first, " | ");
|
|
self.unparse_pattern(pattern);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unparse_match_case(&mut self, ast: &MatchCase) {
|
|
self.p("case ");
|
|
self.unparse_pattern(&ast.pattern);
|
|
if let Some(guard) = &ast.guard {
|
|
self.p(" if ");
|
|
self.unparse_expr(guard, precedence::MAX);
|
|
}
|
|
self.p(":");
|
|
self.body(&ast.body);
|
|
}
|
|
|
|
fn unparse_type_params(&mut self, type_params: &TypeParams) {
|
|
self.p("[");
|
|
let mut first = true;
|
|
for type_param in type_params.iter() {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_type_param(type_param);
|
|
}
|
|
self.p("]");
|
|
}
|
|
|
|
pub(crate) fn unparse_type_param(&mut self, ast: &TypeParam) {
|
|
match ast {
|
|
TypeParam::TypeVar(TypeParamTypeVar {
|
|
name,
|
|
bound,
|
|
default,
|
|
..
|
|
}) => {
|
|
self.p_id(name);
|
|
if let Some(expr) = bound {
|
|
self.p(": ");
|
|
self.unparse_expr(expr, precedence::MAX);
|
|
}
|
|
if let Some(expr) = default {
|
|
self.p(" = ");
|
|
self.unparse_expr(expr, precedence::MAX);
|
|
}
|
|
}
|
|
TypeParam::TypeVarTuple(TypeParamTypeVarTuple { name, default, .. }) => {
|
|
self.p("*");
|
|
self.p_id(name);
|
|
if let Some(expr) = default {
|
|
self.p(" = ");
|
|
self.unparse_expr(expr, precedence::MAX);
|
|
}
|
|
}
|
|
TypeParam::ParamSpec(TypeParamParamSpec { name, default, .. }) => {
|
|
self.p("**");
|
|
self.p_id(name);
|
|
if let Some(expr) = default {
|
|
self.p(" = ");
|
|
self.unparse_expr(expr, precedence::MAX);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn unparse_expr(&mut self, ast: &Expr, level: u8) {
|
|
macro_rules! opprec {
|
|
($opty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => {
|
|
match $x {
|
|
$(<$enu>::$var => (opprec!(@space $opty, $op), precedence::$prec),)*
|
|
}
|
|
};
|
|
(@space bin, $op:literal) => {
|
|
concat!(" ", $op, " ")
|
|
};
|
|
(@space un, $op:literal) => {
|
|
$op
|
|
};
|
|
}
|
|
macro_rules! group_if {
|
|
($lvl:expr, $body:block) => {{
|
|
let group = level > $lvl;
|
|
self.p_if(group, "(");
|
|
let ret = $body;
|
|
self.p_if(group, ")");
|
|
ret
|
|
}};
|
|
}
|
|
match ast {
|
|
Expr::BoolOp(ast::ExprBoolOp {
|
|
op,
|
|
values,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
let (op, prec) = opprec!(bin, op, BoolOp, And("and", AND), Or("or", OR));
|
|
group_if!(prec, {
|
|
let mut first = true;
|
|
for val in values {
|
|
self.p_delim(&mut first, op);
|
|
self.unparse_expr(val, prec + 1);
|
|
}
|
|
});
|
|
}
|
|
Expr::Named(ast::ExprNamed {
|
|
target,
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
group_if!(precedence::NAMED_EXPR, {
|
|
self.unparse_expr(target, precedence::NAMED_EXPR);
|
|
self.p(" := ");
|
|
self.unparse_expr(value, precedence::NAMED_EXPR + 1);
|
|
});
|
|
}
|
|
Expr::BinOp(ast::ExprBinOp {
|
|
left,
|
|
op,
|
|
right,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
let rassoc = matches!(op, Operator::Pow);
|
|
let (op, prec) = opprec!(
|
|
bin,
|
|
op,
|
|
Operator,
|
|
Add("+", ADD),
|
|
Sub("-", SUB),
|
|
Mult("*", MULT),
|
|
MatMult("@", MAT_MULT),
|
|
Div("/", DIV),
|
|
Mod("%", MOD),
|
|
Pow("**", POW),
|
|
LShift("<<", LSHIFT),
|
|
RShift(">>", RSHIFT),
|
|
BitOr("|", BIT_OR),
|
|
BitXor("^", BIT_XOR),
|
|
BitAnd("&", BIT_AND),
|
|
FloorDiv("//", FLOORDIV),
|
|
);
|
|
group_if!(prec, {
|
|
self.unparse_expr(left, prec + u8::from(rassoc));
|
|
self.p(op);
|
|
self.unparse_expr(right, prec + u8::from(!rassoc));
|
|
});
|
|
}
|
|
Expr::UnaryOp(ast::ExprUnaryOp {
|
|
op,
|
|
operand,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
let (op, prec) = opprec!(
|
|
un,
|
|
op,
|
|
ruff_python_ast::UnaryOp,
|
|
Invert("~", INVERT),
|
|
Not("not ", NOT),
|
|
UAdd("+", UADD),
|
|
USub("-", USUB)
|
|
);
|
|
group_if!(prec, {
|
|
self.p(op);
|
|
self.unparse_expr(operand, prec);
|
|
});
|
|
}
|
|
Expr::Lambda(ast::ExprLambda {
|
|
parameters,
|
|
body,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
group_if!(precedence::LAMBDA, {
|
|
self.p("lambda");
|
|
if let Some(parameters) = parameters {
|
|
self.p(" ");
|
|
self.unparse_parameters(parameters);
|
|
}
|
|
self.p(": ");
|
|
self.unparse_expr(body, precedence::LAMBDA);
|
|
});
|
|
}
|
|
Expr::If(ast::ExprIf {
|
|
test,
|
|
body,
|
|
orelse,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
group_if!(precedence::IF_EXP, {
|
|
self.unparse_expr(body, precedence::IF_EXP + 1);
|
|
self.p(" if ");
|
|
self.unparse_expr(test, precedence::IF_EXP + 1);
|
|
self.p(" else ");
|
|
self.unparse_expr(orelse, precedence::IF_EXP);
|
|
});
|
|
}
|
|
Expr::Dict(dict) => {
|
|
self.p("{");
|
|
let mut first = true;
|
|
for ast::DictItem { key, value } in dict {
|
|
self.p_delim(&mut first, ", ");
|
|
if let Some(key) = key {
|
|
self.unparse_expr(key, precedence::COMMA);
|
|
self.p(": ");
|
|
self.unparse_expr(value, precedence::COMMA);
|
|
} else {
|
|
self.p("**");
|
|
self.unparse_expr(value, precedence::MAX);
|
|
}
|
|
}
|
|
self.p("}");
|
|
}
|
|
Expr::Set(set) => {
|
|
if set.is_empty() {
|
|
self.p("set()");
|
|
} else {
|
|
self.p("{");
|
|
let mut first = true;
|
|
for item in set {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_expr(item, precedence::COMMA);
|
|
}
|
|
self.p("}");
|
|
}
|
|
}
|
|
Expr::ListComp(ast::ExprListComp {
|
|
elt,
|
|
generators,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.p("[");
|
|
self.unparse_expr(elt, precedence::COMPREHENSION_ELEMENT);
|
|
self.unparse_comp(generators);
|
|
self.p("]");
|
|
}
|
|
Expr::SetComp(ast::ExprSetComp {
|
|
elt,
|
|
generators,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.p("{");
|
|
self.unparse_expr(elt, precedence::COMPREHENSION_ELEMENT);
|
|
self.unparse_comp(generators);
|
|
self.p("}");
|
|
}
|
|
Expr::DictComp(ast::ExprDictComp {
|
|
key,
|
|
value,
|
|
generators,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.p("{");
|
|
self.unparse_expr(key, precedence::COMPREHENSION_ELEMENT);
|
|
self.p(": ");
|
|
self.unparse_expr(value, precedence::COMPREHENSION_ELEMENT);
|
|
self.unparse_comp(generators);
|
|
self.p("}");
|
|
}
|
|
Expr::Generator(ast::ExprGenerator {
|
|
elt,
|
|
generators,
|
|
parenthesized: _,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.p("(");
|
|
self.unparse_expr(elt, precedence::COMPREHENSION_ELEMENT);
|
|
self.unparse_comp(generators);
|
|
self.p(")");
|
|
}
|
|
Expr::Await(ast::ExprAwait {
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
group_if!(precedence::AWAIT, {
|
|
self.p("await ");
|
|
self.unparse_expr(value, precedence::MAX);
|
|
});
|
|
}
|
|
Expr::Yield(ast::ExprYield {
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
group_if!(precedence::YIELD, {
|
|
self.p("yield");
|
|
if let Some(value) = value {
|
|
self.p(" ");
|
|
self.unparse_expr(value, precedence::YIELD + 1);
|
|
}
|
|
});
|
|
}
|
|
Expr::YieldFrom(ast::ExprYieldFrom {
|
|
value,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
group_if!(precedence::YIELD_FROM, {
|
|
self.p("yield from ");
|
|
self.unparse_expr(value, precedence::MAX);
|
|
});
|
|
}
|
|
Expr::Compare(ast::ExprCompare {
|
|
left,
|
|
ops,
|
|
comparators,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
group_if!(precedence::CMP, {
|
|
let new_lvl = precedence::CMP + 1;
|
|
self.unparse_expr(left, new_lvl);
|
|
for (op, cmp) in ops.iter().zip(comparators) {
|
|
let op = match op {
|
|
CmpOp::Eq => " == ",
|
|
CmpOp::NotEq => " != ",
|
|
CmpOp::Lt => " < ",
|
|
CmpOp::LtE => " <= ",
|
|
CmpOp::Gt => " > ",
|
|
CmpOp::GtE => " >= ",
|
|
CmpOp::Is => " is ",
|
|
CmpOp::IsNot => " is not ",
|
|
CmpOp::In => " in ",
|
|
CmpOp::NotIn => " not in ",
|
|
};
|
|
self.p(op);
|
|
self.unparse_expr(cmp, new_lvl);
|
|
}
|
|
});
|
|
}
|
|
Expr::Call(ast::ExprCall {
|
|
func,
|
|
arguments,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
self.unparse_expr(func, precedence::MAX);
|
|
self.p("(");
|
|
if let (
|
|
[
|
|
Expr::Generator(ast::ExprGenerator {
|
|
elt,
|
|
generators,
|
|
range: _,
|
|
node_index: _,
|
|
parenthesized: _,
|
|
}),
|
|
],
|
|
[],
|
|
) = (arguments.args.as_ref(), arguments.keywords.as_ref())
|
|
{
|
|
// Ensure that a single generator doesn't get double-parenthesized.
|
|
self.unparse_expr(elt, precedence::COMMA);
|
|
self.unparse_comp(generators);
|
|
} else {
|
|
let mut first = true;
|
|
|
|
for arg_or_keyword in arguments.arguments_source_order() {
|
|
match arg_or_keyword {
|
|
ArgOrKeyword::Arg(arg) => {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_expr(arg, precedence::COMMA);
|
|
}
|
|
ArgOrKeyword::Keyword(keyword) => {
|
|
self.p_delim(&mut first, ", ");
|
|
if let Some(arg) = &keyword.arg {
|
|
self.p_id(arg);
|
|
self.p("=");
|
|
self.unparse_expr(&keyword.value, precedence::COMMA);
|
|
} else {
|
|
self.p("**");
|
|
self.unparse_expr(&keyword.value, precedence::MAX);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.p(")");
|
|
}
|
|
Expr::FString(ast::ExprFString { value, .. }) => {
|
|
self.unparse_f_string_value(value);
|
|
}
|
|
Expr::TString(ast::ExprTString { value, .. }) => {
|
|
self.unparse_t_string_value(value);
|
|
}
|
|
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
|
self.unparse_string_literal_value(value);
|
|
}
|
|
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => {
|
|
let mut first = true;
|
|
for bytes_literal in value {
|
|
self.p_delim(&mut first, " ");
|
|
self.p_bytes_repr(&bytes_literal.value, bytes_literal.flags);
|
|
}
|
|
}
|
|
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
|
|
static INF_STR: &str = "1e309";
|
|
assert_eq!(f64::MAX_10_EXP, 308);
|
|
|
|
match value {
|
|
ast::Number::Int(i) => {
|
|
self.p(&format!("{i}"));
|
|
}
|
|
ast::Number::Float(fp) => {
|
|
if fp.is_infinite() {
|
|
self.p(INF_STR);
|
|
} else {
|
|
self.p(&ruff_python_literal::float::to_string(*fp));
|
|
}
|
|
}
|
|
ast::Number::Complex { real, imag } => {
|
|
let value = if *real == 0.0 {
|
|
format!("{imag}j")
|
|
} else {
|
|
format!("({real}{imag:+}j)")
|
|
};
|
|
if real.is_infinite() || imag.is_infinite() {
|
|
self.p(&value.replace("inf", INF_STR));
|
|
} else {
|
|
self.p(&value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => {
|
|
self.p(if *value { "True" } else { "False" });
|
|
}
|
|
Expr::NoneLiteral(_) => {
|
|
self.p("None");
|
|
}
|
|
Expr::EllipsisLiteral(_) => {
|
|
self.p("...");
|
|
}
|
|
Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
|
|
if let Expr::NumberLiteral(ast::ExprNumberLiteral {
|
|
value: ast::Number::Int(_),
|
|
..
|
|
}) = value.as_ref()
|
|
{
|
|
self.p("(");
|
|
self.unparse_expr(value, precedence::MAX);
|
|
self.p(").");
|
|
} else {
|
|
self.unparse_expr(value, precedence::MAX);
|
|
self.p(".");
|
|
}
|
|
self.p_id(attr);
|
|
}
|
|
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
|
self.unparse_expr(value, precedence::MAX);
|
|
self.p("[");
|
|
self.unparse_expr(slice, precedence::SUBSCRIPT);
|
|
self.p("]");
|
|
}
|
|
Expr::Starred(ast::ExprStarred { value, .. }) => {
|
|
self.p("*");
|
|
self.unparse_expr(value, precedence::MAX);
|
|
}
|
|
Expr::Name(ast::ExprName { id, .. }) => self.p(id.as_str()),
|
|
Expr::List(list) => {
|
|
self.p("[");
|
|
let mut first = true;
|
|
for item in list {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_expr(item, precedence::COMMA);
|
|
}
|
|
self.p("]");
|
|
}
|
|
Expr::Tuple(tuple) => {
|
|
if tuple.is_empty() {
|
|
self.p("()");
|
|
} else {
|
|
group_if!(precedence::TUPLE, {
|
|
let mut first = true;
|
|
for item in tuple {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_expr(item, precedence::COMMA);
|
|
}
|
|
self.p_if(tuple.len() == 1, ",");
|
|
});
|
|
}
|
|
}
|
|
Expr::Slice(ast::ExprSlice {
|
|
lower,
|
|
upper,
|
|
step,
|
|
range: _,
|
|
node_index: _,
|
|
}) => {
|
|
if let Some(lower) = lower {
|
|
self.unparse_expr(lower, precedence::SLICE);
|
|
}
|
|
self.p(":");
|
|
if let Some(upper) = upper {
|
|
self.unparse_expr(upper, precedence::SLICE);
|
|
}
|
|
if let Some(step) = step {
|
|
self.p(":");
|
|
self.unparse_expr(step, precedence::SLICE);
|
|
}
|
|
}
|
|
Expr::IpyEscapeCommand(ast::ExprIpyEscapeCommand { kind, value, .. }) => {
|
|
self.p(&format!("{kind}{value}"));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn unparse_singleton(&mut self, singleton: Singleton) {
|
|
match singleton {
|
|
Singleton::None => self.p("None"),
|
|
Singleton::True => self.p("True"),
|
|
Singleton::False => self.p("False"),
|
|
}
|
|
}
|
|
|
|
fn unparse_parameters(&mut self, parameters: &Parameters) {
|
|
let mut first = true;
|
|
for (i, parameter_with_default) in parameters
|
|
.posonlyargs
|
|
.iter()
|
|
.chain(¶meters.args)
|
|
.enumerate()
|
|
{
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_parameter_with_default(parameter_with_default);
|
|
self.p_if(i + 1 == parameters.posonlyargs.len(), ", /");
|
|
}
|
|
if parameters.vararg.is_some() || !parameters.kwonlyargs.is_empty() {
|
|
self.p_delim(&mut first, ", ");
|
|
self.p("*");
|
|
}
|
|
if let Some(vararg) = ¶meters.vararg {
|
|
self.unparse_parameter(vararg);
|
|
}
|
|
for kwarg in ¶meters.kwonlyargs {
|
|
self.p_delim(&mut first, ", ");
|
|
self.unparse_parameter_with_default(kwarg);
|
|
}
|
|
if let Some(kwarg) = ¶meters.kwarg {
|
|
self.p_delim(&mut first, ", ");
|
|
self.p("**");
|
|
self.unparse_parameter(kwarg);
|
|
}
|
|
}
|
|
|
|
fn unparse_parameter(&mut self, parameter: &Parameter) {
|
|
self.p_id(¶meter.name);
|
|
if let Some(ann) = ¶meter.annotation {
|
|
self.p(": ");
|
|
self.unparse_expr(ann, precedence::COMMA);
|
|
}
|
|
}
|
|
|
|
fn unparse_parameter_with_default(&mut self, parameter_with_default: &ParameterWithDefault) {
|
|
self.unparse_parameter(¶meter_with_default.parameter);
|
|
if let Some(default) = ¶meter_with_default.default {
|
|
self.p("=");
|
|
self.unparse_expr(default, precedence::COMMA);
|
|
}
|
|
}
|
|
|
|
fn unparse_comp(&mut self, generators: &[Comprehension]) {
|
|
for comp in generators {
|
|
self.p(if comp.is_async {
|
|
" async for "
|
|
} else {
|
|
" for "
|
|
});
|
|
self.unparse_expr(&comp.target, precedence::COMPREHENSION_TARGET);
|
|
self.p(" in ");
|
|
self.unparse_expr(&comp.iter, precedence::COMPREHENSION);
|
|
for cond in &comp.ifs {
|
|
self.p(" if ");
|
|
self.unparse_expr(cond, precedence::COMPREHENSION);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unparse_string_literal(&mut self, string_literal: &ast::StringLiteral) {
|
|
let ast::StringLiteral { value, flags, .. } = string_literal;
|
|
self.p_str_repr(value, *flags);
|
|
}
|
|
|
|
fn unparse_string_literal_value(&mut self, value: &ast::StringLiteralValue) {
|
|
let mut first = true;
|
|
for string_literal in value {
|
|
self.p_delim(&mut first, " ");
|
|
self.unparse_string_literal(string_literal);
|
|
}
|
|
}
|
|
|
|
fn unparse_f_string_value(&mut self, value: &ast::FStringValue) {
|
|
let mut first = true;
|
|
for f_string_part in value {
|
|
self.p_delim(&mut first, " ");
|
|
match f_string_part {
|
|
ast::FStringPart::Literal(string_literal) => {
|
|
self.unparse_string_literal(string_literal);
|
|
}
|
|
ast::FStringPart::FString(f_string) => {
|
|
self.unparse_interpolated_string(&f_string.elements, f_string.flags.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unparse_interpolated_string_body(
|
|
&mut self,
|
|
values: &[ast::InterpolatedStringElement],
|
|
flags: AnyStringFlags,
|
|
) {
|
|
for value in values {
|
|
self.unparse_interpolated_string_element(value, flags);
|
|
}
|
|
}
|
|
|
|
fn unparse_interpolated_element(
|
|
&mut self,
|
|
val: &Expr,
|
|
debug_text: Option<&DebugText>,
|
|
conversion: ConversionFlag,
|
|
spec: Option<&ast::InterpolatedStringFormatSpec>,
|
|
flags: AnyStringFlags,
|
|
) {
|
|
let mut generator = Generator::new(self.indent, self.line_ending);
|
|
generator.unparse_expr(val, precedence::FORMATTED_VALUE);
|
|
let brace = if generator.buffer.starts_with('{') {
|
|
// put a space to avoid escaping the bracket
|
|
"{ "
|
|
} else {
|
|
"{"
|
|
};
|
|
self.p(brace);
|
|
|
|
if let Some(debug_text) = debug_text {
|
|
self.buffer += debug_text.leading.as_str();
|
|
}
|
|
|
|
self.buffer += &generator.buffer;
|
|
|
|
if let Some(debug_text) = debug_text {
|
|
self.buffer += debug_text.trailing.as_str();
|
|
}
|
|
|
|
if !conversion.is_none() {
|
|
self.p("!");
|
|
|
|
self.p(&format!("{}", conversion as u8 as char));
|
|
}
|
|
|
|
if let Some(spec) = spec {
|
|
self.p(":");
|
|
self.unparse_f_string_specifier(&spec.elements, flags);
|
|
}
|
|
|
|
self.p("}");
|
|
}
|
|
|
|
fn unparse_interpolated_string_element(
|
|
&mut self,
|
|
element: &ast::InterpolatedStringElement,
|
|
flags: AnyStringFlags,
|
|
) {
|
|
match element {
|
|
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
|
|
value,
|
|
..
|
|
}) => {
|
|
self.unparse_interpolated_string_literal_element(value, flags);
|
|
}
|
|
ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
|
|
expression,
|
|
debug_text,
|
|
conversion,
|
|
format_spec,
|
|
range: _,
|
|
node_index: _,
|
|
}) => self.unparse_interpolated_element(
|
|
expression,
|
|
debug_text.as_ref(),
|
|
*conversion,
|
|
format_spec.as_deref(),
|
|
flags,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn unparse_interpolated_string_literal_element(&mut self, s: &str, flags: AnyStringFlags) {
|
|
let s = s.replace('{', "{{").replace('}', "}}");
|
|
if flags.prefix().is_raw() {
|
|
self.buffer += &s;
|
|
return;
|
|
}
|
|
let escape = UnicodeEscape::with_preferred_quote(&s, flags.quote_style());
|
|
if let Some(len) = escape.layout().len {
|
|
self.buffer.reserve(len);
|
|
}
|
|
escape
|
|
.write_body(&mut self.buffer)
|
|
.expect("Writing to a String buffer should never fail");
|
|
}
|
|
|
|
fn unparse_f_string_specifier(
|
|
&mut self,
|
|
values: &[ast::InterpolatedStringElement],
|
|
flags: AnyStringFlags,
|
|
) {
|
|
self.unparse_interpolated_string_body(values, flags);
|
|
}
|
|
|
|
/// Unparse `values` with [`Generator::unparse_f_string_body`], using `quote` as the preferred
|
|
/// surrounding quote style.
|
|
fn unparse_interpolated_string(
|
|
&mut self,
|
|
values: &[ast::InterpolatedStringElement],
|
|
flags: AnyStringFlags,
|
|
) {
|
|
self.p(flags.prefix().as_str());
|
|
self.p(flags.quote_str());
|
|
self.unparse_interpolated_string_body(values, flags);
|
|
self.p(flags.quote_str());
|
|
}
|
|
|
|
fn unparse_t_string_value(&mut self, value: &ast::TStringValue) {
|
|
let mut first = true;
|
|
for t_string in value {
|
|
self.p_delim(&mut first, " ");
|
|
self.unparse_interpolated_string(&t_string.elements, t_string.flags.into());
|
|
}
|
|
}
|
|
|
|
fn unparse_alias(&mut self, alias: &Alias) {
|
|
self.p_id(&alias.name);
|
|
if let Some(asname) = &alias.asname {
|
|
self.p(" as ");
|
|
self.p_id(asname);
|
|
}
|
|
}
|
|
|
|
fn unparse_with_item(&mut self, with_item: &WithItem) {
|
|
self.unparse_expr(&with_item.context_expr, precedence::MAX);
|
|
if let Some(optional_vars) = &with_item.optional_vars {
|
|
self.p(" as ");
|
|
self.unparse_expr(optional_vars, precedence::MAX);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use ruff_python_ast::{Mod, ModModule};
|
|
use ruff_python_parser::{self, Mode, ParseOptions, parse_module};
|
|
use ruff_source_file::LineEnding;
|
|
|
|
use crate::stylist::Indentation;
|
|
|
|
use super::Generator;
|
|
|
|
fn round_trip(contents: &str) -> String {
|
|
let indentation = Indentation::default();
|
|
let line_ending = LineEnding::default();
|
|
let module = parse_module(contents).unwrap();
|
|
let mut generator = Generator::new(&indentation, line_ending);
|
|
generator.unparse_suite(module.suite());
|
|
generator.generate()
|
|
}
|
|
|
|
/// Like [`round_trip`] but configure the [`Generator`] with the requested `indentation` and
|
|
/// `line_ending` settings.
|
|
fn round_trip_with(
|
|
indentation: &Indentation,
|
|
line_ending: LineEnding,
|
|
contents: &str,
|
|
) -> String {
|
|
let module = parse_module(contents).unwrap();
|
|
let mut generator = Generator::new(indentation, line_ending);
|
|
generator.unparse_suite(module.suite());
|
|
generator.generate()
|
|
}
|
|
|
|
fn jupyter_round_trip(contents: &str) -> String {
|
|
let indentation = Indentation::default();
|
|
let line_ending = LineEnding::default();
|
|
let parsed =
|
|
ruff_python_parser::parse(contents, ParseOptions::from(Mode::Ipython)).unwrap();
|
|
let Mod::Module(ModModule { body, .. }) = parsed.into_syntax() else {
|
|
panic!("Source code didn't return ModModule")
|
|
};
|
|
let [stmt] = body.as_slice() else {
|
|
panic!("Expected only one statement in source code")
|
|
};
|
|
let mut generator = Generator::new(&indentation, line_ending);
|
|
generator.unparse_stmt(stmt);
|
|
generator.generate()
|
|
}
|
|
|
|
macro_rules! assert_round_trip {
|
|
($contents:expr) => {
|
|
assert_eq!(
|
|
round_trip($contents),
|
|
$contents.replace('\n', LineEnding::default().as_str())
|
|
);
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn unparse_magic_commands() {
|
|
assert_eq!(
|
|
jupyter_round_trip("%matplotlib inline"),
|
|
"%matplotlib inline"
|
|
);
|
|
assert_eq!(
|
|
jupyter_round_trip("%matplotlib \\\n inline"),
|
|
"%matplotlib inline"
|
|
);
|
|
assert_eq!(jupyter_round_trip("dir = !pwd"), "dir = !pwd");
|
|
}
|
|
|
|
#[test]
|
|
fn unparse() {
|
|
assert_round_trip!("{i for i in b async for i in a if await i for b in i}");
|
|
assert_round_trip!("f(**x)");
|
|
assert_round_trip!("{**x}");
|
|
assert_round_trip!("f(**([] or 5))");
|
|
assert_round_trip!(r#"my_function(*[1], *[2], **{"three": 3}, **{"four": "four"})"#);
|
|
assert_round_trip!("{**([] or 5)}");
|
|
assert_round_trip!("del l[0]");
|
|
assert_round_trip!("del obj.x");
|
|
assert_round_trip!("a @ b");
|
|
assert_round_trip!("a @= b");
|
|
assert_round_trip!("x.foo");
|
|
assert_round_trip!("return await (await bar())");
|
|
assert_round_trip!("(5).foo");
|
|
assert_round_trip!(r#"our_dict = {"a": 1, **{"b": 2, "c": 3}}"#);
|
|
assert_round_trip!(r"j = [1, 2, 3]");
|
|
assert_round_trip!(
|
|
r#"def test(a1, a2, b1=j, b2="123", b3={}, b4=[]):
|
|
pass"#
|
|
);
|
|
assert_round_trip!("a @ b");
|
|
assert_round_trip!("a @= b");
|
|
assert_round_trip!("[1, 2, 3]");
|
|
assert_round_trip!("foo(1)");
|
|
assert_round_trip!("foo(1, 2)");
|
|
assert_round_trip!("foo(x for x in y)");
|
|
assert_round_trip!("foo([x for x in y])");
|
|
assert_round_trip!("foo([(x := 2) for x in y])");
|
|
assert_round_trip!("x = yield 1");
|
|
assert_round_trip!("return (yield 1)");
|
|
assert_round_trip!("lambda: (1, 2, 3)");
|
|
assert_round_trip!("return 3 and 4");
|
|
assert_round_trip!("return 3 or 4");
|
|
assert_round_trip!("yield from some()");
|
|
assert_round_trip!(r#"assert (1, 2, 3), "msg""#);
|
|
assert_round_trip!("import ast");
|
|
assert_round_trip!("import operator as op");
|
|
assert_round_trip!("from math import floor");
|
|
assert_round_trip!("from .. import foobar");
|
|
assert_round_trip!("from ..aaa import foo, bar as bar2");
|
|
assert_round_trip!(r#"return f"functools.{qualname}({', '.join(args)})""#);
|
|
assert_round_trip!(r#"my_function(*[1], *[2], **{"three": 3}, **{"four": "four"})"#);
|
|
assert_round_trip!(r#"our_dict = {"a": 1, **{"b": 2, "c": 3}}"#);
|
|
assert_round_trip!("f(**x)");
|
|
assert_round_trip!("{**x}");
|
|
assert_round_trip!("f(**([] or 5))");
|
|
assert_round_trip!("{**([] or 5)}");
|
|
assert_round_trip!(r#"return f"functools.{qualname}({', '.join(args)})""#);
|
|
assert_round_trip!(
|
|
r#"class TreeFactory(*[FactoryMixin, TreeBase], **{"metaclass": Foo}):
|
|
pass"#
|
|
);
|
|
assert_round_trip!(
|
|
r"class Foo(Bar, object):
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"class Foo[T]:
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"class Foo[T](Bar):
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"class Foo[*Ts]:
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"class Foo[**P]:
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"class Foo[T, U, *Ts, **P]:
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def f() -> (int, str):
|
|
pass"
|
|
);
|
|
assert_round_trip!("[await x async for x in y]");
|
|
assert_round_trip!("[await i for i in b if await c]");
|
|
assert_round_trip!("(await x async for x in y)");
|
|
assert_round_trip!(
|
|
r#"async def read_data(db):
|
|
async with connect(db) as db_cxn:
|
|
data = await db_cxn.fetch("SELECT foo FROM bar;")
|
|
async for datum in data:
|
|
if quux(datum):
|
|
return datum"#
|
|
);
|
|
assert_round_trip!(
|
|
r"def f() -> (int, int):
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def test(a, b, /, c, *, d, **kwargs):
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def test(a=3, b=4, /, c=7):
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def test(a, b=4, /, c=8, d=9):
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def test[T]():
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def test[*Ts]():
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def test[**P]():
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def test[T, U, *Ts, **P]():
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"def call(*popenargs, timeout=None, **kwargs):
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"@functools.lru_cache(maxsize=None)
|
|
def f(x: int, y: int) -> int:
|
|
return x + y"
|
|
);
|
|
assert_round_trip!(
|
|
r"try:
|
|
pass
|
|
except Exception as e:
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"try:
|
|
pass
|
|
except* Exception as e:
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"match x:
|
|
case [1, 2, 3]:
|
|
return 2
|
|
case 4 as y:
|
|
return y"
|
|
);
|
|
assert_round_trip!(
|
|
r"type X = int
|
|
type Y = str"
|
|
);
|
|
assert_eq!(round_trip(r"x = (1, 2, 3)"), r"x = 1, 2, 3");
|
|
assert_eq!(round_trip(r"-(1) + ~(2) + +(3)"), r"-1 + ~2 + +3");
|
|
assert_round_trip!(
|
|
r"def f():
|
|
|
|
def f():
|
|
pass"
|
|
);
|
|
assert_round_trip!(
|
|
r"@foo
|
|
def f():
|
|
|
|
@foo
|
|
def f():
|
|
pass"
|
|
);
|
|
|
|
assert_round_trip!(
|
|
r"@foo
|
|
class Foo:
|
|
|
|
@foo
|
|
def f():
|
|
pass"
|
|
);
|
|
|
|
assert_round_trip!(r"[lambda n: n for n in range(10)]");
|
|
assert_round_trip!(r"[n[0:2] for n in range(10)]");
|
|
assert_round_trip!(r"[n[0] for n in range(10)]");
|
|
assert_round_trip!(r"[(n, n * 2) for n in range(10)]");
|
|
assert_round_trip!(r"[1 if n % 2 == 0 else 0 for n in range(10)]");
|
|
assert_round_trip!(r"[n % 2 == 0 or 0 for n in range(10)]");
|
|
assert_round_trip!(r"[(n := 2) for n in range(10)]");
|
|
assert_round_trip!(r"((n := 2) for n in range(10))");
|
|
assert_round_trip!(r"[n * 2 for n in range(10)]");
|
|
assert_round_trip!(r"{n * 2 for n in range(10)}");
|
|
assert_round_trip!(r"{i: n * 2 for i, n in enumerate(range(10))}");
|
|
assert_round_trip!(
|
|
"class SchemaItem(NamedTuple):
|
|
fields: ((\"property_key\", str),)"
|
|
);
|
|
assert_round_trip!(
|
|
"def func():
|
|
return (i := 1)"
|
|
);
|
|
assert_round_trip!("yield (i := 1)");
|
|
assert_round_trip!("x = (i := 1)");
|
|
assert_round_trip!("x += (i := 1)");
|
|
|
|
// Type aliases
|
|
assert_round_trip!(r"type Foo = int | str");
|
|
assert_round_trip!(r"type Foo[T] = list[T]");
|
|
assert_round_trip!(r"type Foo[*Ts] = ...");
|
|
assert_round_trip!(r"type Foo[**P] = ...");
|
|
assert_round_trip!(r"type Foo[T = int] = list[T]");
|
|
assert_round_trip!(r"type Foo[*Ts = int] = ...");
|
|
assert_round_trip!(r"type Foo[*Ts = *int] = ...");
|
|
assert_round_trip!(r"type Foo[**P = int] = ...");
|
|
assert_round_trip!(r"type Foo[T, U, *Ts, **P] = ...");
|
|
// https://github.com/astral-sh/ruff/issues/6498
|
|
assert_round_trip!(r"f(a=1, *args, **kwargs)");
|
|
assert_round_trip!(r"f(*args, a=1, **kwargs)");
|
|
assert_round_trip!(r"f(*args, a=1, *args2, **kwargs)");
|
|
assert_round_trip!("class A(*args, a=2, *args2, **kwargs):\n pass");
|
|
}
|
|
|
|
#[test]
|
|
fn quote() {
|
|
assert_round_trip!(r#""hello""#);
|
|
assert_round_trip!(r"'hello'");
|
|
assert_round_trip!(r"u'hello'");
|
|
assert_round_trip!(r"r'hello'");
|
|
assert_round_trip!(r"b'hello'");
|
|
assert_round_trip!(r#"b"hello""#);
|
|
assert_round_trip!(r"f'hello'");
|
|
assert_round_trip!(r#"f"hello""#);
|
|
assert_eq!(round_trip(r#"("abc" "def" "ghi")"#), r#""abc" "def" "ghi""#);
|
|
assert_eq!(round_trip(r#""he\"llo""#), r#"'he"llo'"#);
|
|
assert_eq!(round_trip(r#"b"he\"llo""#), r#"b'he"llo'"#);
|
|
assert_eq!(round_trip(r#"f"abc{'def'}{1}""#), r#"f"abc{'def'}{1}""#);
|
|
assert_round_trip!(r#"f'abc{"def"}{1}'"#);
|
|
}
|
|
|
|
/// test all of the valid string literal prefix and quote combinations from
|
|
/// https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
|
|
///
|
|
/// Note that the numeric ids on the input/output and quote fields prevent name conflicts from
|
|
/// the test_matrix but are otherwise unnecessary
|
|
#[test_case::test_matrix(
|
|
[
|
|
("r", "r", 0),
|
|
("u", "u", 1),
|
|
("R", "R", 2),
|
|
("U", "u", 3), // case not tracked
|
|
("f", "f", 4),
|
|
("F", "f", 5), // f case not tracked
|
|
("fr", "rf", 6), // r before f
|
|
("Fr", "rf", 7), // f case not tracked, r before f
|
|
("fR", "Rf", 8), // r before f
|
|
("FR", "Rf", 9), // f case not tracked, r before f
|
|
("rf", "rf", 10),
|
|
("rF", "rf", 11), // f case not tracked
|
|
("Rf", "Rf", 12),
|
|
("RF", "Rf", 13), // f case not tracked
|
|
// bytestrings
|
|
("b", "b", 14),
|
|
("B", "b", 15), // b case
|
|
("br", "rb", 16), // r before b
|
|
("Br", "rb", 17), // b case, r before b
|
|
("bR", "Rb", 18), // r before b
|
|
("BR", "Rb", 19), // b case, r before b
|
|
("rb", "rb", 20),
|
|
("rB", "rb", 21), // b case
|
|
("Rb", "Rb", 22),
|
|
("RB", "Rb", 23), // b case
|
|
],
|
|
[("\"", 0), ("'",1), ("\"\"\"", 2), ("'''", 3)],
|
|
["hello", "{hello} {world}"]
|
|
)]
|
|
fn prefix_quotes((inp, out, _id): (&str, &str, u8), (quote, _id2): (&str, u8), base: &str) {
|
|
let input = format!("{inp}{quote}{base}{quote}");
|
|
let output = format!("{out}{quote}{base}{quote}");
|
|
assert_eq!(round_trip(&input), output);
|
|
}
|
|
|
|
#[test]
|
|
fn raw() {
|
|
assert_round_trip!(r#"r"a\.b""#); // https://github.com/astral-sh/ruff/issues/9663
|
|
assert_round_trip!(r#"R"a\.b""#);
|
|
}
|
|
|
|
#[test]
|
|
fn self_documenting_fstring() {
|
|
assert_round_trip!(r#"f"{ chr(65) = }""#);
|
|
assert_round_trip!(r#"f"{ chr(65) = !s}""#);
|
|
assert_round_trip!(r#"f"{ chr(65) = !r}""#);
|
|
assert_round_trip!(r#"f"{ chr(65) = :#x}""#);
|
|
assert_round_trip!(r#"f"{ ( chr(65) ) = }""#);
|
|
assert_round_trip!(r#"f"{a=!r:0.05f}""#);
|
|
// https://github.com/astral-sh/ruff/issues/18742
|
|
assert_eq!(
|
|
round_trip(
|
|
r#"
|
|
f"{1=
|
|
}"
|
|
"#
|
|
),
|
|
r#"
|
|
f"{1=
|
|
}"
|
|
"#
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn implicit_string_concatenation() {
|
|
assert_round_trip!(r#""first" "second" "third""#);
|
|
assert_round_trip!(r#"b"first" b"second" b"third""#);
|
|
assert_round_trip!(r#""first" "second" f"third {var}""#);
|
|
}
|
|
|
|
#[test]
|
|
fn indent() {
|
|
assert_eq!(
|
|
round_trip(
|
|
r"
|
|
if True:
|
|
pass
|
|
"
|
|
.trim(),
|
|
),
|
|
r"
|
|
if True:
|
|
pass
|
|
"
|
|
.trim()
|
|
.replace('\n', LineEnding::default().as_str())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn set_indent() {
|
|
assert_eq!(
|
|
round_trip_with(
|
|
&Indentation::new(" ".to_string()),
|
|
LineEnding::default(),
|
|
r"
|
|
if True:
|
|
pass
|
|
"
|
|
.trim(),
|
|
),
|
|
r"
|
|
if True:
|
|
pass
|
|
"
|
|
.trim()
|
|
.replace('\n', LineEnding::default().as_str())
|
|
);
|
|
assert_eq!(
|
|
round_trip_with(
|
|
&Indentation::new(" ".to_string()),
|
|
LineEnding::default(),
|
|
r"
|
|
if True:
|
|
pass
|
|
"
|
|
.trim(),
|
|
),
|
|
r"
|
|
if True:
|
|
pass
|
|
"
|
|
.trim()
|
|
.replace('\n', LineEnding::default().as_str())
|
|
);
|
|
assert_eq!(
|
|
round_trip_with(
|
|
&Indentation::new("\t".to_string()),
|
|
LineEnding::default(),
|
|
r"
|
|
if True:
|
|
pass
|
|
"
|
|
.trim(),
|
|
),
|
|
r"
|
|
if True:
|
|
pass
|
|
"
|
|
.trim()
|
|
.replace('\n', LineEnding::default().as_str())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn set_line_ending() {
|
|
assert_eq!(
|
|
round_trip_with(
|
|
&Indentation::default(),
|
|
LineEnding::Lf,
|
|
"if True:\n print(42)",
|
|
),
|
|
"if True:\n print(42)",
|
|
);
|
|
|
|
assert_eq!(
|
|
round_trip_with(
|
|
&Indentation::default(),
|
|
LineEnding::CrLf,
|
|
"if True:\n print(42)",
|
|
),
|
|
"if True:\r\n print(42)",
|
|
);
|
|
|
|
assert_eq!(
|
|
round_trip_with(
|
|
&Indentation::default(),
|
|
LineEnding::Cr,
|
|
"if True:\n print(42)",
|
|
),
|
|
"if True:\r print(42)",
|
|
);
|
|
}
|
|
}
|