Coverage for src/pytest_vulture/vulture/comment.py: 0.00%

1"""Manages vulture: ignore in source code."""

2

3 import ast

4 from contextlib import suppress

5 from pathlib import Path

6 from typing import TYPE_CHECKING, List, Optional

7

8 from vulture.core import Item

9

10

11 if TYPE_CHECKING:

12 from _ast import AST

13

14

15 class CommentFinder:

16 """Parse python code to find vulture: ignore comments."""

17

18 _tree = None

19 _content: str = ""

20 _ignored_lines: List[int]

21 _path: Path

22 _VULTURE_IGNORE = "vulture: ignore"

23

24 def __init__(self) -> None:

25 self._path = Path()

26 self._ignored_lines = []

27

28 def check_comment(self, vulture: Item) -> bool:

29 """Check if the vulture output line is ignored with a # vulture: ignore comment

30 Examples::

31 >>> Path("/tmp/test.py").write_text("def test():pass")

32 15

33 >>> finder = CommentFinder()

34 >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 1, 1, "unused function 'test'", 50))

35 False

36 >>> Path("/tmp/test.py").write_text('def test(): # vulture: ignore\\n pass')

37 40

38 >>> finder = CommentFinder() # the file has changed, must recreate the instance

39 >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 1, 1, "unused function 'test'", 50))

40 True

41 >>> finder.check_comment(Item("test", "function", Path("/tmp/test.py"), 3, 3, "unused function 'test'", 50))

42 False

43 """

44 line_number = vulture.first_lineno

45 if line_number is None: # pragma: no cover

46 return False

47 # Check if is the same file as before, if not, reload

48 if vulture.filename.as_posix() != self._path.as_posix():

49 self.__reset(vulture.filename)

50 return self.__find_rec(vulture)

51

52 def __find_rec(self, vulture: Item, tree: "Optional[AST]" = None, *, ignore_mode: bool = False) -> bool:

53 """Find comments recursively."""

54 if tree is None:

55 tree = self._tree

56 if tree is None: # pragma: no cover

57 return False

58 with suppress(AttributeError):

59 line_nb: int = getattr(tree, "lineno") # noqa: B009

60 if not ignore_mode and line_nb in self._ignored_lines:

61 ignore_mode = True

62 if ignore_mode and vulture.first_lineno in [

63 line_nb,

64 line_nb - self.__get_decorators(tree),

65 ]:

66 return True

67 with suppress(AttributeError):

68 for new_tree in tree.body: # type: ignore[attr-defined]

69 if self.__find_rec(vulture, tree=new_tree, ignore_mode=ignore_mode):

70 return True

71 with suppress(AttributeError):

72 for new_tree in tree.orelse: # type: ignore[attr-defined]

73 if self.__find_rec(vulture, tree=new_tree, ignore_mode=ignore_mode):

74 return True

75 return False

76

77 @staticmethod

78 def __get_decorators(tree: "Optional[AST]") -> int:

79 if tree is None: # pragma: no cover

80 return 0

81 try:

82 return len(tree.decorator_list) # type: ignore[attr-defined]

83 except AttributeError:

84 return 0

85

86 def __reset(self, path: Path) -> None:

87 self._path = path

88 self._ignored_lines = []

89 try:

90 self._content = self._path.read_text(encoding="utf-8-sig")

91 self._tree = ast.parse(self._content)

92 content_split = self._content.split("\n")

93 for index_line, elem in enumerate(content_split):

94 if self._VULTURE_IGNORE in elem:

95 self._ignored_lines.append(index_line + 1)

96

97 except OSError as err: # pragma: no cover

98 print(f"warning : unable to read : {self._path.as_posix()} : {err}") # noqa: T201

99 self._content = ""