209 lines
7.6 KiB
Python
209 lines
7.6 KiB
Python
|
# 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": "<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) -> 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
|
||
|
)
|