Coverage for src/pytest_vulture/vulture/comment.py: 0.00%
53 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-22 10:23 +0100
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-22 10:23 +0100
1"""Manages vulture: ignore in source code."""
3import ast
4from contextlib import suppress
5from pathlib import Path
6from typing import TYPE_CHECKING, List, Optional
8from vulture.core import Item
11if TYPE_CHECKING:
12 from _ast import AST
15class CommentFinder:
16 """Parse python code to find vulture: ignore comments."""
18 _tree = None
19 _content: str = ""
20 _ignored_lines: List[int]
21 _path: Path
22 _VULTURE_IGNORE = "vulture: ignore"
24 def __init__(self) -> None:
25 self._path = Path()
26 self._ignored_lines = []
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)
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
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
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)
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 = ""