diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_google.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_google.py index 5cd3f192adf12..d2088336db6ec 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_google.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_google.py @@ -119,3 +119,91 @@ class A(metaclass=abc.abcmeta): def f(self): """Lorem ipsum.""" return True + + +# OK - implicit None early return +def foo(obj: object) -> None: + """A very helpful docstring. + + Args: + obj (object): An object. + """ + if obj is None: + return + print(obj) + + +# OK - explicit None early return +def foo(obj: object) -> None: + """A very helpful docstring. + + Args: + obj (object): An object. + """ + if obj is None: + return None + print(obj) + + +# OK - explicit None early return w/o useful type annotations +def foo(obj): + """A very helpful docstring. + + Args: + obj (object): An object. + """ + if obj is None: + return None + print(obj) + + +# OK - multiple explicit None early returns +def foo(obj: object) -> None: + """A very helpful docstring. + + Args: + obj (object): An object. + """ + if obj is None: + return None + if obj == "None": + return + if obj == 0: + return None + print(obj) + + +# DOC201 - non-early return explicit None +def foo(x: int) -> int | None: + """A very helpful docstring. + + Args: + x (int): An interger. + """ + if x < 0: + return None + else: + return x + + +# DOC201 - non-early return explicit None w/o useful type annotations +def foo(x): + """A very helpful docstring. + + Args: + x (int): An interger. + """ + if x < 0: + return None + else: + return x + + +# DOC201 - only returns None, but return annotation is not None +def foo(s: str) -> str | None: + """A very helpful docstring. + + Args: + s (str): A string. + """ + return None diff --git a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_numpy.py b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_numpy.py index 362836f11d834..0d1081ad875b8 100644 --- a/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_numpy.py +++ b/crates/ruff_linter/resources/test/fixtures/pydoclint/DOC201_numpy.py @@ -85,3 +85,105 @@ class A(metaclass=abc.abcmeta): def f(self): """Lorem ipsum.""" return True + + +# OK - implicit None early return +def foo(obj: object) -> None: + """A very helpful docstring. + + Parameters + ---------- + obj : object + An object. + """ + if obj is None: + return + print(obj) + + +# OK - explicit None early return +def foo(obj: object) -> None: + """A very helpful docstring. + + Parameters + ---------- + obj : object + An object. + """ + if obj is None: + return None + print(obj) + + +# OK - explicit None early return w/o useful type annotations +def foo(obj): + """A very helpful docstring. + + Parameters + ---------- + obj : object + An object. + """ + if obj is None: + return None + print(obj) + + +# OK - multiple explicit None early returns +def foo(obj: object) -> None: + """A very helpful docstring. + + Parameters + ---------- + obj : object + An object. + """ + if obj is None: + return None + if obj == "None": + return + if obj == 0: + return None + print(obj) + + +# DOC201 - non-early return explicit None +def foo(x: int) -> int | None: + """A very helpful docstring. + + Parameters + ---------- + x : int + An interger. + """ + if x < 0: + return None + else: + return x + + +# DOC201 - non-early return explicit None w/o useful type annotations +def foo(x): + """A very helpful docstring. + + Parameters + ---------- + x : int + An interger. + """ + if x < 0: + return None + else: + return x + + +# DOC201 - only returns None, but return annotation is not None +def foo(s: str) -> str | None: + """A very helpful docstring. + + Parameters + ---------- + x : str + A string. + """ + return None diff --git a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs index 53c9899383e79..bf7d68049acb3 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -25,7 +25,8 @@ use crate::rules::pydocstyle::settings::Convention; /// Docstrings missing return sections are a sign of incomplete documentation /// or refactors. /// -/// This rule is not enforced for abstract methods and stubs functions. +/// This rule is not enforced for abstract methods, stubs functions, or +/// functions that only return `None`. /// /// ## Example /// ```python @@ -494,13 +495,26 @@ fn parse_entries_numpy(content: &str) -> Vec { entries } -/// An individual documentable statement in a function body. +/// An individual `yield` expression in a function body. +#[derive(Debug)] +struct YieldEntry { + range: TextRange, +} + +impl Ranged for YieldEntry { + fn range(&self) -> TextRange { + self.range + } +} + +/// An individual `return` statement in a function body. #[derive(Debug)] -struct Entry { +struct ReturnEntry { range: TextRange, + is_none_return: bool, } -impl Ranged for Entry { +impl Ranged for ReturnEntry { fn range(&self) -> TextRange { self.range } @@ -522,15 +536,15 @@ impl Ranged for ExceptionEntry<'_> { /// A summary of documentable statements from the function body #[derive(Debug)] struct BodyEntries<'a> { - returns: Vec, - yields: Vec, + returns: Vec, + yields: Vec, raised_exceptions: Vec>, } /// An AST visitor to extract a summary of documentable statements from a function body. struct BodyVisitor<'a> { - returns: Vec, - yields: Vec, + returns: Vec, + yields: Vec, currently_suspended_exceptions: Option<&'a ast::Expr>, raised_exceptions: Vec>, semantic: &'a SemanticModel<'a>, @@ -623,9 +637,12 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> { } Stmt::Return(ast::StmtReturn { range, - value: Some(_), + value: Some(value), }) => { - self.returns.push(Entry { range: *range }); + self.returns.push(ReturnEntry { + range: *range, + is_none_return: value.is_none_literal_expr(), + }); } Stmt::FunctionDef(_) | Stmt::ClassDef(_) => return, _ => {} @@ -640,10 +657,10 @@ impl<'a> Visitor<'a> for BodyVisitor<'a> { range, value: Some(_), }) => { - self.yields.push(Entry { range: *range }); + self.yields.push(YieldEntry { range: *range }); } Expr::YieldFrom(ast::ExprYieldFrom { range, .. }) => { - self.yields.push(Entry { range: *range }); + self.yields.push(YieldEntry { range: *range }); } Expr::Lambda(_) => return, _ => {} @@ -737,8 +754,22 @@ pub(crate) fn check_docstring( let extra_property_decorators = checker.settings.pydocstyle.property_decorators(); if !definition.is_property(extra_property_decorators, checker.semantic()) { if let Some(body_return) = body_entries.returns.first() { - let diagnostic = Diagnostic::new(DocstringMissingReturns, body_return.range()); - diagnostics.push(diagnostic); + match function_def.returns.as_deref() { + Some(returns) if !Expr::is_none_literal_expr(returns) => diagnostics.push( + Diagnostic::new(DocstringMissingReturns, body_return.range()), + ), + None if body_entries + .returns + .iter() + .any(|entry| !entry.is_none_return) => + { + diagnostics.push(Diagnostic::new( + DocstringMissingReturns, + body_return.range(), + )); + } + _ => {} + } } } } diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_google.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_google.py.snap index 16534806d0524..6878a1bbe683d 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_google.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_google.py.snap @@ -38,3 +38,34 @@ DOC201_google.py:121:9: DOC201 `return` is not documented in docstring | ^^^^^^^^^^^ DOC201 | = help: Add a "Returns" section to the docstring + +DOC201_google.py:184:9: DOC201 `return` is not documented in docstring + | +182 | """ +183 | if x < 0: +184 | return None + | ^^^^^^^^^^^ DOC201 +185 | else: +186 | return x + | + = help: Add a "Returns" section to the docstring + +DOC201_google.py:197:9: DOC201 `return` is not documented in docstring + | +195 | """ +196 | if x < 0: +197 | return None + | ^^^^^^^^^^^ DOC201 +198 | else: +199 | return x + | + = help: Add a "Returns" section to the docstring + +DOC201_google.py:209:5: DOC201 `return` is not documented in docstring + | +207 | s (str): A string. +208 | """ +209 | return None + | ^^^^^^^^^^^ DOC201 + | + = help: Add a "Returns" section to the docstring diff --git a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_numpy.py.snap b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_numpy.py.snap index 04d87deb5aa0d..e72763333ea1f 100644 --- a/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_numpy.py.snap +++ b/crates/ruff_linter/src/rules/pydoclint/snapshots/ruff_linter__rules__pydoclint__tests__docstring-missing-returns_DOC201_numpy.py.snap @@ -27,3 +27,34 @@ DOC201_numpy.py:87:9: DOC201 `return` is not documented in docstring | ^^^^^^^^^^^ DOC201 | = help: Add a "Returns" section to the docstring + +DOC201_numpy.py:160:9: DOC201 `return` is not documented in docstring + | +158 | """ +159 | if x < 0: +160 | return None + | ^^^^^^^^^^^ DOC201 +161 | else: +162 | return x + | + = help: Add a "Returns" section to the docstring + +DOC201_numpy.py:175:9: DOC201 `return` is not documented in docstring + | +173 | """ +174 | if x < 0: +175 | return None + | ^^^^^^^^^^^ DOC201 +176 | else: +177 | return x + | + = help: Add a "Returns" section to the docstring + +DOC201_numpy.py:189:5: DOC201 `return` is not documented in docstring + | +187 | A string. +188 | """ +189 | return None + | ^^^^^^^^^^^ DOC201 + | + = help: Add a "Returns" section to the docstring