recommendation_checker.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  3. from typing import Union
  4. import astroid
  5. from astroid import nodes
  6. from pylint import checkers, interfaces
  7. from pylint.checkers import utils
  8. from pylint.utils.utils import get_global_option
  9. class RecommendationChecker(checkers.BaseChecker):
  10. __implements__ = (interfaces.IAstroidChecker,)
  11. name = "refactoring"
  12. msgs = {
  13. "C0200": (
  14. "Consider using enumerate instead of iterating with range and len",
  15. "consider-using-enumerate",
  16. "Emitted when code that iterates with range and len is "
  17. "encountered. Such code can be simplified by using the "
  18. "enumerate builtin.",
  19. ),
  20. "C0201": (
  21. "Consider iterating the dictionary directly instead of calling .keys()",
  22. "consider-iterating-dictionary",
  23. "Emitted when the keys of a dictionary are iterated through the ``.keys()`` "
  24. "method or when ``.keys()`` is used for a membership check. "
  25. "It is enough to iterate through the dictionary itself, "
  26. "``for key in dictionary``. For membership checks, "
  27. "``if key in dictionary`` is faster.",
  28. ),
  29. "C0206": (
  30. "Consider iterating with .items()",
  31. "consider-using-dict-items",
  32. "Emitted when iterating over the keys of a dictionary and accessing the "
  33. "value by index lookup. "
  34. "Both the key and value can be accessed by iterating using the .items() "
  35. "method of the dictionary instead.",
  36. ),
  37. "C0207": (
  38. "Use %s instead",
  39. "use-maxsplit-arg",
  40. "Emitted when accessing only the first or last element of str.split(). "
  41. "The first and last element can be accessed by using "
  42. "str.split(sep, maxsplit=1)[0] or str.rsplit(sep, maxsplit=1)[-1] "
  43. "instead.",
  44. ),
  45. "C0208": (
  46. "Use a sequence type when iterating over values",
  47. "use-sequence-for-iteration",
  48. "When iterating over values, sequence types (e.g., ``lists``, ``tuples``, ``ranges``) "
  49. "are more efficient than ``sets``.",
  50. ),
  51. "C0209": (
  52. "Formatting a regular string which could be a f-string",
  53. "consider-using-f-string",
  54. "Used when we detect a string that is being formatted with format() or % "
  55. "which could potentially be a f-string. The use of f-strings is preferred. "
  56. "Requires Python 3.6 and ``py-version >= 3.6``.",
  57. ),
  58. }
  59. def open(self) -> None:
  60. py_version = get_global_option(self, "py-version")
  61. self._py36_plus = py_version >= (3, 6)
  62. @staticmethod
  63. def _is_builtin(node, function):
  64. inferred = utils.safe_infer(node)
  65. if not inferred:
  66. return False
  67. return utils.is_builtin_object(inferred) and inferred.name == function
  68. @utils.check_messages("consider-iterating-dictionary", "use-maxsplit-arg")
  69. def visit_call(self, node: nodes.Call) -> None:
  70. self._check_consider_iterating_dictionary(node)
  71. self._check_use_maxsplit_arg(node)
  72. def _check_consider_iterating_dictionary(self, node: nodes.Call) -> None:
  73. if not isinstance(node.func, nodes.Attribute):
  74. return
  75. if node.func.attrname != "keys":
  76. return
  77. comp_ancestor = utils.get_node_first_ancestor_of_type(node, nodes.Compare)
  78. if (
  79. isinstance(node.parent, (nodes.For, nodes.Comprehension))
  80. or comp_ancestor
  81. and any(
  82. op
  83. for op, comparator in comp_ancestor.ops
  84. if op in {"in", "not in"}
  85. and (comparator in node.node_ancestors() or comparator is node)
  86. )
  87. ):
  88. inferred = utils.safe_infer(node.func)
  89. if not isinstance(inferred, astroid.BoundMethod) or not isinstance(
  90. inferred.bound, nodes.Dict
  91. ):
  92. return
  93. self.add_message("consider-iterating-dictionary", node=node)
  94. def _check_use_maxsplit_arg(self, node: nodes.Call) -> None:
  95. """Add message when accessing first or last elements of a str.split() or str.rsplit()."""
  96. # Check if call is split() or rsplit()
  97. if not (
  98. isinstance(node.func, nodes.Attribute)
  99. and node.func.attrname in {"split", "rsplit"}
  100. and isinstance(utils.safe_infer(node.func), astroid.BoundMethod)
  101. ):
  102. return
  103. try:
  104. utils.get_argument_from_call(node, 0, "sep")
  105. except utils.NoSuchArgumentError:
  106. return
  107. try:
  108. # Ignore if maxsplit arg has been set
  109. utils.get_argument_from_call(node, 1, "maxsplit")
  110. return
  111. except utils.NoSuchArgumentError:
  112. pass
  113. if isinstance(node.parent, nodes.Subscript):
  114. try:
  115. subscript_value = utils.get_subscript_const_value(node.parent).value
  116. except utils.InferredTypeError:
  117. return
  118. # Check for cases where variable (Name) subscripts may be mutated within a loop
  119. if isinstance(node.parent.slice, nodes.Name):
  120. # Check if loop present within the scope of the node
  121. scope = node.scope()
  122. for loop_node in scope.nodes_of_class((nodes.For, nodes.While)):
  123. if not loop_node.parent_of(node):
  124. continue
  125. # Check if var is mutated within loop (Assign/AugAssign)
  126. for assignment_node in loop_node.nodes_of_class(nodes.AugAssign):
  127. if node.parent.slice.name == assignment_node.target.name:
  128. return
  129. for assignment_node in loop_node.nodes_of_class(nodes.Assign):
  130. if node.parent.slice.name in [
  131. n.name for n in assignment_node.targets
  132. ]:
  133. return
  134. if subscript_value in (-1, 0):
  135. fn_name = node.func.attrname
  136. new_fn = "rsplit" if subscript_value == -1 else "split"
  137. new_name = (
  138. node.func.as_string().rsplit(fn_name, maxsplit=1)[0]
  139. + new_fn
  140. + f"({node.args[0].as_string()}, maxsplit=1)[{subscript_value}]"
  141. )
  142. self.add_message("use-maxsplit-arg", node=node, args=(new_name,))
  143. @utils.check_messages(
  144. "consider-using-enumerate",
  145. "consider-using-dict-items",
  146. "use-sequence-for-iteration",
  147. )
  148. def visit_for(self, node: nodes.For) -> None:
  149. self._check_consider_using_enumerate(node)
  150. self._check_consider_using_dict_items(node)
  151. self._check_use_sequence_for_iteration(node)
  152. def _check_consider_using_enumerate(self, node: nodes.For) -> None:
  153. """Emit a convention whenever range and len are used for indexing."""
  154. # Verify that we have a `range([start], len(...), [stop])` call and
  155. # that the object which is iterated is used as a subscript in the
  156. # body of the for.
  157. # Is it a proper range call?
  158. if not isinstance(node.iter, nodes.Call):
  159. return
  160. if not self._is_builtin(node.iter.func, "range"):
  161. return
  162. if not node.iter.args:
  163. return
  164. is_constant_zero = (
  165. isinstance(node.iter.args[0], nodes.Const) and node.iter.args[0].value == 0
  166. )
  167. if len(node.iter.args) == 2 and not is_constant_zero:
  168. return
  169. if len(node.iter.args) > 2:
  170. return
  171. # Is it a proper len call?
  172. if not isinstance(node.iter.args[-1], nodes.Call):
  173. return
  174. second_func = node.iter.args[-1].func
  175. if not self._is_builtin(second_func, "len"):
  176. return
  177. len_args = node.iter.args[-1].args
  178. if not len_args or len(len_args) != 1:
  179. return
  180. iterating_object = len_args[0]
  181. if isinstance(iterating_object, nodes.Name):
  182. expected_subscript_val_type = nodes.Name
  183. elif isinstance(iterating_object, nodes.Attribute):
  184. expected_subscript_val_type = nodes.Attribute
  185. else:
  186. return
  187. # If we're defining __iter__ on self, enumerate won't work
  188. scope = node.scope()
  189. if (
  190. isinstance(iterating_object, nodes.Name)
  191. and iterating_object.name == "self"
  192. and scope.name == "__iter__"
  193. ):
  194. return
  195. # Verify that the body of the for loop uses a subscript
  196. # with the object that was iterated. This uses some heuristics
  197. # in order to make sure that the same object is used in the
  198. # for body.
  199. for child in node.body:
  200. for subscript in child.nodes_of_class(nodes.Subscript):
  201. if not isinstance(subscript.value, expected_subscript_val_type):
  202. continue
  203. value = subscript.slice
  204. if not isinstance(value, nodes.Name):
  205. continue
  206. if subscript.value.scope() != node.scope():
  207. # Ignore this subscript if it's not in the same
  208. # scope. This means that in the body of the for
  209. # loop, another scope was created, where the same
  210. # name for the iterating object was used.
  211. continue
  212. if value.name == node.target.name and (
  213. isinstance(subscript.value, nodes.Name)
  214. and iterating_object.name == subscript.value.name
  215. or isinstance(subscript.value, nodes.Attribute)
  216. and iterating_object.attrname == subscript.value.attrname
  217. ):
  218. self.add_message("consider-using-enumerate", node=node)
  219. return
  220. def _check_consider_using_dict_items(self, node: nodes.For) -> None:
  221. """Add message when accessing dict values by index lookup."""
  222. # Verify that we have a .keys() call and
  223. # that the object which is iterated is used as a subscript in the
  224. # body of the for.
  225. iterating_object_name = utils.get_iterating_dictionary_name(node)
  226. if iterating_object_name is None:
  227. return
  228. # Verify that the body of the for loop uses a subscript
  229. # with the object that was iterated. This uses some heuristics
  230. # in order to make sure that the same object is used in the
  231. # for body.
  232. for child in node.body:
  233. for subscript in child.nodes_of_class(nodes.Subscript):
  234. if not isinstance(subscript.value, (nodes.Name, nodes.Attribute)):
  235. continue
  236. value = subscript.slice
  237. if (
  238. not isinstance(value, nodes.Name)
  239. or value.name != node.target.name
  240. or iterating_object_name != subscript.value.as_string()
  241. ):
  242. continue
  243. last_definition_lineno = value.lookup(value.name)[1][-1].lineno
  244. if last_definition_lineno > node.lineno:
  245. # Ignore this subscript if it has been redefined after
  246. # the for loop. This checks for the line number using .lookup()
  247. # to get the line number where the iterating object was last
  248. # defined and compare that to the for loop's line number
  249. continue
  250. if (
  251. isinstance(subscript.parent, nodes.Assign)
  252. and subscript in subscript.parent.targets
  253. or isinstance(subscript.parent, nodes.AugAssign)
  254. and subscript == subscript.parent.target
  255. ):
  256. # Ignore this subscript if it is the target of an assignment
  257. # Early termination as dict index lookup is necessary
  258. return
  259. self.add_message("consider-using-dict-items", node=node)
  260. return
  261. @utils.check_messages(
  262. "consider-using-dict-items",
  263. "use-sequence-for-iteration",
  264. )
  265. def visit_comprehension(self, node: nodes.Comprehension) -> None:
  266. self._check_consider_using_dict_items_comprehension(node)
  267. self._check_use_sequence_for_iteration(node)
  268. def _check_consider_using_dict_items_comprehension(
  269. self, node: nodes.Comprehension
  270. ) -> None:
  271. """Add message when accessing dict values by index lookup."""
  272. iterating_object_name = utils.get_iterating_dictionary_name(node)
  273. if iterating_object_name is None:
  274. return
  275. for child in node.parent.get_children():
  276. for subscript in child.nodes_of_class(nodes.Subscript):
  277. if not isinstance(subscript.value, (nodes.Name, nodes.Attribute)):
  278. continue
  279. value = subscript.slice
  280. if (
  281. not isinstance(value, nodes.Name)
  282. or value.name != node.target.name
  283. or iterating_object_name != subscript.value.as_string()
  284. ):
  285. continue
  286. self.add_message("consider-using-dict-items", node=node)
  287. return
  288. def _check_use_sequence_for_iteration(
  289. self, node: Union[nodes.For, nodes.Comprehension]
  290. ) -> None:
  291. """Check if code iterates over an in-place defined set."""
  292. if isinstance(node.iter, nodes.Set):
  293. self.add_message("use-sequence-for-iteration", node=node.iter)
  294. @utils.check_messages("consider-using-f-string")
  295. def visit_const(self, node: nodes.Const) -> None:
  296. if self._py36_plus:
  297. # f-strings require Python 3.6
  298. if node.pytype() == "builtins.str" and not isinstance(
  299. node.parent, nodes.JoinedStr
  300. ):
  301. self._detect_replacable_format_call(node)
  302. def _detect_replacable_format_call(self, node: nodes.Const) -> None:
  303. """Check whether a string is used in a call to format() or '%' and whether it
  304. can be replaced by a f-string"""
  305. if (
  306. isinstance(node.parent, nodes.Attribute)
  307. and node.parent.attrname == "format"
  308. ):
  309. # Don't warn on referencing / assigning .format without calling it
  310. if not isinstance(node.parent.parent, nodes.Call):
  311. return
  312. if node.parent.parent.args:
  313. for arg in node.parent.parent.args:
  314. # If star expressions with more than 1 element are being used
  315. if isinstance(arg, nodes.Starred):
  316. inferred = utils.safe_infer(arg.value)
  317. if (
  318. isinstance(inferred, astroid.List)
  319. and len(inferred.elts) > 1
  320. ):
  321. return
  322. # Backslashes can't be in f-string expressions
  323. if "\\" in arg.as_string():
  324. return
  325. elif node.parent.parent.keywords:
  326. keyword_args = [
  327. i[0] for i in utils.parse_format_method_string(node.value)[0]
  328. ]
  329. for keyword in node.parent.parent.keywords:
  330. # If keyword is used multiple times
  331. if keyword_args.count(keyword.arg) > 1:
  332. return
  333. keyword = utils.safe_infer(keyword.value)
  334. # If lists of more than one element are being unpacked
  335. if isinstance(keyword, nodes.Dict):
  336. if len(keyword.items) > 1 and len(keyword_args) > 1:
  337. return
  338. # If all tests pass, then raise message
  339. self.add_message(
  340. "consider-using-f-string",
  341. node=node,
  342. line=node.lineno,
  343. col_offset=node.col_offset,
  344. )
  345. elif isinstance(node.parent, nodes.BinOp) and node.parent.op == "%":
  346. # Backslashes can't be in f-string expressions
  347. if "\\" in node.parent.right.as_string():
  348. return
  349. inferred_right = utils.safe_infer(node.parent.right)
  350. # If dicts or lists of length > 1 are used
  351. if isinstance(inferred_right, nodes.Dict):
  352. if len(inferred_right.items) > 1:
  353. return
  354. elif isinstance(inferred_right, nodes.List):
  355. if len(inferred_right.elts) > 1:
  356. return
  357. # If all tests pass, then raise message
  358. self.add_message(
  359. "consider-using-f-string",
  360. node=node,
  361. line=node.lineno,
  362. col_offset=node.col_offset,
  363. )