private_import.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. """Check for imports on private external modules and names."""
  5. from __future__ import annotations
  6. from pathlib import Path
  7. from typing import TYPE_CHECKING
  8. from astroid import nodes
  9. from pylint.checkers import BaseChecker, utils
  10. from pylint.interfaces import HIGH
  11. if TYPE_CHECKING:
  12. from pylint.lint.pylinter import PyLinter
  13. class PrivateImportChecker(BaseChecker):
  14. name = "import-private-name"
  15. msgs = {
  16. "C2701": (
  17. "Imported private %s (%s)",
  18. "import-private-name",
  19. "Used when a private module or object prefixed with _ is imported. "
  20. "PEP8 guidance on Naming Conventions states that public attributes with "
  21. "leading underscores should be considered private.",
  22. ),
  23. }
  24. def __init__(self, linter: PyLinter) -> None:
  25. BaseChecker.__init__(self, linter)
  26. # A mapping of private names used as a type annotation to whether it is an acceptable import
  27. self.all_used_type_annotations: dict[str, bool] = {}
  28. self.populated_annotations = False
  29. @utils.only_required_for_messages("import-private-name")
  30. def visit_import(self, node: nodes.Import) -> None:
  31. if utils.in_type_checking_block(node):
  32. return
  33. names = [name[0] for name in node.names]
  34. private_names = self._get_private_imports(names)
  35. private_names = self._get_type_annotation_names(node, private_names)
  36. if private_names:
  37. imported_identifier = "modules" if len(private_names) > 1 else "module"
  38. private_name_string = ", ".join(private_names)
  39. self.add_message(
  40. "import-private-name",
  41. node=node,
  42. args=(imported_identifier, private_name_string),
  43. confidence=HIGH,
  44. )
  45. @utils.only_required_for_messages("import-private-name")
  46. def visit_importfrom(self, node: nodes.ImportFrom) -> None:
  47. if utils.in_type_checking_block(node):
  48. return
  49. # Only check imported names if the module is external
  50. if self.same_root_dir(node, node.modname):
  51. return
  52. names = [n[0] for n in node.names]
  53. # Check the imported objects first. If they are all valid type annotations,
  54. # the package can be private
  55. private_names = self._get_type_annotation_names(node, names)
  56. if not private_names:
  57. return
  58. # There are invalid imported objects, so check the name of the package
  59. private_module_imports = self._get_private_imports([node.modname])
  60. private_module_imports = self._get_type_annotation_names(
  61. node, private_module_imports
  62. )
  63. if private_module_imports:
  64. self.add_message(
  65. "import-private-name",
  66. node=node,
  67. args=("module", private_module_imports[0]),
  68. confidence=HIGH,
  69. )
  70. return # Do not emit messages on the objects if the package is private
  71. private_names = self._get_private_imports(private_names)
  72. if private_names:
  73. imported_identifier = "objects" if len(private_names) > 1 else "object"
  74. private_name_string = ", ".join(private_names)
  75. self.add_message(
  76. "import-private-name",
  77. node=node,
  78. args=(imported_identifier, private_name_string),
  79. confidence=HIGH,
  80. )
  81. def _get_private_imports(self, names: list[str]) -> list[str]:
  82. """Returns the private names from input names by a simple string check."""
  83. return [name for name in names if self._name_is_private(name)]
  84. @staticmethod
  85. def _name_is_private(name: str) -> bool:
  86. """Returns true if the name exists, starts with `_`, and if len(name) > 4
  87. it is not a dunder, i.e. it does not begin and end with two underscores.
  88. """
  89. return (
  90. bool(name)
  91. and name[0] == "_"
  92. and (len(name) <= 4 or name[1] != "_" or name[-2:] != "__")
  93. )
  94. def _get_type_annotation_names(
  95. self, node: nodes.Import | nodes.ImportFrom, names: list[str]
  96. ) -> list[str]:
  97. """Removes from names any names that are used as type annotations with no other
  98. illegal usages.
  99. """
  100. if names and not self.populated_annotations:
  101. self._populate_type_annotations(node.root(), self.all_used_type_annotations)
  102. self.populated_annotations = True
  103. return [
  104. n
  105. for n in names
  106. if n not in self.all_used_type_annotations
  107. or (
  108. n in self.all_used_type_annotations
  109. and not self.all_used_type_annotations[n]
  110. )
  111. ]
  112. def _populate_type_annotations(
  113. self, node: nodes.LocalsDictNodeNG, all_used_type_annotations: dict[str, bool]
  114. ) -> None:
  115. """Adds to `all_used_type_annotations` all names ever used as a type annotation
  116. in the node's (nested) scopes and whether they are only used as annotation.
  117. """
  118. for name in node.locals:
  119. # If we find a private type annotation, make sure we do not mask illegal usages
  120. private_name = None
  121. # All the assignments using this variable that we might have to check for
  122. # illegal usages later
  123. name_assignments = []
  124. for usage_node in node.locals[name]:
  125. if isinstance(usage_node, nodes.AssignName) and isinstance(
  126. usage_node.parent, (nodes.AnnAssign, nodes.Assign)
  127. ):
  128. assign_parent = usage_node.parent
  129. if isinstance(assign_parent, nodes.AnnAssign):
  130. name_assignments.append(assign_parent)
  131. private_name = self._populate_type_annotations_annotation(
  132. usage_node.parent.annotation, all_used_type_annotations
  133. )
  134. elif isinstance(assign_parent, nodes.Assign):
  135. name_assignments.append(assign_parent)
  136. if isinstance(usage_node, nodes.FunctionDef):
  137. self._populate_type_annotations_function(
  138. usage_node, all_used_type_annotations
  139. )
  140. if isinstance(usage_node, nodes.LocalsDictNodeNG):
  141. self._populate_type_annotations(
  142. usage_node, all_used_type_annotations
  143. )
  144. if private_name is not None:
  145. # Found a new private annotation, make sure we are not accessing it elsewhere
  146. all_used_type_annotations[
  147. private_name
  148. ] = self._assignments_call_private_name(name_assignments, private_name)
  149. def _populate_type_annotations_function(
  150. self, node: nodes.FunctionDef, all_used_type_annotations: dict[str, bool]
  151. ) -> None:
  152. """Adds all names used as type annotation in the arguments and return type of
  153. the function node into the dict `all_used_type_annotations`.
  154. """
  155. if node.args and node.args.annotations:
  156. for annotation in node.args.annotations:
  157. self._populate_type_annotations_annotation(
  158. annotation, all_used_type_annotations
  159. )
  160. if node.returns:
  161. self._populate_type_annotations_annotation(
  162. node.returns, all_used_type_annotations
  163. )
  164. def _populate_type_annotations_annotation(
  165. self,
  166. node: nodes.Attribute | nodes.Subscript | nodes.Name | None,
  167. all_used_type_annotations: dict[str, bool],
  168. ) -> str | None:
  169. """Handles the possibility of an annotation either being a Name, i.e. just type,
  170. or a Subscript e.g. `Optional[type]` or an Attribute, e.g. `pylint.lint.linter`.
  171. """
  172. if isinstance(node, nodes.Name) and node.name not in all_used_type_annotations:
  173. all_used_type_annotations[node.name] = True
  174. return node.name # type: ignore[no-any-return]
  175. if isinstance(node, nodes.Subscript): # e.g. Optional[List[str]]
  176. # slice is the next nested type
  177. self._populate_type_annotations_annotation(
  178. node.slice, all_used_type_annotations
  179. )
  180. # value is the current type name: could be a Name or Attribute
  181. return self._populate_type_annotations_annotation(
  182. node.value, all_used_type_annotations
  183. )
  184. if isinstance(node, nodes.Attribute):
  185. # An attribute is a type like `pylint.lint.pylinter`. node.expr is the next level
  186. # up, could be another attribute
  187. return self._populate_type_annotations_annotation(
  188. node.expr, all_used_type_annotations
  189. )
  190. return None
  191. @staticmethod
  192. def _assignments_call_private_name(
  193. assignments: list[nodes.AnnAssign | nodes.Assign], private_name: str
  194. ) -> bool:
  195. """Returns True if no assignments involve accessing `private_name`."""
  196. if all(not assignment.value for assignment in assignments):
  197. # Variable annotated but unassigned is not allowed because there may be
  198. # possible illegal access elsewhere
  199. return False
  200. for assignment in assignments:
  201. current_attribute = None
  202. if isinstance(assignment.value, nodes.Call):
  203. current_attribute = assignment.value.func
  204. elif isinstance(assignment.value, nodes.Attribute):
  205. current_attribute = assignment.value
  206. elif isinstance(assignment.value, nodes.Name):
  207. current_attribute = assignment.value.name
  208. if not current_attribute:
  209. continue
  210. while isinstance(current_attribute, (nodes.Attribute, nodes.Call)):
  211. if isinstance(current_attribute, nodes.Call):
  212. current_attribute = current_attribute.func
  213. if not isinstance(current_attribute, nodes.Name):
  214. current_attribute = current_attribute.expr
  215. if (
  216. isinstance(current_attribute, nodes.Name)
  217. and current_attribute.name == private_name
  218. ):
  219. return False
  220. return True
  221. @staticmethod
  222. def same_root_dir(
  223. node: nodes.Import | nodes.ImportFrom, import_mod_name: str
  224. ) -> bool:
  225. """Does the node's file's path contain the base name of `import_mod_name`?"""
  226. if not import_mod_name: # from . import ...
  227. return True
  228. if node.level: # from .foo import ..., from ..bar import ...
  229. return True
  230. base_import_package = import_mod_name.split(".")[0]
  231. return base_import_package in Path(node.root().file).parent.parts
  232. def register(linter: PyLinter) -> None:
  233. linter.register_checker(PrivateImportChecker(linter))