Respect `__all__` imports when determining definition visibility (#4357)

This commit is contained in:
Charlie Marsh 2023-05-11 13:43:51 -04:00 committed by GitHub
parent 72e0ffc1ac
commit 9158f13ee6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 192 additions and 83 deletions

View File

@ -0,0 +1,18 @@
def public_func():
pass
def private_func():
pass
class PublicClass:
class PublicNestedClass:
pass
class PrivateClass:
pass
__all__ = ("public_func", "PublicClass")

View File

@ -28,7 +28,7 @@ use ruff_python_semantic::binding::{
Importation, StarImportation, SubmoduleImportation, Importation, StarImportation, SubmoduleImportation,
}; };
use ruff_python_semantic::context::Context; use ruff_python_semantic::context::Context;
use ruff_python_semantic::definition::{Module, ModuleKind}; use ruff_python_semantic::definition::{ContextualizedDefinition, Module, ModuleKind};
use ruff_python_semantic::node::NodeId; use ruff_python_semantic::node::NodeId;
use ruff_python_semantic::scope::{ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind}; use ruff_python_semantic::scope::{ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
@ -5393,10 +5393,23 @@ impl<'a> Checker<'a> {
return; return;
} }
let mut overloaded_name: Option<String> = None; // Compute visibility of all definitions.
let global_scope = self.ctx.global_scope();
let exports: Option<&[&str]> = global_scope
.get("__all__")
.map(|index| &self.ctx.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => Some(names.as_slice()),
_ => None,
});
let definitions = std::mem::take(&mut self.ctx.definitions); let definitions = std::mem::take(&mut self.ctx.definitions);
for (definition, visibility) in definitions.iter() {
let mut overloaded_name: Option<String> = None;
for ContextualizedDefinition {
definition,
visibility,
} in definitions.resolve(exports).iter()
{
let docstring = docstrings::extraction::extract_docstring(definition); let docstring = docstrings::extraction::extract_docstring(definition);
// flake8-annotations // flake8-annotations
@ -5415,7 +5428,9 @@ impl<'a> Checker<'a> {
}) { }) {
self.diagnostics self.diagnostics
.extend(flake8_annotations::rules::definition( .extend(flake8_annotations::rules::definition(
self, definition, visibility, self,
definition,
*visibility,
)); ));
} }
overloaded_name = flake8_annotations::helpers::overloaded_name(self, definition); overloaded_name = flake8_annotations::helpers::overloaded_name(self, definition);
@ -5442,7 +5457,7 @@ impl<'a> Checker<'a> {
// Extract a `Docstring` from a `Definition`. // Extract a `Docstring` from a `Definition`.
let Some(expr) = docstring else { let Some(expr) = docstring else {
pydocstyle::rules::not_missing(self, definition, visibility); pydocstyle::rules::not_missing(self, definition, *visibility);
continue; continue;
}; };

View File

@ -168,4 +168,23 @@ mod tests {
assert_messages!(diagnostics); assert_messages!(diagnostics);
Ok(()) Ok(())
} }
#[test]
fn all() -> Result<()> {
let diagnostics = test_path(
Path::new("pydocstyle/all.py"),
&settings::Settings::for_rules([
Rule::UndocumentedPublicModule,
Rule::UndocumentedPublicClass,
Rule::UndocumentedPublicMethod,
Rule::UndocumentedPublicFunction,
Rule::UndocumentedPublicPackage,
Rule::UndocumentedMagicMethod,
Rule::UndocumentedPublicNestedClass,
Rule::UndocumentedPublicInit,
]),
)?;
assert_messages!(diagnostics);
Ok(())
}
} }

View File

@ -0,0 +1,34 @@
---
source: crates/ruff/src/rules/pydocstyle/mod.rs
---
all.py:1:1: D100 Missing docstring in public module
|
1 | def public_func():
| D100
2 | pass
|
all.py:1:5: D103 Missing docstring in public function
|
1 | def public_func():
| ^^^^^^^^^^^ D103
2 | pass
|
all.py:9:7: D101 Missing docstring in public class
|
9 | class PublicClass:
| ^^^^^^^^^^^ D101
10 | class PublicNestedClass:
11 | pass
|
all.py:10:11: D106 Missing docstring in public nested class
|
10 | class PublicClass:
11 | class PublicNestedClass:
| ^^^^^^^^^^^^^^^^^ D106
12 | pass
|

View File

@ -2,11 +2,10 @@
//! can be documented, such as a module, class, or function. //! can be documented, such as a module, class, or function.
use std::fmt::Debug; use std::fmt::Debug;
use std::iter::FusedIterator;
use std::num::TryFromIntError; use std::num::TryFromIntError;
use std::ops::{Deref, Index}; use std::ops::{Deref, Index};
use rustpython_parser::ast::Stmt; use rustpython_parser::ast::{self, Stmt, StmtKind};
use crate::analyze::visibility::{ use crate::analyze::visibility::{
class_visibility, function_visibility, method_visibility, ModuleSource, Visibility, class_visibility, function_visibility, method_visibility, ModuleSource, Visibility,
@ -86,6 +85,17 @@ pub struct Member<'a> {
pub stmt: &'a Stmt, pub stmt: &'a Stmt,
} }
impl<'a> Member<'a> {
fn name(&self) -> &'a str {
match &self.stmt.node {
StmtKind::FunctionDef(ast::StmtFunctionDef { name, .. }) => name,
StmtKind::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => name,
StmtKind::ClassDef(ast::StmtClassDef { name, .. }) => name,
_ => unreachable!("Unexpected member kind: {:?}", self.kind),
}
}
}
/// A definition within a Python program. /// A definition within a Python program.
#[derive(Debug)] #[derive(Debug)]
pub enum Definition<'a> { pub enum Definition<'a> {
@ -125,13 +135,76 @@ impl<'a> Definitions<'a> {
next_id next_id
} }
/// Iterate over all definitions in a program, with their visibilities. /// Resolve the visibility of each definition in the collection.
pub fn iter(&self) -> DefinitionsIter { pub fn resolve(self, exports: Option<&[&str]>) -> ContextualizedDefinitions<'a> {
DefinitionsIter { let mut definitions: Vec<ContextualizedDefinition<'a>> = Vec::with_capacity(self.len());
inner: self.0.iter(),
definitions: Vec::with_capacity(self.0.len()), for definition in self {
visibilities: Vec::with_capacity(self.0.len()), // Determine the visibility of the next definition, taking into account its parent's
// visibility.
let visibility = {
match &definition {
Definition::Module(module) => module.source.to_visibility(),
Definition::Member(member) => match member.kind {
MemberKind::Class => {
let parent = &definitions[usize::from(member.parent)];
if parent.visibility.is_private()
|| exports
.map_or(false, |exports| !exports.contains(&member.name()))
{
Visibility::Private
} else {
class_visibility(member.stmt)
}
}
MemberKind::NestedClass => {
let parent = &definitions[usize::from(member.parent)];
if parent.visibility.is_private()
|| matches!(
parent.definition,
Definition::Member(Member {
kind: MemberKind::Function
| MemberKind::NestedFunction
| MemberKind::Method,
..
})
)
{
Visibility::Private
} else {
class_visibility(member.stmt)
}
}
MemberKind::Function => {
let parent = &definitions[usize::from(member.parent)];
if parent.visibility.is_private()
|| exports
.map_or(false, |exports| !exports.contains(&member.name()))
{
Visibility::Private
} else {
function_visibility(member.stmt)
}
}
MemberKind::NestedFunction => Visibility::Private,
MemberKind::Method => {
let parent = &definitions[usize::from(member.parent)];
if parent.visibility.is_private() {
Visibility::Private
} else {
method_visibility(member.stmt)
}
}
},
}
};
definitions.push(ContextualizedDefinition {
definition,
visibility,
});
} }
ContextualizedDefinitions(definitions)
} }
} }
@ -150,76 +223,26 @@ impl<'a> Deref for Definitions<'a> {
} }
} }
pub struct DefinitionsIter<'a> { impl<'a> IntoIterator for Definitions<'a> {
inner: std::slice::Iter<'a, Definition<'a>>, type Item = Definition<'a>;
definitions: Vec<&'a Definition<'a>>, type IntoIter = std::vec::IntoIter<Self::Item>;
visibilities: Vec<Visibility>,
}
impl<'a> Iterator for DefinitionsIter<'a> { fn into_iter(self) -> Self::IntoIter {
type Item = (&'a Definition<'a>, Visibility); self.0.into_iter()
fn next(&mut self) -> Option<Self::Item> {
let definition = self.inner.next()?;
// Determine the visibility of the next definition, taking into account its parent's
// visibility.
let visibility = {
match definition {
Definition::Module(module) => module.source.to_visibility(),
Definition::Member(member) => match member.kind {
MemberKind::Class => {
if self.visibilities[usize::from(member.parent)].is_private() {
Visibility::Private
} else {
class_visibility(member.stmt)
}
}
MemberKind::NestedClass => {
if self.visibilities[usize::from(member.parent)].is_private()
|| matches!(
self.definitions[usize::from(member.parent)],
Definition::Member(Member {
kind: MemberKind::Function
| MemberKind::NestedFunction
| MemberKind::Method,
..
})
)
{
Visibility::Private
} else {
class_visibility(member.stmt)
}
}
MemberKind::Function => {
if self.visibilities[usize::from(member.parent)].is_private() {
Visibility::Private
} else {
function_visibility(member.stmt)
}
}
MemberKind::NestedFunction => Visibility::Private,
MemberKind::Method => {
if self.visibilities[usize::from(member.parent)].is_private() {
Visibility::Private
} else {
method_visibility(member.stmt)
}
}
},
}
};
self.definitions.push(definition);
self.visibilities.push(visibility);
Some((definition, visibility))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
} }
} }
impl FusedIterator for DefinitionsIter<'_> {} /// A [`Definition`] in a Python program with its resolved [`Visibility`].
impl ExactSizeIterator for DefinitionsIter<'_> {} pub struct ContextualizedDefinition<'a> {
pub definition: Definition<'a>,
pub visibility: Visibility,
}
/// A collection of [`Definition`] structs in a Python program with resolved [`Visibility`].
pub struct ContextualizedDefinitions<'a>(Vec<ContextualizedDefinition<'a>>);
impl<'a> ContextualizedDefinitions<'a> {
pub fn iter(&self) -> impl Iterator<Item = &ContextualizedDefinition<'a>> {
self.0.iter()
}
}