Skip to content

Commit

Permalink
[docparam extension] Considers type comments as type documentation.
Browse files Browse the repository at this point in the history
Closes pylint-dev#6287

Co-authored-by: Pierre Sassoulas <[email protected]>
  • Loading branch information
AWhetter and Pierre-Sassoulas committed May 22, 2023
1 parent 893cb78 commit aa9926e
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 14 deletions.
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/6287.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
docparams extension considers type comments as type documentation.

Closes #6287
108 changes: 108 additions & 0 deletions pylint/extensions/_check_docs_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@

from __future__ import annotations

import itertools
import re
from collections.abc import Iterable

import astroid
from astroid import nodes
from astroid.util import UninferableBase

from pylint.checkers import utils
from pylint.constants import PY38_PLUS


def space_indentation(s: str) -> int:
Expand Down Expand Up @@ -159,6 +162,111 @@ def possible_exc_types(node: nodes.NodeNG) -> set[nodes.ClassDef]:
return set()


def _is_ellipsis(node: nodes.NodeNG) -> bool:
if not PY38_PLUS:
return isinstance(node, nodes.Ellipsis)

return isinstance(node, nodes.Const) and node.value == Ellipsis


def _merge_annotations(
annotations: Iterable[nodes.NodeNG], comment_annotations: Iterable[nodes.NodeNG]
) -> Iterable[nodes.NodeNG | None]:
for ann, comment_ann in itertools.zip_longest(annotations, comment_annotations):
if ann and not _is_ellipsis(ann):
yield ann
elif comment_ann and not _is_ellipsis(comment_ann):
yield comment_ann
else:
yield None


def _annotations_list(args_node: nodes.Arguments) -> list[nodes.NodeNG]:
"""Get a merged list of annotations.
The annotations can come from:
* Real type annotations.
* A type comment on the function.
* A type common on the individual argument.
:param args_node: The node to get the annotations for.
:returns: The annotations.
"""
plain_annotations = args_node.annotations or ()
func_comment_annotations = args_node.parent.type_comment_args or ()
comment_annotations = args_node.type_comment_posonlyargs
comment_annotations += args_node.type_comment_args or []
comment_annotations += args_node.type_comment_kwonlyargs
return list(
_merge_annotations(
plain_annotations,
_merge_annotations(func_comment_annotations, comment_annotations),
)
)


def args_with_annotation(args_node: nodes.Arguments) -> set[str]:
result = set()
annotations = _annotations_list(args_node)
annotation_offset = 0

if args_node.posonlyargs:
posonlyargs_annotations = args_node.posonlyargs_annotations
if not any(args_node.posonlyargs_annotations):
num_args = len(args_node.posonlyargs)
posonlyargs_annotations = annotations[
annotation_offset : annotation_offset + num_args
]
annotation_offset += num_args

for arg, annotation in zip(args_node.posonlyargs, posonlyargs_annotations):
if annotation:
result.add(arg.name)

if args_node.args:
num_args = len(args_node.args)
for arg, annotation in zip(
args_node.args,
annotations[annotation_offset : annotation_offset + num_args],
):
if annotation:
result.add(arg.name)

annotation_offset += num_args

if args_node.vararg:
annotation = None
if args_node.varargannotation:
result.add(args_node.vararg)
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
result.add(args_node.vararg)
annotation_offset += 1

if args_node.kwonlyargs:
kwonlyargs_annotations = args_node.kwonlyargs_annotations
if not any(args_node.kwonlyargs_annotations):
num_args = len(args_node.kwonlyargs)
kwonlyargs_annotations = annotations[
annotation_offset : annotation_offset + num_args
]
annotation_offset += num_args

for arg, annotation in zip(args_node.kwonlyargs, kwonlyargs_annotations):
if annotation:
result.add(arg.name)

if args_node.kwarg:
annotation = None
if args_node.kwargannotation:
result.add(args_node.kwarg)
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
result.add(args_node.kwarg)
annotation_offset += 1

return result


def docstringify(
docstring: nodes.Const | None, default_type: str = "default"
) -> Docstring:
Expand Down
23 changes: 9 additions & 14 deletions pylint/extensions/docparams.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ def visit_return(self, node: nodes.Return) -> None:
if not (doc.has_returns() or (doc.has_property_returns() and is_property)):
self.add_message("missing-return-doc", node=func_node, confidence=HIGH)

if func_node.returns:
if func_node.returns or func_node.type_comment_returns:
return

if not (doc.has_rtype() or (doc.has_property_type() and is_property)):
Expand Down Expand Up @@ -379,7 +379,9 @@ def visit_yield(self, node: nodes.Yield | nodes.YieldFrom) -> None:
if not doc_has_yields:
self.add_message("missing-yield-doc", node=func_node, confidence=HIGH)

if not (doc_has_yields_type or func_node.returns):
if not (
doc_has_yields_type or func_node.returns or func_node.type_comment_returns
):
self.add_message("missing-yield-type-doc", node=func_node, confidence=HIGH)

visit_yieldfrom = visit_yield
Expand Down Expand Up @@ -540,8 +542,9 @@ class constructor.

# Collect the function arguments.
expected_argument_names = {arg.name for arg in arguments_node.args}
expected_argument_names.update(arg.name for arg in arguments_node.kwonlyargs)
expected_argument_names.update(arg.name for arg in arguments_node.posonlyargs)
expected_argument_names.update(
a.name for a in arguments_node.posonlyargs + arguments_node.kwonlyargs
)
not_needed_type_in_docstring = self.not_needed_param_in_docstring.copy()

expected_but_ignored_argument_names = set()
Expand All @@ -564,7 +567,7 @@ class constructor.
if not params_with_doc and not params_with_type and accept_no_param_doc:
tolerate_missing_params = True

# This is before the update of param_with_type because this must check only
# This is before the update of params_with_type because this must check only
# the type documented in a docstring, not the one using pep484
# See #4117 and #4593
self._compare_ignored_args(
Expand All @@ -573,15 +576,7 @@ class constructor.
expected_but_ignored_argument_names,
warning_node,
)
for index, arg_name in enumerate(arguments_node.args):
if arguments_node.annotations[index]:
params_with_type.add(arg_name.name)
for index, arg_name in enumerate(arguments_node.kwonlyargs):
if arguments_node.kwonlyargs_annotations[index]:
params_with_type.add(arg_name.name)
for index, arg_name in enumerate(arguments_node.posonlyargs):
if arguments_node.posonlyargs_annotations[index]:
params_with_type.add(arg_name.name)
params_with_type |= utils.args_with_annotation(arguments_node)

if not tolerate_missing_params:
missing_param_doc = (expected_argument_names - params_with_doc) - (
Expand Down
51 changes: 51 additions & 0 deletions tests/functional/ext/docparams/missing_param_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,54 @@ def foobar15(*args):
Relevant parameters.
"""
print(args)


def foobar16(one: int, two: str, three: float) -> int:
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1


def foobar17(one, two, three):
# type: (int, str, float) -> int
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1


def foobar18(
one, # type: int
two, # type: str
three, # type: float
):
# type: (...) -> int
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1
53 changes: 53 additions & 0 deletions tests/functional/ext/docparams/missing_param_doc_py38.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#pylint: disable= missing-module-docstring

def foobar1(one: int, /, two: str, *, three: float) -> int:
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1


def foobar2(one, /, two, * three):
# type: (int, str, float) -> int
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1


def foobar3(
one, # type: int
/,
two, # type: str
*,
three, # type: float
):
# type: (...) -> int
"""Description of the function
Args:
one: A number.
two: Another number.
three: Yes another number.
Returns:
The number one.
"""
print(one, two, three)
return 1
11 changes: 11 additions & 0 deletions tests/functional/ext/docparams/missing_param_doc_py38.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[MASTER]
load-plugins=pylint.extensions.docparams,

[testoptions]
min_pyver=3.8

[PARAMETER_DOCUMENTATION]
accept-no-param-doc=no
accept-no-raise-doc=no
accept-no-return-doc=no
accept-no-yields-doc=no

0 comments on commit aa9926e

Please sign in to comment.