# -*- coding: utf-8 -*- # Copyright (c) 2014-2018 Claudiu Popa # Copyright (c) 2014-2015 Brett Cannon # Copyright (c) 2015 Simu Toni # Copyright (c) 2015 Pavel Roskin # Copyright (c) 2015 Ionel Cristian Maries # Copyright (c) 2015 Cosmin Poieana # Copyright (c) 2015 Viorel Stirbu # Copyright (c) 2016, 2018 Jakub Wilk # Copyright (c) 2016-2017 Roy Williams # Copyright (c) 2016 Roy Williams # Copyright (c) 2016 Łukasz Rogalski # Copyright (c) 2016 Erik # Copyright (c) 2017 Ville Skyttä # Copyright (c) 2017 Daniel Miller # Copyright (c) 2017 hippo91 # Copyright (c) 2017 ahirnish # Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com> # Copyright (c) 2018 Anthony Sottile # Copyright (c) 2018 Ashley Whetter # Copyright (c) 2018 Ville Skyttä # Copyright (c) 2018 gaurikholkar # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint/blob/master/COPYING """Check Python 2 code for Python 2/3 source-compatible issues.""" from __future__ import absolute_import, print_function from collections import namedtuple import re import sys import tokenize from typing import FrozenSet import astroid from astroid import bases from pylint import checkers, interfaces from pylint.checkers.utils import node_ignores_exception, find_try_except_wrapper_node from pylint.interfaces import INFERENCE_FAILURE, INFERENCE from pylint.utils import WarningScope from pylint.checkers import utils _ZERO = re.compile("^0+$") def _is_old_octal(literal): if _ZERO.match(literal): return False if re.match(r"0\d+", literal): try: int(literal, 8) except ValueError: return False return True return None def _inferred_value_is_dict(value): if isinstance(value, astroid.Dict): return True return isinstance(value, astroid.Instance) and "dict" in value.basenames def _is_builtin(node): return getattr(node, "name", None) in ("__builtin__", "builtins") _ACCEPTS_ITERATOR = { "iter", "list", "tuple", "sorted", "set", "sum", "any", "all", "enumerate", "dict", "filter", "reversed", "max", "min", "frozenset", "OrderedDict", } ATTRIBUTES_ACCEPTS_ITERATOR = {"join", "from_iterable"} _BUILTIN_METHOD_ACCEPTS_ITERATOR = { "builtins.list.extend", "builtins.dict.update", "builtins.set.update", } DICT_METHODS = {"items", "keys", "values"} def _in_iterating_context(node): """Check if the node is being used as an iterator. Definition is taken from lib2to3.fixer_util.in_special_context(). """ parent = node.parent # Since a call can't be the loop variant we only need to know if the node's # parent is a 'for' loop to know it's being used as the iterator for the # loop. if isinstance(parent, astroid.For): return True # Need to make sure the use of the node is in the iterator part of the # comprehension. if isinstance(parent, astroid.Comprehension): if parent.iter == node: return True # Various built-ins can take in an iterable or list and lead to the same # value. elif isinstance(parent, astroid.Call): if isinstance(parent.func, astroid.Name): parent_scope = parent.func.lookup(parent.func.name)[0] if _is_builtin(parent_scope) and parent.func.name in _ACCEPTS_ITERATOR: return True elif isinstance(parent.func, astroid.Attribute): if parent.func.attrname in ATTRIBUTES_ACCEPTS_ITERATOR: return True inferred = utils.safe_infer(parent.func) if inferred: if inferred.qname() in _BUILTIN_METHOD_ACCEPTS_ITERATOR: return True root = inferred.root() if root and root.name == "itertools": return True # If the call is in an unpacking, there's no need to warn, # since it can be considered iterating. elif isinstance(parent, astroid.Assign) and isinstance( parent.targets[0], (astroid.List, astroid.Tuple) ): if len(parent.targets[0].elts) > 1: return True # If the call is in a containment check, we consider that to # be an iterating context elif ( isinstance(parent, astroid.Compare) and len(parent.ops) == 1 and parent.ops[0][0] == "in" ): return True # Also if it's an `yield from`, that's fair elif isinstance(parent, astroid.YieldFrom): return True if isinstance(parent, astroid.Starred): return True return False def _is_conditional_import(node): """Checks if an import node is in the context of a conditional. """ parent = node.parent return isinstance( parent, (astroid.TryExcept, astroid.ExceptHandler, astroid.If, astroid.IfExp) ) Branch = namedtuple("Branch", ["node", "is_py2_only"]) class Python3Checker(checkers.BaseChecker): __implements__ = interfaces.IAstroidChecker enabled = False name = "python3" msgs = { # Errors for what will syntactically break in Python 3, warnings for # everything else. "E1601": ( "print statement used", "print-statement", "Used when a print statement is used " "(`print` is a function in Python 3)", ), "E1602": ( "Parameter unpacking specified", "parameter-unpacking", "Used when parameter unpacking is specified for a function" "(Python 3 doesn't allow it)", ), "E1603": ( "Implicit unpacking of exceptions is not supported in Python 3", "unpacking-in-except", "Python3 will not allow implicit unpacking of " "exceptions in except clauses. " "See http://www.python.org/dev/peps/pep-3110/", {"old_names": [("W0712", "unpacking-in-except")]}, ), "E1604": ( "Use raise ErrorClass(args) instead of raise ErrorClass, args.", "old-raise-syntax", "Used when the alternate raise syntax " "'raise foo, bar' is used " "instead of 'raise foo(bar)'.", {"old_names": [("W0121", "old-raise-syntax")]}, ), "E1605": ( "Use of the `` operator", "backtick", 'Used when the deprecated "``" (backtick) operator is used ' "instead of the str() function.", {"scope": WarningScope.NODE, "old_names": [("W0333", "backtick")]}, ), "E1609": ( "Import * only allowed at module level", "import-star-module-level", "Used when the import star syntax is used somewhere " "else than the module level.", {"maxversion": (3, 0)}, ), "W1601": ( "apply built-in referenced", "apply-builtin", "Used when the apply built-in function is referenced " "(missing from Python 3)", ), "W1602": ( "basestring built-in referenced", "basestring-builtin", "Used when the basestring built-in function is referenced " "(missing from Python 3)", ), "W1603": ( "buffer built-in referenced", "buffer-builtin", "Used when the buffer built-in function is referenced " "(missing from Python 3)", ), "W1604": ( "cmp built-in referenced", "cmp-builtin", "Used when the cmp built-in function is referenced " "(missing from Python 3)", ), "W1605": ( "coerce built-in referenced", "coerce-builtin", "Used when the coerce built-in function is referenced " "(missing from Python 3)", ), "W1606": ( "execfile built-in referenced", "execfile-builtin", "Used when the execfile built-in function is referenced " "(missing from Python 3)", ), "W1607": ( "file built-in referenced", "file-builtin", "Used when the file built-in function is referenced " "(missing from Python 3)", ), "W1608": ( "long built-in referenced", "long-builtin", "Used when the long built-in function is referenced " "(missing from Python 3)", ), "W1609": ( "raw_input built-in referenced", "raw_input-builtin", "Used when the raw_input built-in function is referenced " "(missing from Python 3)", ), "W1610": ( "reduce built-in referenced", "reduce-builtin", "Used when the reduce built-in function is referenced " "(missing from Python 3)", ), "W1611": ( "StandardError built-in referenced", "standarderror-builtin", "Used when the StandardError built-in function is referenced " "(missing from Python 3)", ), "W1612": ( "unicode built-in referenced", "unicode-builtin", "Used when the unicode built-in function is referenced " "(missing from Python 3)", ), "W1613": ( "xrange built-in referenced", "xrange-builtin", "Used when the xrange built-in function is referenced " "(missing from Python 3)", ), "W1614": ( "__coerce__ method defined", "coerce-method", "Used when a __coerce__ method is defined " "(method is not used by Python 3)", ), "W1615": ( "__delslice__ method defined", "delslice-method", "Used when a __delslice__ method is defined " "(method is not used by Python 3)", ), "W1616": ( "__getslice__ method defined", "getslice-method", "Used when a __getslice__ method is defined " "(method is not used by Python 3)", ), "W1617": ( "__setslice__ method defined", "setslice-method", "Used when a __setslice__ method is defined " "(method is not used by Python 3)", ), "W1618": ( "import missing `from __future__ import absolute_import`", "no-absolute-import", "Used when an import is not accompanied by " "``from __future__ import absolute_import`` " "(default behaviour in Python 3)", ), "W1619": ( "division w/o __future__ statement", "old-division", "Used for non-floor division w/o a float literal or " "``from __future__ import division`` " "(Python 3 returns a float for int division unconditionally)", ), "W1620": ( "Calling a dict.iter*() method", "dict-iter-method", "Used for calls to dict.iterkeys(), itervalues() or iteritems() " "(Python 3 lacks these methods)", ), "W1621": ( "Calling a dict.view*() method", "dict-view-method", "Used for calls to dict.viewkeys(), viewvalues() or viewitems() " "(Python 3 lacks these methods)", ), "W1622": ( "Called a next() method on an object", "next-method-called", "Used when an object's next() method is called " "(Python 3 uses the next() built-in function)", ), "W1623": ( "Assigning to a class's __metaclass__ attribute", "metaclass-assignment", "Used when a metaclass is specified by assigning to __metaclass__ " "(Python 3 specifies the metaclass as a class statement argument)", ), "W1624": ( "Indexing exceptions will not work on Python 3", "indexing-exception", "Indexing exceptions will not work on Python 3. Use " "`exception.args[index]` instead.", {"old_names": [("W0713", "indexing-exception")]}, ), "W1625": ( "Raising a string exception", "raising-string", "Used when a string exception is raised. This will not " "work on Python 3.", {"old_names": [("W0701", "raising-string")]}, ), "W1626": ( "reload built-in referenced", "reload-builtin", "Used when the reload built-in function is referenced " "(missing from Python 3). You can use instead imp.reload " "or importlib.reload.", ), "W1627": ( "__oct__ method defined", "oct-method", "Used when an __oct__ method is defined " "(method is not used by Python 3)", ), "W1628": ( "__hex__ method defined", "hex-method", "Used when a __hex__ method is defined (method is not used by Python 3)", ), "W1629": ( "__nonzero__ method defined", "nonzero-method", "Used when a __nonzero__ method is defined " "(method is not used by Python 3)", ), "W1630": ( "__cmp__ method defined", "cmp-method", "Used when a __cmp__ method is defined (method is not used by Python 3)", ), # 'W1631': replaced by W1636 "W1632": ( "input built-in referenced", "input-builtin", "Used when the input built-in is referenced " "(backwards-incompatible semantics in Python 3)", ), "W1633": ( "round built-in referenced", "round-builtin", "Used when the round built-in is referenced " "(backwards-incompatible semantics in Python 3)", ), "W1634": ( "intern built-in referenced", "intern-builtin", "Used when the intern built-in is referenced " "(Moved to sys.intern in Python 3)", ), "W1635": ( "unichr built-in referenced", "unichr-builtin", "Used when the unichr built-in is referenced (Use chr in Python 3)", ), "W1636": ( "map built-in referenced when not iterating", "map-builtin-not-iterating", "Used when the map built-in is referenced in a non-iterating " "context (returns an iterator in Python 3)", {"old_names": [("W1631", "implicit-map-evaluation")]}, ), "W1637": ( "zip built-in referenced when not iterating", "zip-builtin-not-iterating", "Used when the zip built-in is referenced in a non-iterating " "context (returns an iterator in Python 3)", ), "W1638": ( "range built-in referenced when not iterating", "range-builtin-not-iterating", "Used when the range built-in is referenced in a non-iterating " "context (returns an iterator in Python 3)", ), "W1639": ( "filter built-in referenced when not iterating", "filter-builtin-not-iterating", "Used when the filter built-in is referenced in a non-iterating " "context (returns an iterator in Python 3)", ), "W1640": ( "Using the cmp argument for list.sort / sorted", "using-cmp-argument", "Using the cmp argument for list.sort or the sorted " "builtin should be avoided, since it was removed in " "Python 3. Using either `key` or `functools.cmp_to_key` " "should be preferred.", ), "W1641": ( "Implementing __eq__ without also implementing __hash__", "eq-without-hash", "Used when a class implements __eq__ but not __hash__. In Python 2, objects " "get object.__hash__ as the default implementation, in Python 3 objects get " "None as their default __hash__ implementation if they also implement __eq__.", ), "W1642": ( "__div__ method defined", "div-method", "Used when a __div__ method is defined. Using `__truediv__` and setting" "__div__ = __truediv__ should be preferred." "(method is not used by Python 3)", ), "W1643": ( "__idiv__ method defined", "idiv-method", "Used when an __idiv__ method is defined. Using `__itruediv__` and setting" "__idiv__ = __itruediv__ should be preferred." "(method is not used by Python 3)", ), "W1644": ( "__rdiv__ method defined", "rdiv-method", "Used when a __rdiv__ method is defined. Using `__rtruediv__` and setting" "__rdiv__ = __rtruediv__ should be preferred." "(method is not used by Python 3)", ), "W1645": ( "Exception.message removed in Python 3", "exception-message-attribute", "Used when the message attribute is accessed on an Exception. Use " "str(exception) instead.", ), "W1646": ( "non-text encoding used in str.decode", "invalid-str-codec", "Used when using str.encode or str.decode with a non-text encoding. Use " "codecs module to handle arbitrary codecs.", ), "W1647": ( "sys.maxint removed in Python 3", "sys-max-int", "Used when accessing sys.maxint. Use sys.maxsize instead.", ), "W1648": ( "Module moved in Python 3", "bad-python3-import", "Used when importing a module that no longer exists in Python 3.", ), "W1649": ( "Accessing a deprecated function on the string module", "deprecated-string-function", "Used when accessing a string function that has been deprecated in Python 3.", ), "W1650": ( "Using str.translate with deprecated deletechars parameters", "deprecated-str-translate-call", "Used when using the deprecated deletechars parameters from str.translate. Use " "re.sub to remove the desired characters ", ), "W1651": ( "Accessing a deprecated function on the itertools module", "deprecated-itertools-function", "Used when accessing a function on itertools that has been removed in Python 3.", ), "W1652": ( "Accessing a deprecated fields on the types module", "deprecated-types-field", "Used when accessing a field on types that has been removed in Python 3.", ), "W1653": ( "next method defined", "next-method-defined", "Used when a next method is defined that would be an iterator in Python 2 but " "is treated as a normal function in Python 3.", ), "W1654": ( "dict.items referenced when not iterating", "dict-items-not-iterating", "Used when dict.items is referenced in a non-iterating " "context (returns an iterator in Python 3)", ), "W1655": ( "dict.keys referenced when not iterating", "dict-keys-not-iterating", "Used when dict.keys is referenced in a non-iterating " "context (returns an iterator in Python 3)", ), "W1656": ( "dict.values referenced when not iterating", "dict-values-not-iterating", "Used when dict.values is referenced in a non-iterating " "context (returns an iterator in Python 3)", ), "W1657": ( "Accessing a removed attribute on the operator module", "deprecated-operator-function", "Used when accessing a field on operator module that has been " "removed in Python 3.", ), "W1658": ( "Accessing a removed attribute on the urllib module", "deprecated-urllib-function", "Used when accessing a field on urllib module that has been " "removed or moved in Python 3.", ), "W1659": ( "Accessing a removed xreadlines attribute", "xreadlines-attribute", "Used when accessing the xreadlines() function on a file stream, " "removed in Python 3.", ), "W1660": ( "Accessing a removed attribute on the sys module", "deprecated-sys-function", "Used when accessing a field on sys module that has been " "removed in Python 3.", ), "W1661": ( "Using an exception object that was bound by an except handler", "exception-escape", "Emitted when using an exception, that was bound in an except " "handler, outside of the except handler. On Python 3 these " "exceptions will be deleted once they get out " "of the except handler.", ), "W1662": ( "Using a variable that was bound inside a comprehension", "comprehension-escape", "Emitted when using a variable, that was bound in a comprehension " "handler, outside of the comprehension itself. On Python 3 these " "variables will be deleted outside of the " "comprehension.", ), } _bad_builtins = frozenset( [ "apply", "basestring", "buffer", "cmp", "coerce", "execfile", "file", "input", # Not missing, but incompatible semantics "intern", "long", "raw_input", "reduce", "round", # Not missing, but incompatible semantics "StandardError", "unichr", "unicode", "xrange", "reload", ] ) _unused_magic_methods = frozenset( [ "__coerce__", "__delslice__", "__getslice__", "__setslice__", "__oct__", "__hex__", "__nonzero__", "__cmp__", "__div__", "__idiv__", "__rdiv__", ] ) _invalid_encodings = frozenset( [ "base64_codec", "base64", "base_64", "bz2_codec", "bz2", "hex_codec", "hex", "quopri_codec", "quopri", "quotedprintable", "quoted_printable", "uu_codec", "uu", "zlib_codec", "zlib", "zip", "rot13", "rot_13", ] ) _bad_python3_module_map = { "sys-max-int": {"sys": frozenset(["maxint"])}, "deprecated-itertools-function": { "itertools": frozenset( ["izip", "ifilter", "imap", "izip_longest", "ifilterfalse"] ) }, "deprecated-types-field": { "types": frozenset( [ "EllipsisType", "XRangeType", "ComplexType", "StringType", "TypeType", "LongType", "UnicodeType", "ClassType", "BufferType", "StringTypes", "NotImplementedType", "NoneType", "InstanceType", "FloatType", "SliceType", "UnboundMethodType", "ObjectType", "IntType", "TupleType", "ListType", "DictType", "FileType", "DictionaryType", "BooleanType", "DictProxyType", ] ) }, "bad-python3-import": frozenset( [ "anydbm", "BaseHTTPServer", "__builtin__", "CGIHTTPServer", "ConfigParser", "copy_reg", "cPickle", "cStringIO", "Cookie", "cookielib", "dbhash", "dumbdbm", "dumbdb", "Dialog", "DocXMLRPCServer", "FileDialog", "FixTk", "gdbm", "htmlentitydefs", "HTMLParser", "httplib", "markupbase", "Queue", "repr", "robotparser", "ScrolledText", "SimpleDialog", "SimpleHTTPServer", "SimpleXMLRPCServer", "StringIO", "dummy_thread", "SocketServer", "test.test_support", "Tkinter", "Tix", "Tkconstants", "tkColorChooser", "tkCommonDialog", "Tkdnd", "tkFileDialog", "tkFont", "tkMessageBox", "tkSimpleDialog", "UserList", "UserString", "whichdb", "_winreg", "xmlrpclib", "audiodev", "Bastion", "bsddb185", "bsddb3", "Canvas", "cfmfile", "cl", "commands", "compiler", "dircache", "dl", "exception", "fpformat", "htmllib", "ihooks", "imageop", "imputil", "linuxaudiodev", "md5", "mhlib", "mimetools", "MimeWriter", "mimify", "multifile", "mutex", "new", "popen2", "posixfile", "pure", "rexec", "rfc822", "sets", "sha", "sgmllib", "sre", "stringold", "sunaudio", "sv", "test.testall", "thread", "timing", "toaiff", "user", "urllib2", "urlparse", ] ), "deprecated-string-function": { "string": frozenset( [ "maketrans", "atof", "atoi", "atol", "capitalize", "expandtabs", "find", "rfind", "index", "rindex", "count", "lower", "letters", "split", "rsplit", "splitfields", "join", "joinfields", "lstrip", "rstrip", "strip", "swapcase", "translate", "upper", "ljust", "rjust", "center", "zfill", "replace", "lowercase", "letters", "uppercase", "atol_error", "atof_error", "atoi_error", "index_error", ] ) }, "deprecated-operator-function": {"operator": frozenset({"div"})}, "deprecated-urllib-function": { "urllib": frozenset( { "addbase", "addclosehook", "addinfo", "addinfourl", "always_safe", "basejoin", "ftpcache", "ftperrors", "ftpwrapper", "getproxies", "getproxies_environment", "getproxies_macosx_sysconf", "main", "noheaders", "pathname2url", "proxy_bypass", "proxy_bypass_environment", "proxy_bypass_macosx_sysconf", "quote", "quote_plus", "reporthook", "splitattr", "splithost", "splitnport", "splitpasswd", "splitport", "splitquery", "splittag", "splittype", "splituser", "splitvalue", "unquote", "unquote_plus", "unwrap", "url2pathname", "urlcleanup", "urlencode", "urlopen", "urlretrieve", } ) }, "deprecated-sys-function": {"sys": frozenset({"exc_clear"})}, } if (3, 4) <= sys.version_info < (3, 4, 4): # Python 3.4.0 -> 3.4.3 has a bug which breaks `repr_tree()`: # https://bugs.python.org/issue23572 _python_2_tests = frozenset() # type: FrozenSet[str] else: _python_2_tests = frozenset( [ astroid.extract_node(x).repr_tree() for x in [ "sys.version_info[0] == 2", "sys.version_info[0] < 3", "sys.version_info == (2, 7)", "sys.version_info <= (2, 7)", "sys.version_info < (3, 0)", ] ] ) def __init__(self, *args, **kwargs): self._future_division = False self._future_absolute_import = False self._modules_warned_about = set() self._branch_stack = [] super(Python3Checker, self).__init__(*args, **kwargs) # pylint: disable=keyword-arg-before-vararg, arguments-differ def add_message(self, msg_id, always_warn=False, *args, **kwargs): if always_warn or not ( self._branch_stack and self._branch_stack[-1].is_py2_only ): super(Python3Checker, self).add_message(msg_id, *args, **kwargs) def _is_py2_test(self, node): if isinstance(node.test, astroid.Attribute) and isinstance( node.test.expr, astroid.Name ): if node.test.expr.name == "six" and node.test.attrname == "PY2": return True elif ( isinstance(node.test, astroid.Compare) and node.test.repr_tree() in self._python_2_tests ): return True return False def visit_if(self, node): self._branch_stack.append(Branch(node, self._is_py2_test(node))) def leave_if(self, node): assert self._branch_stack.pop().node == node def visit_ifexp(self, node): self._branch_stack.append(Branch(node, self._is_py2_test(node))) def leave_ifexp(self, node): assert self._branch_stack.pop().node == node def visit_module(self, node): # pylint: disable=unused-argument """Clear checker state after previous module.""" self._future_division = False self._future_absolute_import = False def visit_functiondef(self, node): if node.is_method(): if node.name in self._unused_magic_methods: method_name = node.name if node.name.startswith("__"): method_name = node.name[2:-2] self.add_message(method_name + "-method", node=node) elif node.name == "next": # If there is a method named `next` declared, if it is invokable # with zero arguments then it implements the Iterator protocol. # This means if the method is an instance method or a # classmethod 1 argument should cause a failure, if it is a # staticmethod 0 arguments should cause a failure. failing_arg_count = 1 if utils.decorated_with(node, [bases.BUILTINS + ".staticmethod"]): failing_arg_count = 0 if len(node.args.args) == failing_arg_count: self.add_message("next-method-defined", node=node) @utils.check_messages("parameter-unpacking") def visit_arguments(self, node): for arg in node.args: if isinstance(arg, astroid.Tuple): self.add_message("parameter-unpacking", node=arg) @utils.check_messages("comprehension-escape") def visit_listcomp(self, node): names = { generator.target.name for generator in node.generators if isinstance(generator.target, astroid.AssignName) } scope = node.parent.scope() scope_names = scope.nodes_of_class(astroid.Name, skip_klass=astroid.FunctionDef) has_redefined_assign_name = any( assign_name for assign_name in scope.nodes_of_class( astroid.AssignName, skip_klass=astroid.FunctionDef ) if assign_name.name in names and assign_name.lineno > node.lineno ) if has_redefined_assign_name: return emitted_for_names = set() scope_names = list(scope_names) for scope_name in scope_names: if ( scope_name.name not in names or scope_name.lineno <= node.lineno or scope_name.name in emitted_for_names or scope_name.scope() == node ): continue emitted_for_names.add(scope_name.name) self.add_message("comprehension-escape", node=scope_name) def visit_name(self, node): """Detect when a "bad" built-in is referenced.""" found_node, _ = node.lookup(node.name) if not _is_builtin(found_node): return if node.name not in self._bad_builtins: return if node_ignores_exception(node) or isinstance( find_try_except_wrapper_node(node), astroid.ExceptHandler ): return message = node.name.lower() + "-builtin" self.add_message(message, node=node) @utils.check_messages("print-statement") def visit_print(self, node): self.add_message("print-statement", node=node, always_warn=True) def _warn_if_deprecated(self, node, module, attributes, report_on_modules=True): for message, module_map in self._bad_python3_module_map.items(): if module in module_map and module not in self._modules_warned_about: if isinstance(module_map, frozenset): if report_on_modules: self._modules_warned_about.add(module) self.add_message(message, node=node) elif attributes and module_map[module].intersection(attributes): self.add_message(message, node=node) def visit_importfrom(self, node): if node.modname == "__future__": for name, _ in node.names: if name == "division": self._future_division = True elif name == "absolute_import": self._future_absolute_import = True else: if not self._future_absolute_import: if self.linter.is_message_enabled("no-absolute-import"): self.add_message("no-absolute-import", node=node) self._future_absolute_import = True if not _is_conditional_import(node) and not node.level: self._warn_if_deprecated(node, node.modname, {x[0] for x in node.names}) if node.names[0][0] == "*": if self.linter.is_message_enabled("import-star-module-level"): if not isinstance(node.scope(), astroid.Module): self.add_message("import-star-module-level", node=node) def visit_import(self, node): if not self._future_absolute_import: if self.linter.is_message_enabled("no-absolute-import"): self.add_message("no-absolute-import", node=node) self._future_absolute_import = True if not _is_conditional_import(node): for name, _ in node.names: self._warn_if_deprecated(node, name, None) @utils.check_messages("metaclass-assignment") def visit_classdef(self, node): if "__metaclass__" in node.locals: self.add_message("metaclass-assignment", node=node) locals_and_methods = set(node.locals).union(x.name for x in node.mymethods()) if "__eq__" in locals_and_methods and "__hash__" not in locals_and_methods: self.add_message("eq-without-hash", node=node) @utils.check_messages("old-division") def visit_binop(self, node): if not self._future_division and node.op == "/": for arg in (node.left, node.right): if isinstance(arg, astroid.Const) and isinstance(arg.value, float): break else: self.add_message("old-division", node=node) def _check_cmp_argument(self, node): # Check that the `cmp` argument is used kwargs = [] if isinstance(node.func, astroid.Attribute) and node.func.attrname == "sort": inferred = utils.safe_infer(node.func.expr) if not inferred: return builtins_list = "{}.list".format(bases.BUILTINS) if isinstance(inferred, astroid.List) or inferred.qname() == builtins_list: kwargs = node.keywords elif isinstance(node.func, astroid.Name) and node.func.name == "sorted": inferred = utils.safe_infer(node.func) if not inferred: return builtins_sorted = "{}.sorted".format(bases.BUILTINS) if inferred.qname() == builtins_sorted: kwargs = node.keywords for kwarg in kwargs or []: if kwarg.arg == "cmp": self.add_message("using-cmp-argument", node=node) return @staticmethod def _is_constant_string_or_name(node): if isinstance(node, astroid.Const): return isinstance(node.value, str) return isinstance(node, astroid.Name) @staticmethod def _is_none(node): return isinstance(node, astroid.Const) and node.value is None @staticmethod def _has_only_n_positional_args(node, number_of_args): return len(node.args) == number_of_args and all(node.args) and not node.keywords @staticmethod def _could_be_string(inferred_types): confidence = INFERENCE if inferred_types else INFERENCE_FAILURE for inferred_type in inferred_types: if inferred_type is astroid.Uninferable: confidence = INFERENCE_FAILURE elif not ( isinstance(inferred_type, astroid.Const) and isinstance(inferred_type.value, str) ): return None return confidence def visit_call(self, node): self._check_cmp_argument(node) if isinstance(node.func, astroid.Attribute): inferred_types = set() try: for inferred_receiver in node.func.expr.infer(): if inferred_receiver is astroid.Uninferable: continue inferred_types.add(inferred_receiver) if isinstance(inferred_receiver, astroid.Module): self._warn_if_deprecated( node, inferred_receiver.name, {node.func.attrname}, report_on_modules=False, ) if ( _inferred_value_is_dict(inferred_receiver) and node.func.attrname in DICT_METHODS ): if not _in_iterating_context(node): checker = "dict-{}-not-iterating".format(node.func.attrname) self.add_message(checker, node=node) except astroid.InferenceError: pass if node.args: is_str_confidence = self._could_be_string(inferred_types) if is_str_confidence: if ( node.func.attrname in ("encode", "decode") and len(node.args) >= 1 and node.args[0] ): first_arg = node.args[0] self._validate_encoding(first_arg, node) if ( node.func.attrname == "translate" and self._has_only_n_positional_args(node, 2) and self._is_none(node.args[0]) and self._is_constant_string_or_name(node.args[1]) ): # The above statement looking for calls of the form: # # foo.translate(None, 'abc123') # # or # # foo.translate(None, some_variable) # # This check is somewhat broad and _may_ have some false positives, but # after checking several large codebases it did not have any false # positives while finding several real issues. This call pattern seems # rare enough that the trade off is worth it. self.add_message( "deprecated-str-translate-call", node=node, confidence=is_str_confidence, ) return if node.keywords: return if node.func.attrname == "next": self.add_message("next-method-called", node=node) else: if node.func.attrname in ("iterkeys", "itervalues", "iteritems"): self.add_message("dict-iter-method", node=node) elif node.func.attrname in ("viewkeys", "viewvalues", "viewitems"): self.add_message("dict-view-method", node=node) elif isinstance(node.func, astroid.Name): found_node = node.func.lookup(node.func.name)[0] if _is_builtin(found_node): if node.func.name in ("filter", "map", "range", "zip"): if not _in_iterating_context(node): checker = "{}-builtin-not-iterating".format(node.func.name) self.add_message(checker, node=node) if node.func.name == "open" and node.keywords: kwargs = node.keywords for kwarg in kwargs or []: if kwarg.arg == "encoding": self._validate_encoding(kwarg.value, node) break def _validate_encoding(self, encoding, node): if isinstance(encoding, astroid.Const): value = encoding.value if value in self._invalid_encodings: self.add_message("invalid-str-codec", node=node) @utils.check_messages("indexing-exception") def visit_subscript(self, node): """ Look for indexing exceptions. """ try: for inferred in node.value.infer(): if not isinstance(inferred, astroid.Instance): continue if utils.inherit_from_std_ex(inferred): self.add_message("indexing-exception", node=node) except astroid.InferenceError: return def visit_assignattr(self, node): if isinstance(node.assign_type(), astroid.AugAssign): self.visit_attribute(node) def visit_delattr(self, node): self.visit_attribute(node) @utils.check_messages("exception-message-attribute", "xreadlines-attribute") def visit_attribute(self, node): """Look for removed attributes""" if node.attrname == "xreadlines": self.add_message("xreadlines-attribute", node=node) return exception_message = "message" try: for inferred in node.expr.infer(): if isinstance(inferred, astroid.Instance) and utils.inherit_from_std_ex( inferred ): if node.attrname == exception_message: # Exceptions with .message clearly defined are an exception if exception_message in inferred.instance_attrs: continue self.add_message("exception-message-attribute", node=node) if isinstance(inferred, astroid.Module): self._warn_if_deprecated( node, inferred.name, {node.attrname}, report_on_modules=False ) except astroid.InferenceError: return @utils.check_messages("unpacking-in-except", "comprehension-escape") def visit_excepthandler(self, node): """Visit an except handler block and check for exception unpacking.""" def _is_used_in_except_block(node): scope = node.scope() current = node while ( current and current != scope and not isinstance(current, astroid.ExceptHandler) ): current = current.parent return isinstance(current, astroid.ExceptHandler) and current.type != node if isinstance(node.name, (astroid.Tuple, astroid.List)): self.add_message("unpacking-in-except", node=node) return if not node.name: return # Find any names scope = node.parent.scope() scope_names = scope.nodes_of_class(astroid.Name, skip_klass=astroid.FunctionDef) scope_names = list(scope_names) potential_leaked_names = [ scope_name for scope_name in scope_names if scope_name.name == node.name.name and scope_name.lineno > node.lineno and not _is_used_in_except_block(scope_name) ] reassignments_for_same_name = { assign_name.lineno for assign_name in scope.nodes_of_class( astroid.AssignName, skip_klass=astroid.FunctionDef ) if assign_name.name == node.name.name } for leaked_name in potential_leaked_names: if any( node.lineno < elem < leaked_name.lineno for elem in reassignments_for_same_name ): continue self.add_message("exception-escape", node=leaked_name) @utils.check_messages("backtick") def visit_repr(self, node): self.add_message("backtick", node=node) @utils.check_messages("raising-string", "old-raise-syntax") def visit_raise(self, node): """Visit a raise statement and check for raising strings or old-raise-syntax. """ # Ignore empty raise. if node.exc is None: return expr = node.exc if self._check_raise_value(node, expr): return try: value = next(astroid.unpack_infer(expr)) except astroid.InferenceError: return self._check_raise_value(node, value) def _check_raise_value(self, node, expr): if isinstance(expr, astroid.Const): value = expr.value if isinstance(value, str): self.add_message("raising-string", node=node) return True return None class Python3TokenChecker(checkers.BaseTokenChecker): __implements__ = interfaces.ITokenChecker name = "python3" enabled = False msgs = { "E1606": ( "Use of long suffix", "long-suffix", 'Used when "l" or "L" is used to mark a long integer. ' "This will not work in Python 3, since `int` and `long` " "types have merged.", {"maxversion": (3, 0)}, ), "E1607": ( "Use of the <> operator", "old-ne-operator", 'Used when the deprecated "<>" operator is used instead ' 'of "!=". This is removed in Python 3.', {"maxversion": (3, 0), "old_names": [("W0331", "old-ne-operator")]}, ), "E1608": ( "Use of old octal literal", "old-octal-literal", "Used when encountering the old octal syntax, " "removed in Python 3. To use the new syntax, " "prepend 0o on the number.", {"maxversion": (3, 0)}, ), "E1610": ( "Non-ascii bytes literals not supported in 3.x", "non-ascii-bytes-literal", "Used when non-ascii bytes literals are found in a program. " "They are no longer supported in Python 3.", {"maxversion": (3, 0)}, ), } def process_tokens(self, tokens): for idx, (tok_type, token, start, _, _) in enumerate(tokens): if tok_type == tokenize.NUMBER: if token.lower().endswith("l"): # This has a different semantic than lowercase-l-suffix. self.add_message("long-suffix", line=start[0]) elif _is_old_octal(token): self.add_message("old-octal-literal", line=start[0]) if tokens[idx][1] == "<>": self.add_message("old-ne-operator", line=tokens[idx][2][0]) if tok_type == tokenize.STRING and token.startswith("b"): if any(elem for elem in token if ord(elem) > 127): self.add_message("non-ascii-bytes-literal", line=start[0]) def register(linter): linter.register_checker(Python3Checker(linter)) linter.register_checker(Python3TokenChecker(linter))