# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt """Docstring checker from the basic checker.""" from __future__ import annotations import re from typing import Literal import astroid from astroid import nodes from pylint import interfaces from pylint.checkers import utils from pylint.checkers.base.basic_checker import _BasicChecker from pylint.checkers.utils import ( is_overload_stub, is_property_deleter, is_property_setter, ) # do not require a doc string on private/system methods NO_REQUIRED_DOC_RGX = re.compile("^_") def _infer_dunder_doc_attribute( node: nodes.Module | nodes.ClassDef | nodes.FunctionDef, ) -> str | None: # Try to see if we have a `__doc__` attribute. try: docstring = node["__doc__"] except KeyError: return None docstring = utils.safe_infer(docstring) if not docstring: return None if not isinstance(docstring, nodes.Const): return None return str(docstring.value) class DocStringChecker(_BasicChecker): msgs = { "C0112": ( "Empty %s docstring", "empty-docstring", "Used when a module, function, class or method has an empty " "docstring (it would be too easy ;).", {"old_names": [("W0132", "old-empty-docstring")]}, ), "C0114": ( "Missing module docstring", "missing-module-docstring", "Used when a module has no docstring. " "Empty modules do not require a docstring.", {"old_names": [("C0111", "missing-docstring")]}, ), "C0115": ( "Missing class docstring", "missing-class-docstring", "Used when a class has no docstring. " "Even an empty class must have a docstring.", {"old_names": [("C0111", "missing-docstring")]}, ), "C0116": ( "Missing function or method docstring", "missing-function-docstring", "Used when a function or method has no docstring. " "Some special methods like __init__ do not require a " "docstring.", {"old_names": [("C0111", "missing-docstring")]}, ), } options = ( ( "no-docstring-rgx", { "default": NO_REQUIRED_DOC_RGX, "type": "regexp", "metavar": "", "help": "Regular expression which should only match " "function or class names that do not require a " "docstring.", }, ), ( "docstring-min-length", { "default": -1, "type": "int", "metavar": "", "help": ( "Minimum line length for functions/classes that" " require docstrings, shorter ones are exempt." ), }, ), ) def open(self) -> None: self.linter.stats.reset_undocumented() @utils.only_required_for_messages("missing-module-docstring", "empty-docstring") def visit_module(self, node: nodes.Module) -> None: self._check_docstring("module", node) @utils.only_required_for_messages("missing-class-docstring", "empty-docstring") def visit_classdef(self, node: nodes.ClassDef) -> None: if self.linter.config.no_docstring_rgx.match(node.name) is None: self._check_docstring("class", node) @utils.only_required_for_messages("missing-function-docstring", "empty-docstring") def visit_functiondef(self, node: nodes.FunctionDef) -> None: if self.linter.config.no_docstring_rgx.match(node.name) is None: ftype = "method" if node.is_method() else "function" if ( is_property_setter(node) or is_property_deleter(node) or is_overload_stub(node) ): return if isinstance(node.parent.frame(), nodes.ClassDef): overridden = False confidence = ( interfaces.INFERENCE if utils.has_known_bases(node.parent.frame()) else interfaces.INFERENCE_FAILURE ) # check if node is from a method overridden by its ancestor for ancestor in node.parent.frame().ancestors(): if ancestor.qname() == "builtins.object": continue if node.name in ancestor and isinstance( ancestor[node.name], nodes.FunctionDef ): overridden = True break self._check_docstring( ftype, node, report_missing=not overridden, confidence=confidence # type: ignore[arg-type] ) elif isinstance(node.parent.frame(), nodes.Module): self._check_docstring(ftype, node) # type: ignore[arg-type] else: return visit_asyncfunctiondef = visit_functiondef def _check_docstring( self, node_type: Literal["class", "function", "method", "module"], node: nodes.Module | nodes.ClassDef | nodes.FunctionDef, report_missing: bool = True, confidence: interfaces.Confidence = interfaces.HIGH, ) -> None: """Check if the node has a non-empty docstring.""" docstring = node.doc_node.value if node.doc_node else None if docstring is None: docstring = _infer_dunder_doc_attribute(node) if docstring is None: if not report_missing: return lines = utils.get_node_last_lineno(node) - node.lineno if node_type == "module" and not lines: # If the module does not have a body, there's no reason # to require a docstring. return max_lines = self.linter.config.docstring_min_length if node_type != "module" and max_lines > -1 and lines < max_lines: return if node_type == "class": self.linter.stats.undocumented["klass"] += 1 else: self.linter.stats.undocumented[node_type] += 1 if ( node.body and isinstance(node.body[0], nodes.Expr) and isinstance(node.body[0].value, nodes.Call) ): # Most likely a string with a format call. Let's see. func = utils.safe_infer(node.body[0].value.func) if isinstance(func, astroid.BoundMethod) and isinstance( func.bound, astroid.Instance ): # Strings. if func.bound.name in {"str", "unicode", "bytes"}: return if node_type == "module": message = "missing-module-docstring" elif node_type == "class": message = "missing-class-docstring" else: message = "missing-function-docstring" self.add_message(message, node=node, confidence=confidence) elif not docstring.strip(): if node_type == "class": self.linter.stats.undocumented["klass"] += 1 else: self.linter.stats.undocumented[node_type] += 1 self.add_message( "empty-docstring", node=node, args=(node_type,), confidence=confidence )