mirror of https://github.com/astral-sh/ruff
Add support for attribute docstring in the semantic model (#11315)
## Summary This PR adds updates the semantic model to detect attribute docstring. Refer to [PEP 258](https://peps.python.org/pep-0258/#attribute-docstrings) for the definition of an attribute docstring. This PR doesn't add full support for it but only considers string literals as attribute docstring for the following cases: 1. A string literal following an assignment statement in the **global scope**. 2. A global class attribute For an assignment statement, it's considered an attribute docstring only if the target expression is a name expression (`x = 1`). So, chained assignment, multiple assignment or unpacking, and starred expression, which are all valid in the target position, aren't considered here. In `__init__` method, an assignment to the `self` variable like `self.x = 1` is also a candidate for an attribute docstring. **This PR does not support this position.** ## Test Plan I used the following source code along with a print statement to verify that the attribute docstring detection is correct. Refer to the PR description for the code snippet. I'll add this in the follow-up PR (https://github.com/astral-sh/ruff/pull/11302) which uses this method.
This commit is contained in:
parent
35ba3c91ce
commit
f79c980e17
|
|
@ -73,7 +73,7 @@ mod annotation;
|
||||||
mod deferred;
|
mod deferred;
|
||||||
|
|
||||||
/// State representing whether a docstring is expected or not for the next statement.
|
/// State representing whether a docstring is expected or not for the next statement.
|
||||||
#[derive(Default, Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
enum DocstringState {
|
enum DocstringState {
|
||||||
/// The next statement is expected to be a docstring, but not necessarily so.
|
/// The next statement is expected to be a docstring, but not necessarily so.
|
||||||
///
|
///
|
||||||
|
|
@ -92,15 +92,84 @@ enum DocstringState {
|
||||||
/// For `Foo`, the state is expected when the checker is visiting the class
|
/// For `Foo`, the state is expected when the checker is visiting the class
|
||||||
/// body but isn't going to be present. While, for `bar` function, the docstring
|
/// body but isn't going to be present. While, for `bar` function, the docstring
|
||||||
/// is expected and present.
|
/// is expected and present.
|
||||||
#[default]
|
Expected(ExpectedDocstringKind),
|
||||||
Expected,
|
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for DocstringState {
|
||||||
|
/// Returns the default docstring state which is to expect a module-level docstring.
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Expected(ExpectedDocstringKind::Module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DocstringState {
|
impl DocstringState {
|
||||||
/// Returns `true` if the next statement is expected to be a docstring.
|
/// Returns the docstring kind if the state is expecting a docstring.
|
||||||
const fn is_expected(self) -> bool {
|
const fn expected_kind(self) -> Option<ExpectedDocstringKind> {
|
||||||
matches!(self, DocstringState::Expected)
|
match self {
|
||||||
|
DocstringState::Expected(kind) => Some(kind),
|
||||||
|
DocstringState::Other => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kind of an expected docstring.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
enum ExpectedDocstringKind {
|
||||||
|
/// A module-level docstring.
|
||||||
|
///
|
||||||
|
/// For example,
|
||||||
|
/// ```python
|
||||||
|
/// """This is a module-level docstring."""
|
||||||
|
///
|
||||||
|
/// a = 1
|
||||||
|
/// ```
|
||||||
|
Module,
|
||||||
|
|
||||||
|
/// A class-level docstring.
|
||||||
|
///
|
||||||
|
/// For example,
|
||||||
|
/// ```python
|
||||||
|
/// class Foo:
|
||||||
|
/// """This is the docstring for `Foo` class."""
|
||||||
|
///
|
||||||
|
/// def __init__(self) -> None:
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
Class,
|
||||||
|
|
||||||
|
/// A function-level docstring.
|
||||||
|
///
|
||||||
|
/// For example,
|
||||||
|
/// ```python
|
||||||
|
/// def foo():
|
||||||
|
/// """This is the docstring for `foo` function."""
|
||||||
|
/// pass
|
||||||
|
/// ```
|
||||||
|
Function,
|
||||||
|
|
||||||
|
/// An attribute-level docstring.
|
||||||
|
///
|
||||||
|
/// For example,
|
||||||
|
/// ```python
|
||||||
|
/// a = 1
|
||||||
|
/// """This is the docstring for `a` variable."""
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// class Foo:
|
||||||
|
/// b = 1
|
||||||
|
/// """This is the docstring for `Foo.b` class variable."""
|
||||||
|
/// ```
|
||||||
|
Attribute,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExpectedDocstringKind {
|
||||||
|
/// Returns the semantic model flag that represents the current docstring state.
|
||||||
|
const fn as_flag(self) -> SemanticModelFlags {
|
||||||
|
match self {
|
||||||
|
ExpectedDocstringKind::Attribute => SemanticModelFlags::ATTRIBUTE_DOCSTRING,
|
||||||
|
_ => SemanticModelFlags::PEP_257_DOCSTRING,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,9 +452,9 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||||
|
|
||||||
// Update the semantic model if it is in a docstring. This should be done after the
|
// Update the semantic model if it is in a docstring. This should be done after the
|
||||||
// flags snapshot to ensure that it gets reset once the statement is analyzed.
|
// flags snapshot to ensure that it gets reset once the statement is analyzed.
|
||||||
if self.docstring_state.is_expected() {
|
if let Some(kind) = self.docstring_state.expected_kind() {
|
||||||
if is_docstring_stmt(stmt) {
|
if is_docstring_stmt(stmt) {
|
||||||
self.semantic.flags |= SemanticModelFlags::DOCSTRING;
|
self.semantic.flags |= kind.as_flag();
|
||||||
}
|
}
|
||||||
// Reset the state irrespective of whether the statement is a docstring or not.
|
// Reset the state irrespective of whether the statement is a docstring or not.
|
||||||
self.docstring_state = DocstringState::Other;
|
self.docstring_state = DocstringState::Other;
|
||||||
|
|
@ -709,7 +778,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the docstring state before visiting the class body.
|
// Set the docstring state before visiting the class body.
|
||||||
self.docstring_state = DocstringState::Expected;
|
self.docstring_state = DocstringState::Expected(ExpectedDocstringKind::Class);
|
||||||
self.visit_body(body);
|
self.visit_body(body);
|
||||||
|
|
||||||
let scope_id = self.semantic.scope_id;
|
let scope_id = self.semantic.scope_id;
|
||||||
|
|
@ -874,6 +943,24 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||||
_ => visitor::walk_stmt(self, stmt),
|
_ => visitor::walk_stmt(self, stmt),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if self.semantic().at_top_level() || self.semantic().current_scope().kind.is_class() {
|
||||||
|
match stmt {
|
||||||
|
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
|
||||||
|
if let [Expr::Name(_)] = targets.as_slice() {
|
||||||
|
self.docstring_state =
|
||||||
|
DocstringState::Expected(ExpectedDocstringKind::Attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
|
||||||
|
if target.is_name_expr() {
|
||||||
|
self.docstring_state =
|
||||||
|
DocstringState::Expected(ExpectedDocstringKind::Attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 3: Clean-up
|
// Step 3: Clean-up
|
||||||
|
|
||||||
// Step 4: Analysis
|
// Step 4: Analysis
|
||||||
|
|
@ -2122,7 +2209,7 @@ impl<'a> Checker<'a> {
|
||||||
|
|
||||||
self.visit_parameters(parameters);
|
self.visit_parameters(parameters);
|
||||||
// Set the docstring state before visiting the function body.
|
// Set the docstring state before visiting the function body.
|
||||||
self.docstring_state = DocstringState::Expected;
|
self.docstring_state = DocstringState::Expected(ExpectedDocstringKind::Function);
|
||||||
self.visit_body(body);
|
self.visit_body(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl AlwaysFixableViolation for AvoidableEscapedQuote {
|
||||||
|
|
||||||
/// Q003
|
/// Q003
|
||||||
pub(crate) fn avoidable_escaped_quote(checker: &mut Checker, string_like: StringLike) {
|
pub(crate) fn avoidable_escaped_quote(checker: &mut Checker, string_like: StringLike) {
|
||||||
if checker.semantic().in_docstring()
|
if checker.semantic().in_pep_257_docstring()
|
||||||
|| checker.semantic().in_string_type_definition()
|
|| checker.semantic().in_string_type_definition()
|
||||||
// This rule has support for strings nested inside another f-strings but they're checked
|
// This rule has support for strings nested inside another f-strings but they're checked
|
||||||
// via the outermost f-string. This means that we shouldn't be checking any nested string
|
// via the outermost f-string. This means that we shouldn't be checking any nested string
|
||||||
|
|
|
||||||
|
|
@ -460,7 +460,7 @@ pub(crate) fn check_string_quotes(checker: &mut Checker, string_like: StringLike
|
||||||
StringLike::FString(node) => node.value.iter().map(Ranged::range).collect(),
|
StringLike::FString(node) => node.value.iter().map(Ranged::range).collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if checker.semantic().in_docstring() {
|
if checker.semantic().in_pep_257_docstring() {
|
||||||
if checker.enabled(Rule::BadQuotesDocstring) {
|
if checker.enabled(Rule::BadQuotesDocstring) {
|
||||||
for range in ranges {
|
for range in ranges {
|
||||||
docstring(checker, range);
|
docstring(checker, range);
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ impl AlwaysFixableViolation for UnnecessaryEscapedQuote {
|
||||||
|
|
||||||
/// Q004
|
/// Q004
|
||||||
pub(crate) fn unnecessary_escaped_quote(checker: &mut Checker, string_like: StringLike) {
|
pub(crate) fn unnecessary_escaped_quote(checker: &mut Checker, string_like: StringLike) {
|
||||||
if checker.semantic().in_docstring() {
|
if checker.semantic().in_pep_257_docstring() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ pub(crate) fn ambiguous_unicode_character_comment(
|
||||||
|
|
||||||
/// RUF001, RUF002
|
/// RUF001, RUF002
|
||||||
pub(crate) fn ambiguous_unicode_character_string(checker: &mut Checker, string_like: StringLike) {
|
pub(crate) fn ambiguous_unicode_character_string(checker: &mut Checker, string_like: StringLike) {
|
||||||
let context = if checker.semantic().in_docstring() {
|
let context = if checker.semantic().in_pep_257_docstring() {
|
||||||
Context::Docstring
|
Context::Docstring
|
||||||
} else {
|
} else {
|
||||||
Context::String
|
Context::String
|
||||||
|
|
|
||||||
|
|
@ -1643,9 +1643,17 @@ impl<'a> SemanticModel<'a> {
|
||||||
.intersects(SemanticModelFlags::TYPE_CHECKING_BLOCK)
|
.intersects(SemanticModelFlags::TYPE_CHECKING_BLOCK)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the model is in a docstring.
|
/// Return `true` if the model is in a docstring as described in [PEP 257].
|
||||||
pub const fn in_docstring(&self) -> bool {
|
///
|
||||||
self.flags.intersects(SemanticModelFlags::DOCSTRING)
|
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
|
||||||
|
pub const fn in_pep_257_docstring(&self) -> bool {
|
||||||
|
self.flags.intersects(SemanticModelFlags::PEP_257_DOCSTRING)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the model is in an attribute docstring.
|
||||||
|
pub const fn in_attribute_docstring(&self) -> bool {
|
||||||
|
self.flags
|
||||||
|
.intersects(SemanticModelFlags::ATTRIBUTE_DOCSTRING)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if the model has traversed past the "top-of-file" import boundary.
|
/// Return `true` if the model has traversed past the "top-of-file" import boundary.
|
||||||
|
|
@ -2082,7 +2090,7 @@ bitflags! {
|
||||||
/// ```
|
/// ```
|
||||||
const COMPREHENSION_ASSIGNMENT = 1 << 20;
|
const COMPREHENSION_ASSIGNMENT = 1 << 20;
|
||||||
|
|
||||||
/// The model is in a module / class / function docstring.
|
/// The model is in a docstring as described in [PEP 257].
|
||||||
///
|
///
|
||||||
/// For example, the model could be visiting either the module, class,
|
/// For example, the model could be visiting either the module, class,
|
||||||
/// or function docstring in:
|
/// or function docstring in:
|
||||||
|
|
@ -2099,7 +2107,9 @@ bitflags! {
|
||||||
/// """Function docstring."""
|
/// """Function docstring."""
|
||||||
/// pass
|
/// pass
|
||||||
/// ```
|
/// ```
|
||||||
const DOCSTRING = 1 << 21;
|
///
|
||||||
|
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
|
||||||
|
const PEP_257_DOCSTRING = 1 << 21;
|
||||||
|
|
||||||
/// The model is visiting the r.h.s. of a module-level `__all__` definition.
|
/// The model is visiting the r.h.s. of a module-level `__all__` definition.
|
||||||
///
|
///
|
||||||
|
|
@ -2136,6 +2146,31 @@ bitflags! {
|
||||||
/// while traversing the AST. (This only happens in stub files.)
|
/// while traversing the AST. (This only happens in stub files.)
|
||||||
const DEFERRED_CLASS_BASE = 1 << 25;
|
const DEFERRED_CLASS_BASE = 1 << 25;
|
||||||
|
|
||||||
|
/// The model is in an attribute docstring.
|
||||||
|
///
|
||||||
|
/// An attribute docstring is a string literal immediately following an assignment or an
|
||||||
|
/// annotated assignment statement. The context in which this is valid are:
|
||||||
|
/// 1. At the top level of a module
|
||||||
|
/// 2. At the top level of a class definition i.e., a class attribute
|
||||||
|
///
|
||||||
|
/// For example:
|
||||||
|
/// ```python
|
||||||
|
/// a = 1
|
||||||
|
/// """This is an attribute docstring for `a` variable"""
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// class Foo:
|
||||||
|
/// b = 1
|
||||||
|
/// """This is an attribute docstring for `Foo.b` class variable"""
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Unlike other kinds of docstrings as described in [PEP 257], attribute docstrings are
|
||||||
|
/// discarded at runtime. However, they are used by some documentation renderers and
|
||||||
|
/// static-analysis tools.
|
||||||
|
///
|
||||||
|
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
|
||||||
|
const ATTRIBUTE_DOCSTRING = 1 << 26;
|
||||||
|
|
||||||
/// The context is in any type annotation.
|
/// The context is in any type annotation.
|
||||||
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
|
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue