use ruff_python_ast::{
self as ast, AtomicNodeIndex, Expr as AstExpr,
str::{Quote, TripleQuotes},
str_prefix::{ByteStringPrefix, StringLiteralPrefix},
visitor::transformer::{Transformer, walk_expr, walk_f_string},
};
use ruff_python_codegen::{Generator, Indentation};
use ruff_source_file::LineEnding;
pub(crate) fn stringize_annotation(annotation: &mut AstExpr) -> String {
CanonicalStringLiterals.visit_expr(annotation);
Generator::new(&Indentation::default(), LineEnding::Lf).expr(annotation)
}
struct CanonicalStringLiterals;
impl Transformer for CanonicalStringLiterals {
fn visit_expr(&self, expr: &mut AstExpr) {
match expr {
AstExpr::StringLiteral(s) => rebuild_string_literal(s),
AstExpr::BytesLiteral(b) => rebuild_bytes_literal(b),
AstExpr::FString(f) if f.value.is_implicit_concatenated() => merge_f_string_parts(f),
_ => {}
}
walk_expr(self, expr);
}
fn visit_f_string(&self, f_string: &mut ast::FString) {
f_string.flags = f_string
.flags
.with_quote_style(Quote::Single)
.with_triple_quotes(TripleQuotes::No);
walk_f_string(self, f_string);
}
}
fn canonical_string_flags(flags: ast::StringLiteralFlags) -> ast::StringLiteralFlags {
let prefix = match flags.prefix() {
StringLiteralPrefix::Unicode => StringLiteralPrefix::Unicode,
StringLiteralPrefix::Empty | StringLiteralPrefix::Raw { .. } => StringLiteralPrefix::Empty,
};
flags
.with_prefix(prefix)
.with_quote_style(Quote::Single)
.with_triple_quotes(TripleQuotes::No)
}
fn rebuild_string_literal(expr: &mut ast::ExprStringLiteral) {
let canonical = ast::StringLiteral {
range: expr.range,
node_index: AtomicNodeIndex::default(),
value: expr.value.to_str().into(),
flags: canonical_string_flags(expr.value.first_literal_flags()),
};
expr.value = ast::StringLiteralValue::single(canonical);
}
fn rebuild_bytes_literal(expr: &mut ast::ExprBytesLiteral) {
let canonical = ast::BytesLiteral {
range: expr.range,
node_index: AtomicNodeIndex::default(),
value: expr.value.bytes().collect(),
flags: ast::BytesLiteralFlags::empty()
.with_prefix(ByteStringPrefix::Regular)
.with_quote_style(Quote::Single)
.with_triple_quotes(TripleQuotes::No),
};
expr.value = ast::BytesLiteralValue::single(canonical);
}
fn merge_f_string_parts(expr: &mut ast::ExprFString) {
let mut elements: Vec<ast::InterpolatedStringElement> = Vec::new();
let mut flags = None;
for part in &expr.value {
match part {
ast::FStringPart::Literal(literal) => {
elements.push(ast::InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement {
range: literal.range,
node_index: AtomicNodeIndex::default(),
value: literal.value.clone(),
},
));
}
ast::FStringPart::FString(f_string) => {
flags = flags.or(Some(f_string.flags));
elements.extend(f_string.elements.iter().cloned());
}
}
}
let Some(flags) = flags else { return };
expr.value = ast::FStringValue::single(ast::FString {
range: expr.range,
node_index: AtomicNodeIndex::default(),
elements: elements.into(),
flags,
});
}