#![allow(clippy::doc_markdown)]
use std::cmp::Ordering;
use std::sync::LazyLock;
use std::{borrow::Cow, collections::VecDeque};
use itertools::Itertools;
use regex::Regex;
use ruff_formatter::printer::SourceMapGeneration;
use ruff_python_ast::{AnyStringFlags, StringFlags, str::Quote};
use ruff_python_parser::ParseOptions;
use ruff_python_trivia::TriviaRanges;
use {
ruff_formatter::{FormatOptions, IndentStyle, LineWidth, Printed, write},
ruff_python_trivia::{PythonWhitespace, is_python_whitespace},
ruff_text_size::{Ranged, TextLen, TextRange, TextSize},
};
use super::NormalizedString;
use crate::string::StringQuotes;
use crate::{DocstringCodeLineWidth, FormatModuleError, prelude::*};
pub(crate) fn format(normalized: &NormalizedString, f: &mut PyFormatter) -> FormatResult<()> {
let docstring = &normalized.text();
if contains_unescaped_newline(docstring) {
return normalized.fmt(f);
}
let already_normalized = matches!(docstring, Cow::Borrowed(_));
let mut lines = docstring.split('\n').peekable();
let kind = normalized.flags();
let quotes = StringQuotes::from(kind);
write!(f, [kind.prefix(), quotes])?;
let mut offset = normalized.start();
let first = lines.next().unwrap_or_default();
let trim_end = first.trim_end();
let trim_both = trim_end.trim_start();
if trim_both.starts_with(quotes.quote_char.as_char()) {
space().fmt(f)?;
}
if !trim_end.is_empty() {
let leading_whitespace = trim_end.text_len() - trim_both.text_len();
let trimmed_line_range =
TextRange::at(offset, trim_end.text_len()).add_start(leading_whitespace);
if already_normalized {
source_text_slice(trimmed_line_range).fmt(f)?;
} else {
text(trim_both).fmt(f)?;
}
}
offset += first.text_len();
if docstring[first.len()..].trim().is_empty() {
if needs_chaperone_space(normalized.flags(), trim_end)
|| (trim_end.is_empty() && !docstring.is_empty())
{
space().fmt(f)?;
}
quotes.fmt(f)?;
return Ok(());
}
hard_line_break().fmt(f)?;
offset += "\n".text_len();
let stripped_indentation = lines
.clone()
.filter(|line| !line.trim().is_empty())
.map(Indentation::from_str)
.min_by_key(|indentation| indentation.columns())
.unwrap_or_default();
DocstringLinePrinter {
f,
action_queue: VecDeque::new(),
offset,
stripped_indentation,
already_normalized,
quote_char: quotes.quote_char,
code_example: CodeExample::default(),
}
.add_iter(lines)?;
let trim_end = docstring
.as_ref()
.trim_end_matches(|c: char| c.is_whitespace() && c != '\n');
if needs_chaperone_space(normalized.flags(), trim_end) {
space().fmt(f)?;
}
write!(f, [quotes])
}
fn contains_unescaped_newline(haystack: &str) -> bool {
let mut rest = haystack;
while let Some(index) = memchr::memchr(b'\\', rest.as_bytes()) {
rest = rest[index + 1..].trim_whitespace_start();
if rest.starts_with('\n') {
return true;
}
}
false
}
struct DocstringLinePrinter<'ast, 'buf, 'fmt, 'src> {
f: &'fmt mut PyFormatter<'ast, 'buf>,
action_queue: VecDeque<CodeExampleAddAction<'src>>,
offset: TextSize,
stripped_indentation: Indentation,
already_normalized: bool,
quote_char: Quote,
code_example: CodeExample<'src>,
}
impl<'src> DocstringLinePrinter<'_, '_, '_, 'src> {
fn add_iter(
&mut self,
mut lines: std::iter::Peekable<std::str::Split<'src, char>>,
) -> FormatResult<()> {
while let Some(line) = lines.next() {
let line = InputDocstringLine {
line,
offset: self.offset,
next: lines.peek().copied(),
};
self.offset += line.line.text_len() + "\n".text_len();
self.add_one(line)?;
}
self.code_example.finish(&mut self.action_queue);
self.run_action_queue()
}
fn add_one(&mut self, line: InputDocstringLine<'src>) -> FormatResult<()> {
if !self.f.options().docstring_code().is_enabled() || self.f.context().docstring().is_some()
{
return self.print_one(&line.as_output());
}
self.code_example.add(line, &mut self.action_queue);
self.run_action_queue()
}
fn run_action_queue(&mut self) -> FormatResult<()> {
while let Some(action) = self.action_queue.pop_front() {
match action {
CodeExampleAddAction::Print { original } => {
self.print_one(&original.as_output())?;
}
CodeExampleAddAction::Kept => {}
CodeExampleAddAction::Reset { code } => {
for codeline in code {
self.print_one(&codeline.original.as_output())?;
}
}
CodeExampleAddAction::Format { mut kind } => {
let Some(formatted_lines) = self.format(&mut kind)? else {
self.action_queue.push_front(CodeExampleAddAction::Reset {
code: kind.into_code(),
});
continue;
};
self.already_normalized = false;
match kind {
CodeExampleKind::Doctest(CodeExampleDoctest { ps1_indent, .. }) => {
let mut lines = formatted_lines.into_iter();
let Some(first) = lines.next() else { continue };
self.print_one(
&first.map(|line| std::format!("{ps1_indent}>>> {line}")),
)?;
for docline in lines {
self.print_one(
&docline.map(|line| std::format!("{ps1_indent}... {line}")),
)?;
}
}
CodeExampleKind::Rst(litblock) => {
let Some(min_indent) = litblock.min_indent else {
continue;
};
let indent = " ".repeat(min_indent.columns());
for docline in formatted_lines {
self.print_one(
&docline.map(|line| std::format!("{indent}{line}")),
)?;
}
}
CodeExampleKind::Markdown(fenced) => {
let indent = " ".repeat(fenced.opening_fence_indent.columns());
for docline in formatted_lines {
self.print_one(
&docline.map(|line| std::format!("{indent}{line}")),
)?;
}
}
}
}
}
}
Ok(())
}
fn print_one(&mut self, line: &OutputDocstringLine<'_>) -> FormatResult<()> {
let trim_end = line.line.trim_end();
if trim_end.is_empty() {
return if line.is_last {
Ok(())
} else {
empty_line().fmt(self.f)
};
}
let indent_offset = match self.f.options().indent_style() {
IndentStyle::Space => {
let tab_or_non_ascii_space = trim_end
.chars()
.take_while(|c| c.is_whitespace())
.any(|c| c != ' ');
if tab_or_non_ascii_space {
None
} else {
let stripped_indentation_len = self.stripped_indentation.text_len();
Some(stripped_indentation_len)
}
}
IndentStyle::Tab => {
let line_indent = Indentation::from_str(trim_end);
let non_ascii_whitespace = trim_end
.chars()
.take_while(|c| c.is_whitespace())
.any(|c| !matches!(c, ' ' | '\t'));
let trimmed = line_indent.trim_start(self.stripped_indentation);
let preserve_indent = !non_ascii_whitespace
&& trimmed.is_some_and(|trimmed| !trimmed.is_spaces_tabs());
preserve_indent.then_some(self.stripped_indentation.text_len())
}
};
if let Some(indent_offset) = indent_offset {
if self.already_normalized {
let trimmed_line_range =
TextRange::at(line.offset, trim_end.text_len()).add_start(indent_offset);
source_text_slice(trimmed_line_range).fmt(self.f)?;
} else {
text(&trim_end[indent_offset.to_usize()..]).fmt(self.f)?;
}
} else {
let indent_len =
Indentation::from_str(trim_end).columns() - self.stripped_indentation.columns();
let in_docstring_indent = " ".repeat(indent_len) + trim_end.trim_start();
text(&in_docstring_indent).fmt(self.f)?;
}
if !line.is_last {
hard_line_break().fmt(self.f)?;
}
Ok(())
}
fn format(
&mut self,
kind: &mut CodeExampleKind<'_>,
) -> FormatResult<Option<Vec<OutputDocstringLine<'static>>>> {
let line_width = match self.f.options().docstring_code_line_width() {
DocstringCodeLineWidth::Fixed(width) => width,
DocstringCodeLineWidth::Dynamic => {
let global_line_width = self.f.options().line_width().value();
let indent_width = self.f.options().indent_width();
let indent_level = self.f.context().indent_level();
let mut current_indent = indent_level
.to_ascii_spaces(indent_width)
.saturating_add(kind.extra_indent_ascii_spaces());
current_indent = current_indent.saturating_add(
u16::try_from(
kind.indent()
.columns()
.saturating_sub(self.stripped_indentation.columns()),
)
.unwrap_or(u16::MAX),
);
let width = std::cmp::max(1, global_line_width.saturating_sub(current_indent));
LineWidth::try_from(width).expect("width should be capped at a minimum of 1")
}
};
let code = kind.code();
let (Some(unformatted_first), Some(unformatted_last)) = (code.first(), code.last()) else {
return Ok(None);
};
let codeblob = code
.iter()
.map(|line| line.code)
.collect::<Vec<&str>>()
.join("\n");
let options = self
.f
.options()
.clone()
.with_line_width(line_width)
.with_indent_style(IndentStyle::Space)
.with_source_map_generation(SourceMapGeneration::Disabled);
let printed = match docstring_format_source(options, self.quote_char, &codeblob) {
Ok(printed) => printed,
Err(FormatModuleError::FormatError(err)) => return Err(err),
Err(FormatModuleError::ParseError(_) | FormatModuleError::PrintError(_)) => {
return Ok(None);
}
};
let wrapped = match self.quote_char {
Quote::Single => std::format!("'''{}'''", printed.as_code()),
Quote::Double => {
std::format!(r#""""{}""""#, printed.as_code())
}
};
let result =
ruff_python_parser::parse(&wrapped, ParseOptions::from(self.f.options().source_type()));
if result.is_err() {
return Ok(None);
}
let mut lines = printed
.as_code()
.lines()
.map(|line| OutputDocstringLine {
line: Cow::Owned(line.to_string()),
offset: unformatted_first.original.offset,
is_last: false,
})
.collect::<Vec<_>>();
if let Some(reformatted_last) = lines.last_mut() {
reformatted_last.is_last = unformatted_last.original.is_last();
}
Ok(Some(lines))
}
}
#[derive(Clone, Copy, Debug)]
struct InputDocstringLine<'src> {
line: &'src str,
offset: TextSize,
next: Option<&'src str>,
}
impl<'src> InputDocstringLine<'src> {
fn as_output(&self) -> OutputDocstringLine<'src> {
OutputDocstringLine {
line: Cow::Borrowed(self.line),
offset: self.offset,
is_last: self.is_last(),
}
}
fn is_last(&self) -> bool {
self.next.is_none()
}
}
#[derive(Clone, Debug)]
struct OutputDocstringLine<'src> {
line: Cow<'src, str>,
offset: TextSize,
is_last: bool,
}
impl OutputDocstringLine<'_> {
fn map(self, mut map: impl FnMut(&str) -> String) -> OutputDocstringLine<'static> {
OutputDocstringLine {
line: Cow::Owned(map(&self.line)),
..self
}
}
}
#[derive(Debug, Default)]
struct CodeExample<'src> {
kind: Option<CodeExampleKind<'src>>,
}
impl<'src> CodeExample<'src> {
fn add(
&mut self,
original: InputDocstringLine<'src>,
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
) {
match self.kind.take() {
None => {
self.add_start(original, queue);
}
Some(CodeExampleKind::Doctest(doctest)) => {
let Some(doctest) = doctest.add_code_line(original, queue) else {
self.add_start(original, queue);
return;
};
self.kind = Some(CodeExampleKind::Doctest(doctest));
}
Some(CodeExampleKind::Rst(litblock)) => {
let Some(litblock) = litblock.add_code_line(original, queue) else {
self.add_start(original, queue);
return;
};
self.kind = Some(CodeExampleKind::Rst(litblock));
}
Some(CodeExampleKind::Markdown(fenced)) => {
let Some(fenced) = fenced.add_code_line(original, queue) else {
return;
};
self.kind = Some(CodeExampleKind::Markdown(fenced));
}
}
}
fn finish(&mut self, queue: &mut VecDeque<CodeExampleAddAction<'src>>) {
let Some(kind) = self.kind.take() else { return };
queue.push_back(CodeExampleAddAction::Format { kind });
}
fn add_start(
&mut self,
original: InputDocstringLine<'src>,
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
) {
assert!(self.kind.is_none(), "expected no existing code example");
if let Some(doctest) = CodeExampleDoctest::new(original) {
self.kind = Some(CodeExampleKind::Doctest(doctest));
queue.push_back(CodeExampleAddAction::Kept);
} else if let Some(litblock) = CodeExampleRst::new(original) {
self.kind = Some(CodeExampleKind::Rst(litblock));
queue.push_back(CodeExampleAddAction::Print { original });
} else if let Some(fenced) = CodeExampleMarkdown::new(original) {
self.kind = Some(CodeExampleKind::Markdown(fenced));
queue.push_back(CodeExampleAddAction::Print { original });
} else {
queue.push_back(CodeExampleAddAction::Print { original });
}
}
}
#[derive(Debug)]
enum CodeExampleKind<'src> {
Doctest(CodeExampleDoctest<'src>),
Rst(CodeExampleRst<'src>),
Markdown(CodeExampleMarkdown<'src>),
}
impl<'src> CodeExampleKind<'src> {
fn code(&mut self) -> &[CodeExampleLine<'src>] {
match *self {
CodeExampleKind::Doctest(ref doctest) => &doctest.lines,
CodeExampleKind::Rst(ref mut litblock) => litblock.indented_code(),
CodeExampleKind::Markdown(ref fenced) => &fenced.lines,
}
}
fn into_code(self) -> Vec<CodeExampleLine<'src>> {
match self {
CodeExampleKind::Doctest(doctest) => doctest.lines,
CodeExampleKind::Rst(litblock) => litblock.lines,
CodeExampleKind::Markdown(fenced) => fenced.lines,
}
}
fn extra_indent_ascii_spaces(&self) -> u16 {
match *self {
CodeExampleKind::Doctest(_) => 4,
_ => 0,
}
}
fn indent(&self) -> Indentation {
match self {
CodeExampleKind::Doctest(doctest) => Indentation::from_str(doctest.ps1_indent),
CodeExampleKind::Rst(rst) => rst.min_indent.unwrap_or(rst.opening_indent),
CodeExampleKind::Markdown(markdown) => markdown.opening_fence_indent,
}
}
}
#[derive(Debug)]
struct CodeExampleDoctest<'src> {
lines: Vec<CodeExampleLine<'src>>,
ps1_indent: &'src str,
}
impl<'src> CodeExampleDoctest<'src> {
fn new(original: InputDocstringLine<'src>) -> Option<CodeExampleDoctest<'src>> {
let trim_start = original.line.trim_start();
let code = trim_start.strip_prefix(">>> ")?;
let indent_len = original
.line
.len()
.checked_sub(trim_start.len())
.expect("suffix is <= original");
let lines = vec![CodeExampleLine { original, code }];
let ps1_indent = &original.line[..indent_len];
let doctest = CodeExampleDoctest { lines, ps1_indent };
Some(doctest)
}
fn add_code_line(
mut self,
original: InputDocstringLine<'src>,
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
) -> Option<CodeExampleDoctest<'src>> {
let Some((ps2_indent, ps2_after)) = original.line.split_once("...") else {
queue.push_back(self.into_format_action());
return None;
};
if self.ps1_indent != ps2_indent {
queue.push_back(self.into_format_action());
return None;
}
let code = match ps2_after.strip_prefix(' ') {
None if ps2_after.is_empty() => "",
None => {
queue.push_back(self.into_format_action());
return None;
}
Some(code) => code,
};
self.lines.push(CodeExampleLine { original, code });
queue.push_back(CodeExampleAddAction::Kept);
Some(self)
}
fn into_format_action(self) -> CodeExampleAddAction<'src> {
CodeExampleAddAction::Format {
kind: CodeExampleKind::Doctest(self),
}
}
}
#[derive(Debug)]
struct CodeExampleRst<'src> {
lines: Vec<CodeExampleLine<'src>>,
opening_indent: Indentation,
min_indent: Option<Indentation>,
is_directive: bool,
}
impl<'src> CodeExampleRst<'src> {
fn new(original: InputDocstringLine<'src>) -> Option<CodeExampleRst<'src>> {
let (opening_indent, rest) = indent_with_suffix(original.line);
if rest.starts_with(".. ") {
if let Some(litblock) = CodeExampleRst::new_code_block(original) {
return Some(litblock);
}
return None;
}
if !rest.trim_end().ends_with("::") {
return None;
}
Some(CodeExampleRst {
lines: vec![],
opening_indent: Indentation::from_str(opening_indent),
min_indent: None,
is_directive: false,
})
}
fn new_code_block(original: InputDocstringLine<'src>) -> Option<CodeExampleRst<'src>> {
static DIRECTIVE_START: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?m)^\s*\.\. \s*(?i:code-block|sourcecode)::\s*(?i:python|py|python3|py3)$",
)
.unwrap()
});
if !DIRECTIVE_START.is_match(original.line) {
return None;
}
Some(CodeExampleRst {
lines: vec![],
opening_indent: Indentation::from_str(original.line),
min_indent: None,
is_directive: true,
})
}
fn indented_code(&mut self) -> &[CodeExampleLine<'src>] {
let Some(min_indent) = self.min_indent else {
return &[];
};
for line in &mut self.lines {
line.code = if line.original.line.trim().is_empty() {
""
} else {
min_indent.trim_start_str(line.original.line)
};
}
&self.lines
}
fn add_code_line(
mut self,
original: InputDocstringLine<'src>,
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
) -> Option<CodeExampleRst<'src>> {
let Some(min_indent) = self.min_indent else {
return self.add_first_line(original, queue);
};
let (indent, rest) = indent_with_suffix(original.line);
if rest.is_empty() {
if let Some(next) = original.next {
let (next_indent, next_rest) = indent_with_suffix(next);
if !next_rest.is_empty()
&& Indentation::from_str(next_indent) <= self.opening_indent
{
self.push_format_action(queue);
return None;
}
} else {
self.push_format_action(queue);
return None;
}
self.push(original);
queue.push_back(CodeExampleAddAction::Kept);
return Some(self);
}
let indent_len = Indentation::from_str(indent);
if indent_len <= self.opening_indent {
queue.push_back(self.into_reset_action());
return None;
} else if indent_len < min_indent {
self.min_indent = Some(indent_len);
}
self.push(original);
queue.push_back(CodeExampleAddAction::Kept);
Some(self)
}
fn add_first_line(
mut self,
original: InputDocstringLine<'src>,
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
) -> Option<CodeExampleRst<'src>> {
assert!(self.min_indent.is_none());
let (indent, rest) = indent_with_suffix(original.line);
if rest.is_empty() {
queue.push_back(CodeExampleAddAction::Print { original });
return Some(self);
}
if self.is_directive && is_rst_option(rest) {
queue.push_back(CodeExampleAddAction::Print { original });
return Some(self);
}
let min_indent = Indentation::from_str(indent);
if min_indent <= self.opening_indent {
queue.push_back(self.into_reset_action());
return None;
}
self.min_indent = Some(min_indent);
self.push(original);
queue.push_back(CodeExampleAddAction::Kept);
Some(self)
}
fn push(&mut self, original: InputDocstringLine<'src>) {
let code = original.line;
self.lines.push(CodeExampleLine { original, code });
}
fn push_format_action(mut self, queue: &mut VecDeque<CodeExampleAddAction<'src>>) {
let has_non_whitespace = |line: &CodeExampleLine| {
line.original
.line
.chars()
.any(|ch| !is_python_whitespace(ch))
};
let first_trailing_empty_line = self
.lines
.iter()
.rposition(has_non_whitespace)
.map_or(0, |i| i + 1);
let trailing_lines = self.lines.split_off(first_trailing_empty_line);
queue.push_back(CodeExampleAddAction::Format {
kind: CodeExampleKind::Rst(self),
});
queue.extend(
trailing_lines
.into_iter()
.map(|line| CodeExampleAddAction::Print {
original: line.original,
}),
);
}
fn into_reset_action(self) -> CodeExampleAddAction<'src> {
CodeExampleAddAction::Reset { code: self.lines }
}
}
#[derive(Debug)]
struct CodeExampleMarkdown<'src> {
lines: Vec<CodeExampleLine<'src>>,
opening_fence_indent: Indentation,
fence_kind: MarkdownFenceKind,
fence_len: usize,
}
impl<'src> CodeExampleMarkdown<'src> {
fn new(original: InputDocstringLine<'src>) -> Option<CodeExampleMarkdown<'src>> {
static FENCE_START: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?xm)
^
(?:
# In the backtick case, info strings (following the fence)
# cannot contain backticks themselves, since it would
# introduce ambiguity with parsing inline code. In other
# words, if we didn't specifically exclude matching `
# in the info string for backtick fences, then we might
# erroneously consider something to be a code fence block
# that is actually inline code.
#
# NOTE: The `ticklang` and `tildlang` capture groups are
# currently unused, but there was some discussion about not
# assuming unlabeled blocks were Python. At the time of
# writing, we do assume unlabeled blocks are Python, but
# one could inspect the `ticklang` and `tildlang` capture
# groups to determine whether the block is labeled or not.
(?<ticks>```+)(?:\s*(?<ticklang>(?i:python|py|python3|py3))[^`]*)?
|
(?<tilds>~~~+)(?:\s*(?<tildlang>(?i:python|py|python3|py3))\p{any}*)?
)
$
",
)
.unwrap()
});
let (opening_fence_indent, rest) = indent_with_suffix(original.line);
if !rest.starts_with("```") && !rest.starts_with("~~~") {
return None;
}
let caps = FENCE_START.captures(rest)?;
let (fence_kind, fence_len) = if let Some(ticks) = caps.name("ticks") {
(MarkdownFenceKind::Backtick, ticks.as_str().chars().count())
} else {
let tildes = caps
.name("tilds")
.expect("no ticks means it must be tildes");
(MarkdownFenceKind::Tilde, tildes.as_str().chars().count())
};
Some(CodeExampleMarkdown {
lines: vec![],
opening_fence_indent: Indentation::from_str(opening_fence_indent),
fence_kind,
fence_len,
})
}
fn add_code_line(
mut self,
original: InputDocstringLine<'src>,
queue: &mut VecDeque<CodeExampleAddAction<'src>>,
) -> Option<CodeExampleMarkdown<'src>> {
if self.is_end(original) {
queue.push_back(self.into_format_action());
queue.push_back(CodeExampleAddAction::Print { original });
return None;
}
if !original.line.trim_whitespace().is_empty()
&& Indentation::from_str(original.line) < self.opening_fence_indent
{
queue.push_back(self.into_reset_action());
queue.push_back(CodeExampleAddAction::Print { original });
return None;
}
self.push(original);
queue.push_back(CodeExampleAddAction::Kept);
Some(self)
}
fn is_end(&self, original: InputDocstringLine<'src>) -> bool {
let (_, rest) = indent_with_suffix(original.line);
if !rest.starts_with("```") && !rest.starts_with("~~~") {
return false;
}
let fence_len = rest
.chars()
.take_while(|&ch| ch == self.fence_kind.to_char())
.count();
if fence_len < self.fence_len {
return false;
}
assert!(
self.fence_kind.to_char().is_ascii(),
"fence char should be ASCII",
);
if !rest[fence_len..].chars().all(is_python_whitespace) {
return false;
}
true
}
fn push(&mut self, original: InputDocstringLine<'src>) {
let code = self.opening_fence_indent.trim_start_str(original.line);
self.lines.push(CodeExampleLine { original, code });
}
fn into_format_action(self) -> CodeExampleAddAction<'src> {
CodeExampleAddAction::Format {
kind: CodeExampleKind::Markdown(self),
}
}
fn into_reset_action(self) -> CodeExampleAddAction<'src> {
CodeExampleAddAction::Reset { code: self.lines }
}
}
#[derive(Clone, Copy, Debug)]
enum MarkdownFenceKind {
Backtick,
Tilde,
}
impl MarkdownFenceKind {
fn to_char(self) -> char {
match self {
MarkdownFenceKind::Backtick => '`',
MarkdownFenceKind::Tilde => '~',
}
}
}
#[derive(Debug)]
struct CodeExampleLine<'src> {
original: InputDocstringLine<'src>,
code: &'src str,
}
#[derive(Debug)]
enum CodeExampleAddAction<'src> {
Print { original: InputDocstringLine<'src> },
Kept,
Format {
kind: CodeExampleKind<'src>,
},
Reset {
code: Vec<CodeExampleLine<'src>>,
},
}
fn docstring_format_source(
options: crate::PyFormatOptions,
docstring_quote_style: Quote,
source: &str,
) -> Result<Printed, FormatModuleError> {
let source_type = options.source_type();
let parsed = ruff_python_parser::parse(source, ParseOptions::from(source_type))?;
let trivia = TriviaRanges::from(parsed.tokens());
let source_code = ruff_formatter::SourceCode::new(source);
let comments = crate::Comments::from_ast(parsed.syntax(), source_code, &trivia);
let ctx = PyFormatContext::new(options, source, comments, &trivia, parsed.tokens())
.in_docstring(docstring_quote_style);
let formatted = crate::format!(ctx, [parsed.syntax().format()])?;
formatted
.context()
.comments()
.assert_all_formatted(source_code);
Ok(formatted.print()?)
}
pub(super) fn needs_chaperone_space(flags: AnyStringFlags, trim_end: &str) -> bool {
if count_consecutive_chars_from_end(trim_end, '\\') % 2 == 1 {
return true;
}
if flags.is_triple_quoted() {
if let Some(before_quote) = trim_end.strip_suffix(flags.quote_style().as_char()) {
if count_consecutive_chars_from_end(before_quote, '\\').is_multiple_of(2) {
return true;
}
}
}
false
}
fn count_consecutive_chars_from_end(s: &str, target: char) -> usize {
s.chars().rev().take_while(|c| *c == target).count()
}
#[derive(Copy, Clone, Debug)]
enum Indentation {
Spaces(usize),
Tabs(usize),
TabSpaces { tabs: usize, spaces: usize },
SpacesTabs { spaces: usize, tabs: usize },
Mixed {
width: usize,
len: TextSize,
},
}
impl Indentation {
const TAB_INDENT_WIDTH: usize = 8;
fn from_str(s: &str) -> Self {
let mut iter = s.chars().peekable();
let spaces = iter.peeking_take_while(|c| *c == ' ').count();
let tabs = iter.peeking_take_while(|c| *c == '\t').count();
if tabs == 0 {
return Self::Spaces(spaces);
}
let align_spaces = iter.peeking_take_while(|c| *c == ' ').count();
if spaces == 0 {
if align_spaces == 0 {
return Self::Tabs(tabs);
}
if iter.peek().copied() != Some('\t') {
return Self::TabSpaces {
tabs,
spaces: align_spaces,
};
}
} else if align_spaces == 0 {
return Self::SpacesTabs { spaces, tabs };
}
let mut width = spaces + tabs * Self::TAB_INDENT_WIDTH + align_spaces;
let mut len = TextSize::try_from(spaces + tabs + align_spaces).unwrap();
for char in iter {
if char == '\t' {
width += Self::TAB_INDENT_WIDTH - (width.rem_euclid(Self::TAB_INDENT_WIDTH));
len += '\t'.text_len();
} else if char.is_whitespace() {
width += char.len_utf8();
len += char.text_len();
} else {
break;
}
}
Self::Mixed { width, len }
}
const fn columns(self) -> usize {
match self {
Self::Spaces(count) => count,
Self::Tabs(count) => count * Self::TAB_INDENT_WIDTH,
Self::TabSpaces { tabs, spaces } => tabs * Self::TAB_INDENT_WIDTH + spaces,
Self::SpacesTabs { spaces, tabs } => {
let mut indent = spaces;
indent += Self::TAB_INDENT_WIDTH - indent.rem_euclid(Self::TAB_INDENT_WIDTH);
indent + (tabs - 1) * Self::TAB_INDENT_WIDTH
}
Self::Mixed { width, .. } => width,
}
}
fn text_len(self) -> TextSize {
let len = match self {
Self::Spaces(count) => count,
Self::Tabs(count) => count,
Self::TabSpaces { tabs, spaces } => tabs + spaces,
Self::SpacesTabs { spaces, tabs } => spaces + tabs,
Self::Mixed { len, .. } => return len,
};
TextSize::try_from(len).unwrap()
}
fn trim_start(self, rhs: Self) -> Option<Self> {
let (left_tabs, left_spaces) = match self {
Self::Spaces(spaces) => (0usize, spaces),
Self::Tabs(tabs) => (tabs, 0usize),
Self::TabSpaces { tabs, spaces } => (tabs, spaces),
Self::SpacesTabs {
spaces: left_spaces,
tabs: left_tabs,
} => {
return match rhs {
Self::Spaces(right_spaces) => {
left_spaces.checked_sub(right_spaces).map(|spaces| {
if spaces == 0 {
Self::Tabs(left_tabs)
} else {
Self::SpacesTabs {
tabs: left_tabs,
spaces,
}
}
})
}
Self::SpacesTabs {
spaces: right_spaces,
tabs: right_tabs,
} => left_spaces.checked_sub(right_spaces).and_then(|spaces| {
let tabs = left_tabs.checked_sub(right_tabs)?;
Some(if spaces == 0 {
if tabs == 0 {
Self::Spaces(0)
} else {
Self::Tabs(tabs)
}
} else {
Self::SpacesTabs { spaces, tabs }
})
}),
_ => None,
};
}
Self::Mixed { .. } => return None,
};
let (right_tabs, right_spaces) = match rhs {
Self::Spaces(spaces) => (0usize, spaces),
Self::Tabs(tabs) => (tabs, 0usize),
Self::TabSpaces { tabs, spaces } => (tabs, spaces),
Self::SpacesTabs { .. } | Self::Mixed { .. } => return None,
};
let tabs = left_tabs.checked_sub(right_tabs)?;
let spaces = left_spaces.checked_sub(right_spaces)?;
Some(if tabs == 0 {
Self::Spaces(spaces)
} else if spaces == 0 {
Self::Tabs(tabs)
} else {
Self::TabSpaces { tabs, spaces }
})
}
fn trim_start_str(self, line: &str) -> &str {
let mut seen_indent_len = 0;
let mut trimmed = line;
let indent_len = self.columns();
for char in line.chars() {
if seen_indent_len >= indent_len {
return trimmed;
}
if char == '\t' {
seen_indent_len +=
Self::TAB_INDENT_WIDTH - (seen_indent_len.rem_euclid(Self::TAB_INDENT_WIDTH));
trimmed = &trimmed[1..];
} else if char.is_whitespace() {
seen_indent_len += char.len_utf8();
trimmed = &trimmed[char.len_utf8()..];
} else {
break;
}
}
trimmed
}
const fn is_spaces_tabs(self) -> bool {
matches!(self, Self::SpacesTabs { .. })
}
}
impl PartialOrd for Indentation {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.columns().cmp(&other.columns()))
}
}
impl PartialEq for Indentation {
fn eq(&self, other: &Self) -> bool {
self.columns() == other.columns()
}
}
impl Default for Indentation {
fn default() -> Self {
Self::Spaces(0)
}
}
fn indent_with_suffix(line: &str) -> (&str, &str) {
let suffix = line.trim_whitespace_start();
let indent_len = line
.len()
.checked_sub(suffix.len())
.expect("suffix <= line");
let indent = &line[..indent_len];
(indent, suffix)
}
fn is_rst_option(line: &str) -> bool {
let line = line.trim_start();
if !line.starts_with(':') {
return false;
}
line.chars()
.take_while(|&ch| !is_python_whitespace(ch))
.any(|ch| ch == ':')
}
#[cfg(test)]
mod tests {
use crate::string::docstring::Indentation;
#[test]
fn indentation_like_black() {
assert_eq!(Indentation::from_str("\t \t \t").columns(), 24);
assert_eq!(Indentation::from_str("\t \t").columns(), 24);
assert_eq!(Indentation::from_str("\t\t\t").columns(), 24);
assert_eq!(Indentation::from_str(" ").columns(), 4);
}
}