diff options
author | Pierre Sassoulas <pierre.sassoulas@gmail.com> | 2022-03-24 20:56:14 +0100 |
---|---|---|
committer | Pierre Sassoulas <pierre.sassoulas@gmail.com> | 2022-03-24 22:40:30 +0100 |
commit | 6940715ba15f81fbd7d9e8685c0a714a8b612f24 (patch) | |
tree | 535259a55ce00647d70b6a2e628b1f53b0699b92 | |
parent | 84d22cf24202bf6006fc179541e1853d145d33e0 (diff) | |
download | pylint-git-6940715ba15f81fbd7d9e8685c0a714a8b612f24.tar.gz |
[refactor] Create a file for the DocstringChecker in pylint.checker.base
-rw-r--r-- | pylint/checkers/base/__init__.py | 191 | ||||
-rw-r--r-- | pylint/checkers/base/docstring_checker.py | 209 |
2 files changed, 211 insertions, 189 deletions
diff --git a/pylint/checkers/base/__init__.py b/pylint/checkers/base/__init__.py index 92e8a9ce3..d4e9417ef 100644 --- a/pylint/checkers/base/__init__.py +++ b/pylint/checkers/base/__init__.py @@ -17,13 +17,9 @@ from pylint import utils as lint_utils from pylint.checkers import utils from pylint.checkers.base.basic_checker import _BasicChecker from pylint.checkers.base.comparison_checker import ComparisonChecker +from pylint.checkers.base.docstring_checker import DocStringChecker from pylint.checkers.base.pass_checker import PassChecker -from pylint.checkers.utils import ( - infer_all, - is_overload_stub, - is_property_deleter, - is_property_setter, -) +from pylint.checkers.utils import infer_all, is_property_deleter, is_property_setter from pylint.reporters.ureports import nodes as reporter_nodes from pylint.utils import LinterStats from pylint.utils.utils import get_global_option @@ -132,8 +128,6 @@ DEFAULT_PATTERNS = { ) } -# do not require a doc string on private/system methods -NO_REQUIRED_DOC_RGX = re.compile("^_") REVERSED_PROTOCOL_METHOD = "__reversed__" SEQUENCE_PROTOCOL_METHODS = ("__getitem__", "__len__") REVERSED_METHODS = (SEQUENCE_PROTOCOL_METHODS, (REVERSED_PROTOCOL_METHOD,)) @@ -2146,187 +2140,6 @@ class NameChecker(_BasicChecker): ) -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": "<regexp>", - "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": "<int>", - "help": ( - "Minimum line length for functions/classes that" - " require docstrings, shorter ones are exempt." - ), - }, - ), - ) - - def open(self): - self.linter.stats.reset_undocumented() - - @utils.check_messages("missing-docstring", "empty-docstring") - def visit_module(self, node: nodes.Module) -> None: - self._check_docstring("module", node) - - @utils.check_messages("missing-docstring", "empty-docstring") - def visit_classdef(self, node: nodes.ClassDef) -> None: - if self.config.no_docstring_rgx.match(node.name) is None: - self._check_docstring("class", node) - - @utils.check_messages("missing-docstring", "empty-docstring") - def visit_functiondef(self, node: nodes.FunctionDef) -> None: - if self.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(future=True), nodes.ClassDef): - overridden = False - confidence = ( - interfaces.INFERENCE - if utils.has_known_bases(node.parent.frame(future=True)) - else interfaces.INFERENCE_FAILURE - ) - # check if node is from a method overridden by its ancestor - for ancestor in node.parent.frame(future=True).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(future=True), 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, - report_missing=True, - confidence=interfaces.HIGH, - ): - """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.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 - ) - - -def _infer_dunder_doc_attribute(node): - # 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 docstring.value - - def register(linter: "PyLinter") -> None: linter.register_checker(BasicErrorChecker(linter)) linter.register_checker(BasicChecker(linter)) diff --git a/pylint/checkers/base/docstring_checker.py b/pylint/checkers/base/docstring_checker.py new file mode 100644 index 000000000..73a6c31e5 --- /dev/null +++ b/pylint/checkers/base/docstring_checker.py @@ -0,0 +1,209 @@ +# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html +# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt + +"""Docstring checker from the basic checker.""" + +import re +import sys + +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, +) + +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + +# do not require a doc string on private/system methods +NO_REQUIRED_DOC_RGX = re.compile("^_") + + +def _infer_dunder_doc_attribute(node): + # 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 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": "<regexp>", + "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": "<int>", + "help": ( + "Minimum line length for functions/classes that" + " require docstrings, shorter ones are exempt." + ), + }, + ), + ) + + def open(self): + self.linter.stats.reset_undocumented() + + @utils.check_messages("missing-docstring", "empty-docstring") + def visit_module(self, node: nodes.Module) -> None: + self._check_docstring("module", node) + + @utils.check_messages("missing-docstring", "empty-docstring") + def visit_classdef(self, node: nodes.ClassDef) -> None: + if self.config.no_docstring_rgx.match(node.name) is None: + self._check_docstring("class", node) + + @utils.check_messages("missing-docstring", "empty-docstring") + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + if self.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(future=True), nodes.ClassDef): + overridden = False + confidence = ( + interfaces.INFERENCE + if utils.has_known_bases(node.parent.frame(future=True)) + else interfaces.INFERENCE_FAILURE + ) + # check if node is from a method overridden by its ancestor + for ancestor in node.parent.frame(future=True).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(future=True), 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, + report_missing=True, + confidence=interfaces.HIGH, + ): + """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.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 + ) |