# -*- coding: utf-8 -*- # Copyright (c) 2014-2020 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-2018 Ville Skyttä # Copyright (c) 2017 Daniel Miller # Copyright (c) 2017 hippo91 # Copyright (c) 2017 ahirnish # Copyright (c) 2018-2020 Anthony Sottile # Copyright (c) 2018 sbagan # Copyright (c) 2018 Lucas Cimon # Copyright (c) 2018 Aivar Annamaa # Copyright (c) 2018 ssolanki # Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com> # Copyright (c) 2018 Ashley Whetter # Copyright (c) 2018 gaurikholkar # Copyright (c) 2019 Nick Drozd # Copyright (c) 2019 Hugues Bruant # Copyright (c) 2019 Gabriel R Sezefredo # Copyright (c) 2019 Hugo van Kemenade # Copyright (c) 2019 Pierre Sassoulas # Copyright (c) 2019 bluesheeptoken # Copyright (c) 2020 谭九鼎 <109224573@qq.com> # Copyright (c) 2020 Federico Bond # Copyright (c) 2020 Athos Ribeiro # 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.""" import itertools import re import tokenize from collections import namedtuple import astroid from astroid import bases from pylint import checkers, interfaces from pylint.checkers import utils from pylint.checkers.utils import find_try_except_wrapper_node, node_ignores_exception from pylint.constants import WarningScope from pylint.interfaces import INFERENCE, INFERENCE_FAILURE _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 _infer_if_relevant_attr(node, whitelist): return node.expr.infer() if node.attrname in whitelist else [] 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): if 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 ["in", "not 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 https://www.python.org/dev/peps/pep-3110/", {"old_names": [("W0712", "old-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-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", "old-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", "old-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", "old-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 a range 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"})}, } _deprecated_attrs = frozenset( itertools.chain.from_iterable( attr for module_map in _bad_python3_module_map.values() if isinstance(module_map, dict) for attr in module_map.values() ) ) _relevant_call_attrs = ( DICT_METHODS | _deprecated_attrs | {"encode", "decode", "translate"} ) _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().__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().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): inferred = utils.safe_infer(arg) # If we can infer the object and that object is not an int, bail out. if inferred and not ( ( isinstance(inferred, astroid.Const) and isinstance(inferred.value, int) ) or ( isinstance(inferred, astroid.Instance) and inferred.name == "int" ) ): 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 _infer_if_relevant_attr( node.func, self._relevant_call_attrs ): 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) elif 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) elif 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 _infer_if_relevant_attr( node, self._deprecated_attrs | {exception_message} ): 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, block): current = node while current and current is not block: current = current.parent return current is not None 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, node) ] 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-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))