| OLD | NEW |
| 1 # Copyright (c) 2006-2013 LOGILAB S.A. (Paris, FRANCE). | 1 # Copyright (c) 2006-2013 LOGILAB S.A. (Paris, FRANCE). |
| 2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr | 2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 3 # | 3 # |
| 4 # This program is free software; you can redistribute it and/or modify it under | 4 # This program is free software; you can redistribute it and/or modify it under |
| 5 # the terms of the GNU General Public License as published by the Free Software | 5 # the terms of the GNU General Public License as published by the Free Software |
| 6 # Foundation; either version 2 of the License, or (at your option) any later | 6 # Foundation; either version 2 of the License, or (at your option) any later |
| 7 # version. | 7 # version. |
| 8 # | 8 # |
| 9 # This program is distributed in the hope that it will be useful, but WITHOUT | 9 # This program is distributed in the hope that it will be useful, but WITHOUT |
| 10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | 10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. | 11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
| 12 # | 12 # |
| 13 # You should have received a copy of the GNU General Public License along with | 13 # You should have received a copy of the GNU General Public License along with |
| 14 # this program; if not, write to the Free Software Foundation, Inc., | 14 # this program; if not, write to the Free Software Foundation, Inc., |
| 15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | 15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 16 """try to find more bugs in the code using astroid inference capabilities | 16 """try to find more bugs in the code using astroid inference capabilities |
| 17 """ | 17 """ |
| 18 | 18 |
| 19 import re | 19 import re |
| 20 import shlex | 20 import shlex |
| 21 | 21 |
| 22 import astroid | 22 import astroid |
| 23 from astroid import InferenceError, NotFoundError, YES, Instance | 23 from astroid import InferenceError, NotFoundError, YES, Instance |
| 24 from astroid.bases import BUILTINS | 24 from astroid.bases import BUILTINS |
| 25 | 25 |
| 26 from pylint.interfaces import IAstroidChecker | 26 from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE |
| 27 from pylint.checkers import BaseChecker | 27 from pylint.checkers import BaseChecker |
| 28 from pylint.checkers.utils import safe_infer, is_super, check_messages | 28 from pylint.checkers.utils import ( |
| 29 safe_infer, is_super, |
| 30 check_messages, decorated_with_property) |
| 29 | 31 |
| 30 MSGS = { | 32 MSGS = { |
| 31 'E1101': ('%s %r has no %r member', | 33 'E1101': ('%s %r has no %r member', |
| 32 'no-member', | 34 'no-member', |
| 33 'Used when a variable is accessed for an unexistent member.'), | 35 'Used when a variable is accessed for an unexistent member.', |
| 36 {'old_names': [('E1103', 'maybe-no-member')]}), |
| 34 'E1102': ('%s is not callable', | 37 'E1102': ('%s is not callable', |
| 35 'not-callable', | 38 'not-callable', |
| 36 'Used when an object being called has been inferred to a non \ | 39 'Used when an object being called has been inferred to a non \ |
| 37 callable object'), | 40 callable object'), |
| 38 'E1103': ('%s %r has no %r member (but some types could not be inferred)', | |
| 39 'maybe-no-member', | |
| 40 'Used when a variable is accessed for an unexistent member, but \ | |
| 41 astroid was not able to interpret all possible types of this \ | |
| 42 variable.'), | |
| 43 'E1111': ('Assigning to function call which doesn\'t return', | 41 'E1111': ('Assigning to function call which doesn\'t return', |
| 44 'assignment-from-no-return', | 42 'assignment-from-no-return', |
| 45 'Used when an assignment is done on a function call but the \ | 43 'Used when an assignment is done on a function call but the \ |
| 46 inferred function doesn\'t return anything.'), | 44 inferred function doesn\'t return anything.'), |
| 47 'W1111': ('Assigning to function call which only returns None', | 45 'W1111': ('Assigning to function call which only returns None', |
| 48 'assignment-from-none', | 46 'assignment-from-none', |
| 49 'Used when an assignment is done on a function call but the \ | 47 'Used when an assignment is done on a function call but the \ |
| 50 inferred function returns nothing but None.'), | 48 inferred function returns nothing but None.'), |
| 51 | 49 |
| 52 'E1120': ('No value for argument %s in %s call', | 50 'E1120': ('No value for argument %s in %s call', |
| 53 'no-value-for-parameter', | 51 'no-value-for-parameter', |
| 54 'Used when a function call passes too few arguments.'), | 52 'Used when a function call passes too few arguments.'), |
| 55 'E1121': ('Too many positional arguments for %s call', | 53 'E1121': ('Too many positional arguments for %s call', |
| 56 'too-many-function-args', | 54 'too-many-function-args', |
| 57 'Used when a function call passes too many positional \ | 55 'Used when a function call passes too many positional \ |
| 58 arguments.'), | 56 arguments.'), |
| 59 'E1122': ('Duplicate keyword argument %r in %s call', | |
| 60 'duplicate-keyword-arg', | |
| 61 'Used when a function call passes the same keyword argument \ | |
| 62 multiple times.', | |
| 63 {'maxversion': (2, 6)}), | |
| 64 'E1123': ('Unexpected keyword argument %r in %s call', | 57 'E1123': ('Unexpected keyword argument %r in %s call', |
| 65 'unexpected-keyword-arg', | 58 'unexpected-keyword-arg', |
| 66 'Used when a function call passes a keyword argument that \ | 59 'Used when a function call passes a keyword argument that \ |
| 67 doesn\'t correspond to one of the function\'s parameter names.'), | 60 doesn\'t correspond to one of the function\'s parameter names.'), |
| 68 'E1124': ('Argument %r passed by position and keyword in %s call', | 61 'E1124': ('Argument %r passed by position and keyword in %s call', |
| 69 'redundant-keyword-arg', | 62 'redundant-keyword-arg', |
| 70 'Used when a function call would result in assigning multiple \ | 63 'Used when a function call would result in assigning multiple \ |
| 71 values to a function parameter, one value from a positional \ | 64 values to a function parameter, one value from a positional \ |
| 72 argument and one from a keyword argument.'), | 65 argument and one from a keyword argument.'), |
| 73 'E1125': ('Missing mandatory keyword argument %r in %s call', | 66 'E1125': ('Missing mandatory keyword argument %r in %s call', |
| (...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 185 if self.config.zope: | 178 if self.config.zope: |
| 186 self.generated_members.extend(('REQUEST', 'acl_users', 'aq_parent')) | 179 self.generated_members.extend(('REQUEST', 'acl_users', 'aq_parent')) |
| 187 | 180 |
| 188 def visit_assattr(self, node): | 181 def visit_assattr(self, node): |
| 189 if isinstance(node.ass_type(), astroid.AugAssign): | 182 if isinstance(node.ass_type(), astroid.AugAssign): |
| 190 self.visit_getattr(node) | 183 self.visit_getattr(node) |
| 191 | 184 |
| 192 def visit_delattr(self, node): | 185 def visit_delattr(self, node): |
| 193 self.visit_getattr(node) | 186 self.visit_getattr(node) |
| 194 | 187 |
| 195 @check_messages('no-member', 'maybe-no-member') | 188 @check_messages('no-member') |
| 196 def visit_getattr(self, node): | 189 def visit_getattr(self, node): |
| 197 """check that the accessed attribute exists | 190 """check that the accessed attribute exists |
| 198 | 191 |
| 199 to avoid to much false positives for now, we'll consider the code as | 192 to avoid to much false positives for now, we'll consider the code as |
| 200 correct if a single of the inferred nodes has the accessed attribute. | 193 correct if a single of the inferred nodes has the accessed attribute. |
| 201 | 194 |
| 202 function/method, super call and metaclasses are ignored | 195 function/method, super call and metaclasses are ignored |
| 203 """ | 196 """ |
| 204 # generated_members may containt regular expressions | 197 # generated_members may containt regular expressions |
| 205 # (surrounded by quote `"` and followed by a comma `,`) | 198 # (surrounded by quote `"` and followed by a comma `,`) |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 247 # XXX method / function | 240 # XXX method / function |
| 248 continue | 241 continue |
| 249 except NotFoundError: | 242 except NotFoundError: |
| 250 if isinstance(owner, astroid.Function) and owner.decorators: | 243 if isinstance(owner, astroid.Function) and owner.decorators: |
| 251 continue | 244 continue |
| 252 if isinstance(owner, Instance) and owner.has_dynamic_getattr(): | 245 if isinstance(owner, Instance) and owner.has_dynamic_getattr(): |
| 253 continue | 246 continue |
| 254 # explicit skipping of module member access | 247 # explicit skipping of module member access |
| 255 if owner.root().name in self.config.ignored_modules: | 248 if owner.root().name in self.config.ignored_modules: |
| 256 continue | 249 continue |
| 250 if isinstance(owner, astroid.Class): |
| 251 # Look up in the metaclass only if the owner is itself |
| 252 # a class. |
| 253 # TODO: getattr doesn't return by default members |
| 254 # from the metaclass, because handling various cases |
| 255 # of methods accessible from the metaclass itself |
| 256 # and/or subclasses only is too complicated for little to |
| 257 # no benefit. |
| 258 metaclass = owner.metaclass() |
| 259 try: |
| 260 if metaclass and metaclass.getattr(node.attrname): |
| 261 continue |
| 262 except NotFoundError: |
| 263 pass |
| 257 missingattr.add((owner, name)) | 264 missingattr.add((owner, name)) |
| 258 continue | 265 continue |
| 259 # stop on the first found | 266 # stop on the first found |
| 260 break | 267 break |
| 261 else: | 268 else: |
| 262 # we have not found any node with the attributes, display the | 269 # we have not found any node with the attributes, display the |
| 263 # message for infered nodes | 270 # message for infered nodes |
| 264 done = set() | 271 done = set() |
| 265 for owner, name in missingattr: | 272 for owner, name in missingattr: |
| 266 if isinstance(owner, Instance): | 273 if isinstance(owner, Instance): |
| 267 actual = owner._proxied | 274 actual = owner._proxied |
| 268 else: | 275 else: |
| 269 actual = owner | 276 actual = owner |
| 270 if actual in done: | 277 if actual in done: |
| 271 continue | 278 continue |
| 272 done.add(actual) | 279 done.add(actual) |
| 273 if inference_failure: | 280 confidence = INFERENCE if not inference_failure else INFERENCE_F
AILURE |
| 274 msgid = 'maybe-no-member' | 281 self.add_message('no-member', node=node, |
| 275 else: | |
| 276 msgid = 'no-member' | |
| 277 self.add_message(msgid, node=node, | |
| 278 args=(owner.display_type(), name, | 282 args=(owner.display_type(), name, |
| 279 node.attrname)) | 283 node.attrname), |
| 284 confidence=confidence) |
| 280 | 285 |
| 281 @check_messages('assignment-from-no-return', 'assignment-from-none') | 286 @check_messages('assignment-from-no-return', 'assignment-from-none') |
| 282 def visit_assign(self, node): | 287 def visit_assign(self, node): |
| 283 """check that if assigning to a function call, the function is | 288 """check that if assigning to a function call, the function is |
| 284 possibly returning something valuable | 289 possibly returning something valuable |
| 285 """ | 290 """ |
| 286 if not isinstance(node.value, astroid.CallFunc): | 291 if not isinstance(node.value, astroid.CallFunc): |
| 287 return | 292 return |
| 288 function_node = safe_infer(node.value.func) | 293 function_node = safe_infer(node.value.func) |
| 289 # skip class, generator and incomplete function definition | 294 # skip class, generator and incomplete function definition |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 326 klass = safe_infer(expr) | 331 klass = safe_infer(expr) |
| 327 if (klass is None or klass is astroid.YES or | 332 if (klass is None or klass is astroid.YES or |
| 328 not isinstance(klass, astroid.Instance)): | 333 not isinstance(klass, astroid.Instance)): |
| 329 return | 334 return |
| 330 | 335 |
| 331 try: | 336 try: |
| 332 attrs = klass._proxied.getattr(node.func.attrname) | 337 attrs = klass._proxied.getattr(node.func.attrname) |
| 333 except astroid.NotFoundError: | 338 except astroid.NotFoundError: |
| 334 return | 339 return |
| 335 | 340 |
| 336 stop_checking = False | |
| 337 for attr in attrs: | 341 for attr in attrs: |
| 338 if attr is astroid.YES: | 342 if attr is astroid.YES: |
| 339 continue | 343 continue |
| 340 if stop_checking: | |
| 341 break | |
| 342 if not isinstance(attr, astroid.Function): | 344 if not isinstance(attr, astroid.Function): |
| 343 continue | 345 continue |
| 344 | 346 |
| 345 # Decorated, see if it is decorated with a property | 347 # Decorated, see if it is decorated with a property |
| 346 if not attr.decorators: | 348 if decorated_with_property(attr): |
| 347 continue | 349 self.add_message('not-callable', node=node, |
| 348 for decorator in attr.decorators.nodes: | 350 args=node.func.as_string()) |
| 349 if not isinstance(decorator, astroid.Name): | 351 break |
| 350 continue | |
| 351 try: | |
| 352 for infered in decorator.infer(): | |
| 353 property_like = False | |
| 354 if isinstance(infered, astroid.Class): | |
| 355 if (infered.root().name == BUILTINS and | |
| 356 infered.name == 'property'): | |
| 357 property_like = True | |
| 358 else: | |
| 359 for ancestor in infered.ancestors(): | |
| 360 if (ancestor.name == 'property' and | |
| 361 ancestor.root().name == BUILTINS): | |
| 362 property_like = True | |
| 363 break | |
| 364 if property_like: | |
| 365 self.add_message('not-callable', node=node, | |
| 366 args=node.func.as_string()) | |
| 367 stop_checking = True | |
| 368 break | |
| 369 except InferenceError: | |
| 370 pass | |
| 371 if stop_checking: | |
| 372 break | |
| 373 | 352 |
| 374 @check_messages(*(list(MSGS.keys()))) | 353 @check_messages(*(list(MSGS.keys()))) |
| 375 def visit_callfunc(self, node): | 354 def visit_callfunc(self, node): |
| 376 """check that called functions/methods are inferred to callable objects, | 355 """check that called functions/methods are inferred to callable objects, |
| 377 and that the arguments passed to the function match the parameters in | 356 and that the arguments passed to the function match the parameters in |
| 378 the inferred function's definition | 357 the inferred function's definition |
| 379 """ | 358 """ |
| 380 # Build the set of keyword arguments, checking for duplicate keywords, | 359 # Build the set of keyword arguments, checking for duplicate keywords, |
| 381 # and count the positional arguments. | 360 # and count the positional arguments. |
| 382 keyword_args = set() | 361 keyword_args = set() |
| 383 num_positional_args = 0 | 362 num_positional_args = 0 |
| 384 for arg in node.args: | 363 for arg in node.args: |
| 385 if isinstance(arg, astroid.Keyword): | 364 if isinstance(arg, astroid.Keyword): |
| 386 keyword = arg.arg | 365 keyword_args.add(arg.arg) |
| 387 if keyword in keyword_args: | |
| 388 self.add_message('duplicate-keyword-arg', node=node, | |
| 389 args=(keyword, 'function')) | |
| 390 keyword_args.add(keyword) | |
| 391 else: | 366 else: |
| 392 num_positional_args += 1 | 367 num_positional_args += 1 |
| 393 | 368 |
| 394 called = safe_infer(node.func) | 369 called = safe_infer(node.func) |
| 395 # only function, generator and object defining __call__ are allowed | 370 # only function, generator and object defining __call__ are allowed |
| 396 if called is not None and not called.callable(): | 371 if called is not None and not called.callable(): |
| 397 self.add_message('not-callable', node=node, | 372 self.add_message('not-callable', node=node, |
| 398 args=node.func.as_string()) | 373 args=node.func.as_string()) |
| 399 | 374 |
| 400 self._check_uninferable_callfunc(node) | 375 self._check_uninferable_callfunc(node) |
| (...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 542 @check_messages('invalid-sequence-index') | 517 @check_messages('invalid-sequence-index') |
| 543 def visit_index(self, node): | 518 def visit_index(self, node): |
| 544 if not node.parent or not hasattr(node.parent, "value"): | 519 if not node.parent or not hasattr(node.parent, "value"): |
| 545 return | 520 return |
| 546 | 521 |
| 547 # Look for index operations where the parent is a sequence type. | 522 # Look for index operations where the parent is a sequence type. |
| 548 # If the types can be determined, only allow indices to be int, | 523 # If the types can be determined, only allow indices to be int, |
| 549 # slice or instances with __index__. | 524 # slice or instances with __index__. |
| 550 | 525 |
| 551 parent_type = safe_infer(node.parent.value) | 526 parent_type = safe_infer(node.parent.value) |
| 552 | |
| 553 if not isinstance(parent_type, (astroid.Class, astroid.Instance)): | 527 if not isinstance(parent_type, (astroid.Class, astroid.Instance)): |
| 554 return | 528 return |
| 555 | 529 |
| 556 # Determine what method on the parent this index will use | 530 # Determine what method on the parent this index will use |
| 557 # The parent of this node will be a Subscript, and the parent of that | 531 # The parent of this node will be a Subscript, and the parent of that |
| 558 # node determines if the Subscript is a get, set, or delete operation. | 532 # node determines if the Subscript is a get, set, or delete operation. |
| 559 operation = node.parent.parent | 533 operation = node.parent.parent |
| 560 if isinstance(operation, astroid.Assign): | 534 if isinstance(operation, astroid.Assign): |
| 561 methodname = '__setitem__' | 535 methodname = '__setitem__' |
| 562 elif isinstance(operation, astroid.Delete): | 536 elif isinstance(operation, astroid.Delete): |
| 563 methodname = '__delitem__' | 537 methodname = '__delitem__' |
| 564 else: | 538 else: |
| 565 methodname = '__getitem__' | 539 methodname = '__getitem__' |
| 566 | 540 |
| 567 # Check if this instance's __getitem__, __setitem__, or __delitem__, as | 541 # Check if this instance's __getitem__, __setitem__, or __delitem__, as |
| 568 # appropriate to the statement, is implemented in a builtin sequence | 542 # appropriate to the statement, is implemented in a builtin sequence |
| 569 # type. This way we catch subclasses of sequence types but skip classes | 543 # type. This way we catch subclasses of sequence types but skip classes |
| 570 # that override __getitem__ and which may allow non-integer indices. | 544 # that override __getitem__ and which may allow non-integer indices. |
| 571 try: | 545 try: |
| 572 methods = parent_type.getattr(methodname) | 546 methods = parent_type.getattr(methodname) |
| 573 if methods is astroid.YES: | 547 if methods is astroid.YES: |
| 574 return | 548 return |
| 575 itemmethod = methods[0] | 549 itemmethod = methods[0] |
| 576 except (astroid.NotFoundError, IndexError): | 550 except (astroid.NotFoundError, IndexError): |
| 577 return | 551 return |
| 578 | 552 |
| 579 if not isinstance(itemmethod, astroid.Function): | 553 if not isinstance(itemmethod, astroid.Function): |
| 580 return | 554 return |
| 581 | |
| 582 if itemmethod.root().name != BUILTINS: | 555 if itemmethod.root().name != BUILTINS: |
| 583 return | 556 return |
| 584 | |
| 585 if not itemmethod.parent: | 557 if not itemmethod.parent: |
| 586 return | 558 return |
| 587 | |
| 588 if itemmethod.parent.name not in SEQUENCE_TYPES: | 559 if itemmethod.parent.name not in SEQUENCE_TYPES: |
| 589 return | 560 return |
| 590 | 561 |
| 591 # For ExtSlice objects coming from visit_extslice, no further | 562 # For ExtSlice objects coming from visit_extslice, no further |
| 592 # inference is necessary, since if we got this far the ExtSlice | 563 # inference is necessary, since if we got this far the ExtSlice |
| 593 # is an error. | 564 # is an error. |
| 594 if isinstance(node, astroid.ExtSlice): | 565 if isinstance(node, astroid.ExtSlice): |
| 595 index_type = node | 566 index_type = node |
| 596 else: | 567 else: |
| 597 index_type = safe_infer(node) | 568 index_type = safe_infer(node) |
| 598 | |
| 599 if index_type is None or index_type is astroid.YES: | 569 if index_type is None or index_type is astroid.YES: |
| 600 return | 570 return |
| 601 | 571 |
| 602 # Constants must be of type int | 572 # Constants must be of type int |
| 603 if isinstance(index_type, astroid.Const): | 573 if isinstance(index_type, astroid.Const): |
| 604 if isinstance(index_type.value, int): | 574 if isinstance(index_type.value, int): |
| 605 return | 575 return |
| 606 # Instance values must be int, slice, or have an __index__ method | 576 # Instance values must be int, slice, or have an __index__ method |
| 607 elif isinstance(index_type, astroid.Instance): | 577 elif isinstance(index_type, astroid.Instance): |
| 608 if index_type.pytype() in (BUILTINS + '.int', BUILTINS + '.slice'): | 578 if index_type.pytype() in (BUILTINS + '.int', BUILTINS + '.slice'): |
| 609 return | 579 return |
| 610 | |
| 611 try: | 580 try: |
| 612 index_type.getattr('__index__') | 581 index_type.getattr('__index__') |
| 613 return | 582 return |
| 614 except astroid.NotFoundError: | 583 except astroid.NotFoundError: |
| 615 pass | 584 pass |
| 616 | 585 |
| 617 # Anything else is an error | 586 # Anything else is an error |
| 618 self.add_message('invalid-sequence-index', node=node) | 587 self.add_message('invalid-sequence-index', node=node) |
| 619 | 588 |
| 620 @check_messages('invalid-slice-index') | 589 @check_messages('invalid-slice-index') |
| 621 def visit_slice(self, node): | 590 def visit_slice(self, node): |
| 622 # Check the type of each part of the slice | 591 # Check the type of each part of the slice |
| 623 for index in (node.lower, node.upper, node.step): | 592 for index in (node.lower, node.upper, node.step): |
| 624 if index is None: | 593 if index is None: |
| 625 continue | 594 continue |
| 626 | 595 |
| 627 index_type = safe_infer(index) | 596 index_type = safe_infer(index) |
| 628 | |
| 629 if index_type is None or index_type is astroid.YES: | 597 if index_type is None or index_type is astroid.YES: |
| 630 continue | 598 continue |
| 631 | 599 |
| 632 # Constants must of type int or None | 600 # Constants must of type int or None |
| 633 if isinstance(index_type, astroid.Const): | 601 if isinstance(index_type, astroid.Const): |
| 634 if isinstance(index_type.value, (int, type(None))): | 602 if isinstance(index_type.value, (int, type(None))): |
| 635 continue | 603 continue |
| 636 # Instance values must be of type int, None or an object | 604 # Instance values must be of type int, None or an object |
| 637 # with __index__ | 605 # with __index__ |
| 638 elif isinstance(index_type, astroid.Instance): | 606 elif isinstance(index_type, astroid.Instance): |
| 639 if index_type.pytype() in (BUILTINS + '.int', | 607 if index_type.pytype() in (BUILTINS + '.int', |
| 640 BUILTINS + '.NoneType'): | 608 BUILTINS + '.NoneType'): |
| 641 continue | 609 continue |
| 642 | 610 |
| 643 try: | 611 try: |
| 644 index_type.getattr('__index__') | 612 index_type.getattr('__index__') |
| 645 return | 613 return |
| 646 except astroid.NotFoundError: | 614 except astroid.NotFoundError: |
| 647 pass | 615 pass |
| 648 | 616 |
| 649 # Anything else is an error | 617 # Anything else is an error |
| 650 self.add_message('invalid-slice-index', node=node) | 618 self.add_message('invalid-slice-index', node=node) |
| 651 | 619 |
| 652 def register(linter): | 620 def register(linter): |
| 653 """required method to auto register this checker """ | 621 """required method to auto register this checker """ |
| 654 linter.register_checker(TypeChecker(linter)) | 622 linter.register_checker(TypeChecker(linter)) |
| OLD | NEW |