mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-14 15:40:51 +00:00
Reduce memory usage of Docstring
struct (#16183)
This commit is contained in:
parent
93aff36147
commit
61fef0a64a
15 changed files with 151 additions and 90 deletions
|
@ -1,10 +1,8 @@
|
||||||
use ruff_python_ast::str::raw_contents_range;
|
|
||||||
use ruff_python_semantic::all::DunderAllName;
|
use ruff_python_semantic::all::DunderAllName;
|
||||||
use ruff_python_semantic::{
|
use ruff_python_semantic::{
|
||||||
BindingKind, ContextualizedDefinition, Definition, Export, Member, MemberKind,
|
BindingKind, ContextualizedDefinition, Definition, Export, Member, MemberKind,
|
||||||
};
|
};
|
||||||
use ruff_source_file::LineRanges;
|
use ruff_text_size::Ranged;
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::codes::Rule;
|
use crate::codes::Rule;
|
||||||
|
@ -184,14 +182,9 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let contents = checker.locator().slice(string_literal);
|
// If the `ExprStringLiteral` has multiple parts, it is implicitly concatenated.
|
||||||
|
// We don't support recognising such strings as docstrings in our model currently.
|
||||||
let indentation = checker.locator().slice(TextRange::new(
|
let [sole_string_part] = string_literal.value.as_slice() else {
|
||||||
checker.locator.line_start(string_literal.start()),
|
|
||||||
string_literal.start(),
|
|
||||||
));
|
|
||||||
|
|
||||||
if string_literal.value.is_implicit_concatenated() {
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let location = checker
|
let location = checker
|
||||||
.locator
|
.locator
|
||||||
|
@ -203,16 +196,12 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||||
location.column
|
location.column
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
// SAFETY: Safe for docstrings that pass `should_ignore_docstring`.
|
|
||||||
let body_range = raw_contents_range(contents).unwrap();
|
|
||||||
let docstring = Docstring {
|
let docstring = Docstring {
|
||||||
definition,
|
definition,
|
||||||
expr: string_literal,
|
expr: sole_string_part,
|
||||||
contents,
|
source: checker.source(),
|
||||||
body_range,
|
|
||||||
indentation,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if !pydocstyle::rules::not_empty(checker, &docstring) {
|
if !pydocstyle::rules::not_empty(checker, &docstring) {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use ruff_python_ast::ExprStringLiteral;
|
use ruff_python_ast::{self as ast, StringFlags};
|
||||||
use ruff_python_semantic::Definition;
|
use ruff_python_semantic::Definition;
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_source_file::LineRanges;
|
||||||
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
|
|
||||||
pub(crate) mod extraction;
|
pub(crate) mod extraction;
|
||||||
pub(crate) mod google;
|
pub(crate) mod google;
|
||||||
|
@ -15,26 +16,71 @@ pub(crate) mod styles;
|
||||||
pub(crate) struct Docstring<'a> {
|
pub(crate) struct Docstring<'a> {
|
||||||
pub(crate) definition: &'a Definition<'a>,
|
pub(crate) definition: &'a Definition<'a>,
|
||||||
/// The literal AST node representing the docstring.
|
/// The literal AST node representing the docstring.
|
||||||
pub(crate) expr: &'a ExprStringLiteral,
|
pub(crate) expr: &'a ast::StringLiteral,
|
||||||
/// The content of the docstring, including the leading and trailing quotes.
|
/// The source file the docstring was defined in.
|
||||||
pub(crate) contents: &'a str,
|
pub(crate) source: &'a str,
|
||||||
/// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`].
|
|
||||||
pub(crate) body_range: TextRange,
|
|
||||||
pub(crate) indentation: &'a str,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Docstring<'a> {
|
impl<'a> Docstring<'a> {
|
||||||
|
fn flags(&self) -> ast::StringLiteralFlags {
|
||||||
|
self.expr.flags
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contents of the docstring, including the opening and closing quotes.
|
||||||
|
pub(crate) fn contents(&self) -> &'a str {
|
||||||
|
&self.source[self.range()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contents of the docstring, excluding the opening and closing quotes.
|
||||||
pub(crate) fn body(&self) -> DocstringBody {
|
pub(crate) fn body(&self) -> DocstringBody {
|
||||||
DocstringBody { docstring: self }
|
DocstringBody { docstring: self }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn leading_quote(&self) -> &'a str {
|
/// Compute the start position of the docstring's opening line
|
||||||
&self.contents[TextRange::up_to(self.body_range.start())]
|
pub(crate) fn line_start(&self) -> TextSize {
|
||||||
|
self.source.line_start(self.start())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn triple_quoted(&self) -> bool {
|
/// Return the slice of source code that represents the indentation of the docstring's opening quotes.
|
||||||
let leading_quote = self.leading_quote();
|
pub(crate) fn compute_indentation(&self) -> &'a str {
|
||||||
leading_quote.ends_with("\"\"\"") || leading_quote.ends_with("'''")
|
&self.source[TextRange::new(self.line_start(), self.start())]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn quote_style(&self) -> ast::str::Quote {
|
||||||
|
self.flags().quote_style()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_raw_string(&self) -> bool {
|
||||||
|
self.flags().prefix().is_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_u_string(&self) -> bool {
|
||||||
|
self.flags().prefix().is_unicode()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_triple_quoted(&self) -> bool {
|
||||||
|
self.flags().is_triple_quoted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The docstring's prefixes as they exist in the original source code.
|
||||||
|
pub(crate) fn prefix_str(&self) -> &'a str {
|
||||||
|
// N.B. This will normally be exactly the same as what you might get from
|
||||||
|
// `self.flags().prefix().as_str()`, but doing it this way has a few small advantages.
|
||||||
|
// For example, the casing of the `u` prefix will be preserved if it's a u-string.
|
||||||
|
&self.source[TextRange::new(
|
||||||
|
self.start(),
|
||||||
|
self.start() + self.flags().prefix().text_len(),
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The docstring's "opener" (the string's prefix, if any, and its opening quotes).
|
||||||
|
pub(crate) fn opener(&self) -> &'a str {
|
||||||
|
&self.source[TextRange::new(self.start(), self.start() + self.flags().opener_len())]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The docstring's closing quotes.
|
||||||
|
pub(crate) fn closer(&self) -> &'a str {
|
||||||
|
&self.source[TextRange::new(self.end() - self.flags().closer_len(), self.end())]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,13 +97,13 @@ pub(crate) struct DocstringBody<'a> {
|
||||||
|
|
||||||
impl<'a> DocstringBody<'a> {
|
impl<'a> DocstringBody<'a> {
|
||||||
pub(crate) fn as_str(self) -> &'a str {
|
pub(crate) fn as_str(self) -> &'a str {
|
||||||
&self.docstring.contents[self.docstring.body_range]
|
&self.docstring.source[self.range()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ranged for DocstringBody<'_> {
|
impl Ranged for DocstringBody<'_> {
|
||||||
fn range(&self) -> TextRange {
|
fn range(&self) -> TextRange {
|
||||||
self.docstring.body_range + self.docstring.start()
|
self.docstring.expr.content_range()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,8 +59,7 @@ impl Violation for EscapeSequenceInDocstring {
|
||||||
|
|
||||||
/// D301
|
/// D301
|
||||||
pub(crate) fn backslashes(checker: &Checker, docstring: &Docstring) {
|
pub(crate) fn backslashes(checker: &Checker, docstring: &Docstring) {
|
||||||
// Docstring is already raw.
|
if docstring.is_raw_string() {
|
||||||
if docstring.leading_quote().contains(['r', 'R']) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,10 +98,10 @@ pub(crate) fn backslashes(checker: &Checker, docstring: &Docstring) {
|
||||||
if !matches!(*escaped_char, '\r' | '\n' | 'u' | 'U' | 'N') {
|
if !matches!(*escaped_char, '\r' | '\n' | 'u' | 'U' | 'N') {
|
||||||
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
|
let mut diagnostic = Diagnostic::new(EscapeSequenceInDocstring, docstring.range());
|
||||||
|
|
||||||
if !docstring.leading_quote().contains(['u', 'U']) {
|
if !docstring.is_u_string() {
|
||||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
|
||||||
"r".to_owned() + docstring.contents,
|
"r".to_string(),
|
||||||
docstring.range(),
|
docstring.start(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ impl Violation for MissingBlankLineAfterSummary {
|
||||||
pub(crate) fn blank_after_summary(checker: &Checker, docstring: &Docstring) {
|
pub(crate) fn blank_after_summary(checker: &Checker, docstring: &Docstring) {
|
||||||
let body = docstring.body();
|
let body = docstring.body();
|
||||||
|
|
||||||
if !docstring.triple_quoted() {
|
if !docstring.is_triple_quoted() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||||
use ruff_python_trivia::{indentation_at_offset, PythonWhitespace};
|
use ruff_python_trivia::{indentation_at_offset, PythonWhitespace};
|
||||||
use ruff_source_file::{Line, LineRanges, UniversalNewlineIterator};
|
use ruff_source_file::{Line, LineRanges, UniversalNewlineIterator};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
use ruff_text_size::{TextLen, TextRange};
|
use ruff_text_size::TextRange;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::docstrings::Docstring;
|
use crate::docstrings::Docstring;
|
||||||
|
@ -197,7 +197,7 @@ pub(crate) fn blank_before_after_class(checker: &Checker, docstring: &Docstring)
|
||||||
// Delete the blank line before the class.
|
// Delete the blank line before the class.
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
|
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
|
||||||
blank_lines_start,
|
blank_lines_start,
|
||||||
docstring.start() - docstring.indentation.text_len(),
|
docstring.line_start(),
|
||||||
)));
|
)));
|
||||||
checker.report_diagnostic(diagnostic);
|
checker.report_diagnostic(diagnostic);
|
||||||
}
|
}
|
||||||
|
@ -210,7 +210,7 @@ pub(crate) fn blank_before_after_class(checker: &Checker, docstring: &Docstring)
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
||||||
checker.stylist().line_ending().to_string(),
|
checker.stylist().line_ending().to_string(),
|
||||||
blank_lines_start,
|
blank_lines_start,
|
||||||
docstring.start() - docstring.indentation.text_len(),
|
docstring.line_start(),
|
||||||
)));
|
)));
|
||||||
checker.report_diagnostic(diagnostic);
|
checker.report_diagnostic(diagnostic);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||||
use ruff_python_trivia::PythonWhitespace;
|
use ruff_python_trivia::PythonWhitespace;
|
||||||
use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines};
|
use ruff_source_file::{UniversalNewlineIterator, UniversalNewlines};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
use ruff_text_size::{TextLen, TextRange};
|
use ruff_text_size::TextRange;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::docstrings::Docstring;
|
use crate::docstrings::Docstring;
|
||||||
|
@ -135,7 +135,7 @@ pub(crate) fn blank_before_after_function(checker: &Checker, docstring: &Docstri
|
||||||
// Delete the blank line before the docstring.
|
// Delete the blank line before the docstring.
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
|
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
|
||||||
blank_lines_start,
|
blank_lines_start,
|
||||||
docstring.start() - docstring.indentation.text_len(),
|
docstring.line_start(),
|
||||||
)));
|
)));
|
||||||
checker.report_diagnostic(diagnostic);
|
checker.report_diagnostic(diagnostic);
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,8 +179,9 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut has_seen_tab = docstring.indentation.contains('\t');
|
let docstring_indentation = docstring.compute_indentation();
|
||||||
let docstring_indent_size = docstring.indentation.chars().count();
|
let mut has_seen_tab = docstring_indentation.contains('\t');
|
||||||
|
let docstring_indent_size = docstring_indentation.chars().count();
|
||||||
|
|
||||||
// Lines, other than the last, that are over indented.
|
// Lines, other than the last, that are over indented.
|
||||||
let mut over_indented_lines = vec![];
|
let mut over_indented_lines = vec![];
|
||||||
|
@ -226,7 +227,7 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) {
|
||||||
let mut diagnostic =
|
let mut diagnostic =
|
||||||
Diagnostic::new(UnderIndentation, TextRange::empty(line.start()));
|
Diagnostic::new(UnderIndentation, TextRange::empty(line.start()));
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||||
clean_space(docstring.indentation),
|
clean_space(docstring_indentation),
|
||||||
TextRange::at(line.start(), line_indent.text_len()),
|
TextRange::at(line.start(), line_indent.text_len()),
|
||||||
)));
|
)));
|
||||||
checker.report_diagnostic(diagnostic);
|
checker.report_diagnostic(diagnostic);
|
||||||
|
@ -275,7 +276,7 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) {
|
||||||
if let Some(smallest_over_indent_size) = smallest_over_indent_size {
|
if let Some(smallest_over_indent_size) = smallest_over_indent_size {
|
||||||
for line in over_indented_lines {
|
for line in over_indented_lines {
|
||||||
let line_indent = leading_space(&line);
|
let line_indent = leading_space(&line);
|
||||||
let indent = clean_space(docstring.indentation);
|
let indent = clean_space(docstring_indentation);
|
||||||
|
|
||||||
// We report over-indentation on every line. This isn't great, but
|
// We report over-indentation on every line. This isn't great, but
|
||||||
// enables the fix capability.
|
// enables the fix capability.
|
||||||
|
@ -324,7 +325,7 @@ pub(crate) fn indent(checker: &Checker, docstring: &Docstring) {
|
||||||
if last_line_over_indent > 0 && is_indent_only {
|
if last_line_over_indent > 0 && is_indent_only {
|
||||||
let mut diagnostic =
|
let mut diagnostic =
|
||||||
Diagnostic::new(OverIndentation, TextRange::empty(last.start()));
|
Diagnostic::new(OverIndentation, TextRange::empty(last.start()));
|
||||||
let indent = clean_space(docstring.indentation);
|
let indent = clean_space(docstring_indentation);
|
||||||
let range = TextRange::at(last.start(), line_indent.text_len());
|
let range = TextRange::at(last.start(), line_indent.text_len());
|
||||||
let edit = if indent.is_empty() {
|
let edit = if indent.is_empty() {
|
||||||
Edit::range_deletion(range)
|
Edit::range_deletion(range)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||||
use ruff_python_ast::str::{is_triple_quote, leading_quote};
|
use ruff_python_ast::str::is_triple_quote;
|
||||||
use ruff_python_semantic::Definition;
|
use ruff_python_semantic::Definition;
|
||||||
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlineIterator};
|
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline, UniversalNewlineIterator};
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
|
@ -137,7 +139,6 @@ impl AlwaysFixableViolation for MultiLineSummarySecondLine {
|
||||||
|
|
||||||
/// D212, D213
|
/// D212, D213
|
||||||
pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring) {
|
pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring) {
|
||||||
let contents = docstring.contents;
|
|
||||||
let body = docstring.body();
|
let body = docstring.body();
|
||||||
|
|
||||||
if NewlineWithTrailingNewline::from(body.as_str())
|
if NewlineWithTrailingNewline::from(body.as_str())
|
||||||
|
@ -146,7 +147,8 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let mut content_lines = UniversalNewlineIterator::with_offset(contents, docstring.start());
|
let mut content_lines =
|
||||||
|
UniversalNewlineIterator::with_offset(docstring.contents(), docstring.start());
|
||||||
|
|
||||||
let Some(first_line) = content_lines.next() else {
|
let Some(first_line) = content_lines.next() else {
|
||||||
return;
|
return;
|
||||||
|
@ -179,7 +181,7 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring)
|
||||||
} else {
|
} else {
|
||||||
if checker.enabled(Rule::MultiLineSummarySecondLine) {
|
if checker.enabled(Rule::MultiLineSummarySecondLine) {
|
||||||
let mut diagnostic = Diagnostic::new(MultiLineSummarySecondLine, docstring.range());
|
let mut diagnostic = Diagnostic::new(MultiLineSummarySecondLine, docstring.range());
|
||||||
let mut indentation = String::from(docstring.indentation);
|
let mut indentation = Cow::Borrowed(docstring.compute_indentation());
|
||||||
let mut fixable = true;
|
let mut fixable = true;
|
||||||
if !indentation.chars().all(char::is_whitespace) {
|
if !indentation.chars().all(char::is_whitespace) {
|
||||||
fixable = false;
|
fixable = false;
|
||||||
|
@ -193,6 +195,7 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring)
|
||||||
.slice(TextRange::new(stmt_line_start, member.start()));
|
.slice(TextRange::new(stmt_line_start, member.start()));
|
||||||
|
|
||||||
if stmt_indentation.chars().all(char::is_whitespace) {
|
if stmt_indentation.chars().all(char::is_whitespace) {
|
||||||
|
let indentation = indentation.to_mut();
|
||||||
indentation.clear();
|
indentation.clear();
|
||||||
indentation.push_str(stmt_indentation);
|
indentation.push_str(stmt_indentation);
|
||||||
indentation.push_str(checker.stylist().indentation());
|
indentation.push_str(checker.stylist().indentation());
|
||||||
|
@ -202,14 +205,16 @@ pub(crate) fn multi_line_summary_start(checker: &Checker, docstring: &Docstring)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fixable {
|
if fixable {
|
||||||
let prefix = leading_quote(contents).unwrap();
|
|
||||||
// Use replacement instead of insert to trim possible whitespace between leading
|
// Use replacement instead of insert to trim possible whitespace between leading
|
||||||
// quote and text.
|
// quote and text.
|
||||||
let repl = format!(
|
let repl = format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
checker.stylist().line_ending().as_str(),
|
checker.stylist().line_ending().as_str(),
|
||||||
indentation,
|
indentation,
|
||||||
first_line.strip_prefix(prefix).unwrap().trim_start()
|
first_line
|
||||||
|
.strip_prefix(docstring.opener())
|
||||||
|
.unwrap()
|
||||||
|
.trim_start()
|
||||||
);
|
);
|
||||||
|
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
||||||
|
|
|
@ -59,10 +59,10 @@ impl AlwaysFixableViolation for NewLineAfterLastParagraph {
|
||||||
|
|
||||||
/// D209
|
/// D209
|
||||||
pub(crate) fn newline_after_last_paragraph(checker: &Checker, docstring: &Docstring) {
|
pub(crate) fn newline_after_last_paragraph(checker: &Checker, docstring: &Docstring) {
|
||||||
let contents = docstring.contents;
|
let contents = docstring.contents();
|
||||||
let body = docstring.body();
|
let body = docstring.body();
|
||||||
|
|
||||||
if !docstring.triple_quoted() {
|
if !docstring.is_triple_quoted() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ pub(crate) fn newline_after_last_paragraph(checker: &Checker, docstring: &Docstr
|
||||||
let content = format!(
|
let content = format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
checker.stylist().line_ending().as_str(),
|
checker.stylist().line_ending().as_str(),
|
||||||
clean_space(docstring.indentation)
|
clean_space(docstring.compute_indentation())
|
||||||
);
|
);
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
||||||
content,
|
content,
|
||||||
|
|
|
@ -63,7 +63,7 @@ pub(crate) fn no_surrounding_whitespace(checker: &Checker, docstring: &Docstring
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut diagnostic = Diagnostic::new(SurroundingWhitespace, docstring.range());
|
let mut diagnostic = Diagnostic::new(SurroundingWhitespace, docstring.range());
|
||||||
let quote = docstring.contents.chars().last().unwrap();
|
let quote = docstring.quote_style().as_char();
|
||||||
// If removing whitespace would lead to an invalid string of quote
|
// If removing whitespace would lead to an invalid string of quote
|
||||||
// characters, avoid applying the fix.
|
// characters, avoid applying the fix.
|
||||||
if !trimmed.ends_with(quote) && !trimmed.starts_with(quote) && !ends_with_backslash(trimmed) {
|
if !trimmed.ends_with(quote) && !trimmed.starts_with(quote) && !ends_with_backslash(trimmed) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||||
use ruff_python_ast::str::{leading_quote, trailing_quote};
|
|
||||||
use ruff_source_file::NewlineWithTrailingNewline;
|
use ruff_source_file::NewlineWithTrailingNewline;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
|
@ -64,24 +63,26 @@ pub(crate) fn one_liner(checker: &Checker, docstring: &Docstring) {
|
||||||
|
|
||||||
if non_empty_line_count == 1 && line_count > 1 {
|
if non_empty_line_count == 1 && line_count > 1 {
|
||||||
let mut diagnostic = Diagnostic::new(UnnecessaryMultilineDocstring, docstring.range());
|
let mut diagnostic = Diagnostic::new(UnnecessaryMultilineDocstring, docstring.range());
|
||||||
if let (Some(leading), Some(trailing)) = (
|
|
||||||
leading_quote(docstring.contents),
|
// If removing whitespace would lead to an invalid string of quote
|
||||||
trailing_quote(docstring.contents),
|
// characters, avoid applying the fix.
|
||||||
) {
|
let body = docstring.body();
|
||||||
// If removing whitespace would lead to an invalid string of quote
|
let trimmed = body.trim();
|
||||||
// characters, avoid applying the fix.
|
let quote_char = docstring.quote_style().as_char();
|
||||||
let body = docstring.body();
|
if trimmed.chars().rev().take_while(|c| *c == '\\').count() % 2 == 0
|
||||||
let trimmed = body.trim();
|
&& !trimmed.ends_with(quote_char)
|
||||||
if trimmed.chars().rev().take_while(|c| *c == '\\').count() % 2 == 0
|
&& !trimmed.starts_with(quote_char)
|
||||||
&& !trimmed.ends_with(trailing.chars().last().unwrap())
|
{
|
||||||
&& !trimmed.starts_with(leading.chars().last().unwrap())
|
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||||
{
|
format!(
|
||||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
"{leading}{trimmed}{trailing}",
|
||||||
format!("{leading}{trimmed}{trailing}"),
|
leading = docstring.opener(),
|
||||||
docstring.range(),
|
trailing = docstring.closer()
|
||||||
)));
|
),
|
||||||
}
|
docstring.range(),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
checker.report_diagnostic(diagnostic);
|
checker.report_diagnostic(diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1399,7 +1399,8 @@ fn blanks_and_section_underline(
|
||||||
|
|
||||||
if checker.enabled(Rule::OverindentedSectionUnderline) {
|
if checker.enabled(Rule::OverindentedSectionUnderline) {
|
||||||
let leading_space = leading_space(&non_blank_line);
|
let leading_space = leading_space(&non_blank_line);
|
||||||
if leading_space.len() > docstring.indentation.len() {
|
let docstring_indentation = docstring.compute_indentation();
|
||||||
|
if leading_space.len() > docstring_indentation.len() {
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
OverindentedSectionUnderline {
|
OverindentedSectionUnderline {
|
||||||
name: context.section_name().to_string(),
|
name: context.section_name().to_string(),
|
||||||
|
@ -1412,7 +1413,7 @@ fn blanks_and_section_underline(
|
||||||
blank_lines_end,
|
blank_lines_end,
|
||||||
leading_space.text_len() + TextSize::from(1),
|
leading_space.text_len() + TextSize::from(1),
|
||||||
);
|
);
|
||||||
let contents = clean_space(docstring.indentation);
|
let contents = clean_space(docstring_indentation);
|
||||||
diagnostic.set_fix(Fix::safe_edit(if contents.is_empty() {
|
diagnostic.set_fix(Fix::safe_edit(if contents.is_empty() {
|
||||||
Edit::range_deletion(range)
|
Edit::range_deletion(range)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1540,7 +1541,7 @@ fn blanks_and_section_underline(
|
||||||
let content = format!(
|
let content = format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
checker.stylist().line_ending().as_str(),
|
checker.stylist().line_ending().as_str(),
|
||||||
clean_space(docstring.indentation),
|
clean_space(docstring.compute_indentation()),
|
||||||
"-".repeat(context.section_name().len()),
|
"-".repeat(context.section_name().len()),
|
||||||
);
|
);
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||||
|
@ -1621,7 +1622,7 @@ fn blanks_and_section_underline(
|
||||||
let content = format!(
|
let content = format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
checker.stylist().line_ending().as_str(),
|
checker.stylist().line_ending().as_str(),
|
||||||
clean_space(docstring.indentation),
|
clean_space(docstring.compute_indentation()),
|
||||||
"-".repeat(context.section_name().len()),
|
"-".repeat(context.section_name().len()),
|
||||||
);
|
);
|
||||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||||
|
@ -1671,7 +1672,8 @@ fn common_section(
|
||||||
|
|
||||||
if checker.enabled(Rule::OverindentedSection) {
|
if checker.enabled(Rule::OverindentedSection) {
|
||||||
let leading_space = leading_space(context.summary_line());
|
let leading_space = leading_space(context.summary_line());
|
||||||
if leading_space.len() > docstring.indentation.len() {
|
let docstring_indentation = docstring.compute_indentation();
|
||||||
|
if leading_space.len() > docstring_indentation.len() {
|
||||||
let section_range = context.section_name_range();
|
let section_range = context.section_name_range();
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
OverindentedSection {
|
OverindentedSection {
|
||||||
|
@ -1681,7 +1683,7 @@ fn common_section(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Replace the existing indentation with whitespace of the appropriate length.
|
// Replace the existing indentation with whitespace of the appropriate length.
|
||||||
let content = clean_space(docstring.indentation);
|
let content = clean_space(docstring_indentation);
|
||||||
let fix_range = TextRange::at(context.start(), leading_space.text_len());
|
let fix_range = TextRange::at(context.start(), leading_space.text_len());
|
||||||
diagnostic.set_fix(Fix::safe_edit(if content.is_empty() {
|
diagnostic.set_fix(Fix::safe_edit(if content.is_empty() {
|
||||||
Edit::range_deletion(fix_range)
|
Edit::range_deletion(fix_range)
|
||||||
|
@ -1738,7 +1740,7 @@ fn common_section(
|
||||||
format!(
|
format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
line_end.repeat(2 - num_blank_lines),
|
line_end.repeat(2 - num_blank_lines),
|
||||||
docstring.indentation
|
docstring.compute_indentation()
|
||||||
),
|
),
|
||||||
context.end() - del_len,
|
context.end() - del_len,
|
||||||
context.end(),
|
context.end(),
|
||||||
|
|
|
@ -64,9 +64,8 @@ impl Violation for TripleSingleQuotes {
|
||||||
|
|
||||||
/// D300
|
/// D300
|
||||||
pub(crate) fn triple_quotes(checker: &Checker, docstring: &Docstring) {
|
pub(crate) fn triple_quotes(checker: &Checker, docstring: &Docstring) {
|
||||||
let leading_quote = docstring.leading_quote();
|
let opener = docstring.opener();
|
||||||
|
let prefixes = docstring.prefix_str();
|
||||||
let prefixes = leading_quote.trim_end_matches(['\'', '"']).to_owned();
|
|
||||||
|
|
||||||
let expected_quote = if docstring.body().contains("\"\"\"") {
|
let expected_quote = if docstring.body().contains("\"\"\"") {
|
||||||
if docstring.body().contains("\'\'\'") {
|
if docstring.body().contains("\'\'\'") {
|
||||||
|
@ -79,7 +78,7 @@ pub(crate) fn triple_quotes(checker: &Checker, docstring: &Docstring) {
|
||||||
|
|
||||||
match expected_quote {
|
match expected_quote {
|
||||||
Quote::Single => {
|
Quote::Single => {
|
||||||
if !leading_quote.ends_with("'''") {
|
if !opener.ends_with("'''") {
|
||||||
let mut diagnostic =
|
let mut diagnostic =
|
||||||
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
|
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
|
||||||
|
|
||||||
|
@ -95,7 +94,7 @@ pub(crate) fn triple_quotes(checker: &Checker, docstring: &Docstring) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Quote::Double => {
|
Quote::Double => {
|
||||||
if !leading_quote.ends_with("\"\"\"") {
|
if !opener.ends_with("\"\"\"") {
|
||||||
let mut diagnostic =
|
let mut diagnostic =
|
||||||
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
|
Diagnostic::new(TripleSingleQuotes { expected_quote }, docstring.range());
|
||||||
|
|
||||||
|
|
|
@ -1645,6 +1645,16 @@ impl StringLiteral {
|
||||||
flags: StringLiteralFlags::empty().with_invalid(),
|
flags: StringLiteralFlags::empty().with_invalid(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The range of the string literal's contents.
|
||||||
|
///
|
||||||
|
/// This excludes any prefixes, opening quotes or closing quotes.
|
||||||
|
pub fn content_range(&self) -> TextRange {
|
||||||
|
TextRange::new(
|
||||||
|
self.start() + self.flags.opener_len(),
|
||||||
|
self.end() - self.flags.closer_len(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StringLiteral> for Expr {
|
impl From<StringLiteral> for Expr {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use ruff_text_size::TextSize;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
/// Enumerations of the valid prefixes a string literal can have.
|
/// Enumerations of the valid prefixes a string literal can have.
|
||||||
|
@ -33,6 +35,13 @@ impl StringLiteralPrefix {
|
||||||
Self::Raw { uppercase: false } => "r",
|
Self::Raw { uppercase: false } => "r",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn text_len(self) -> TextSize {
|
||||||
|
match self {
|
||||||
|
Self::Empty => TextSize::new(0),
|
||||||
|
Self::Unicode | Self::Raw { .. } => TextSize::new(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for StringLiteralPrefix {
|
impl fmt::Display for StringLiteralPrefix {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue