summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPierre Sassoulas <pierre.sassoulas@gmail.com>2022-03-24 20:56:14 +0100
committerPierre Sassoulas <pierre.sassoulas@gmail.com>2022-03-24 22:40:30 +0100
commit6940715ba15f81fbd7d9e8685c0a714a8b612f24 (patch)
tree535259a55ce00647d70b6a2e628b1f53b0699b92
parent84d22cf24202bf6006fc179541e1853d145d33e0 (diff)
downloadpylint-git-6940715ba15f81fbd7d9e8685c0a714a8b612f24.tar.gz
[refactor] Create a file for the DocstringChecker in pylint.checker.base
-rw-r--r--pylint/checkers/base/__init__.py191
-rw-r--r--pylint/checkers/base/docstring_checker.py209
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
+ )