diff --git a/ChangeLog b/ChangeLog index 742543a164..7df985629e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -26,11 +26,18 @@ Release date: TBA Close #4120 +* Don't emit ``no-member`` error if guarded behind if statement. + + Ref #1162 + Closes #1990 + Closes #4168 + * The default for ``PYLINTHOME`` is now the standard ``XDG_CACHE_HOME``, and pylint now uses ``appdirs``. Closes #3878 + What's New in Pylint 2.9.6? =========================== Release date: 2021-07-28 diff --git a/doc/whatsnew/2.10.rst b/doc/whatsnew/2.10.rst index 8980b0f91e..7d7312153f 100644 --- a/doc/whatsnew/2.10.rst +++ b/doc/whatsnew/2.10.rst @@ -24,5 +24,12 @@ Other Changes * Performance of the Similarity checker has been improved. * Added ``time.clock`` to deprecated functions/methods for python 3.3 + * Added ``ignored-parents`` option to the design checker to ignore specific classes from the ``too-many-ancestors`` check (R0901). + +* Don't emit ``no-member`` error if guarded behind if statement. + + Ref #1162 + Closes #1990 + Closes #4168 diff --git a/pylint/checkers/typecheck.py b/pylint/checkers/typecheck.py index eab1a74276..360676b6e5 100644 --- a/pylint/checkers/typecheck.py +++ b/pylint/checkers/typecheck.py @@ -454,6 +454,7 @@ def _emit_no_member(node, owner, owner_name, ignored_mixins=True, ignored_none=T * the owner is a class and the name can be found in its metaclass. * The access node is protected by an except handler, which handles AttributeError, Exception or bare except. + * The node is guarded behind and `IF` or `IFExp` node """ # pylint: disable=too-many-return-statements if node_ignores_exception(node, AttributeError): @@ -522,6 +523,30 @@ def _emit_no_member(node, owner, owner_name, ignored_mixins=True, ignored_none=T # Avoid false positive on Enum.__members__.{items(), values, keys} # See https://github.com/PyCQA/pylint/issues/4123 return False + # Don't emit no-member if guarded behind `IF` or `IFExp` + # * Walk up recursively until if statement is found. + # * Check if condition can be inferred as `Const`, + # would evaluate as `False`, + # and wheater the node is part of the `body`. + # * Continue checking until scope of node is reached. + scope: astroid.NodeNG = node.scope() + node_origin: astroid.NodeNG = node + parent: astroid.NodeNG = node.parent + while parent != scope: + if isinstance(parent, (astroid.If, astroid.IfExp)): + inferred = safe_infer(parent.test) + if ( # pylint: disable=too-many-boolean-expressions + isinstance(inferred, astroid.Const) + and inferred.bool_value() is False + and ( + isinstance(parent, astroid.If) + and node_origin in parent.body + or isinstance(parent, astroid.IfExp) + and node_origin == parent.body + ) + ): + return False + node_origin, parent = parent, parent.parent return True diff --git a/tests/functional/n/no/no_member_if_statements.py b/tests/functional/n/no/no_member_if_statements.py new file mode 100644 index 0000000000..ad87e9950b --- /dev/null +++ b/tests/functional/n/no/no_member_if_statements.py @@ -0,0 +1,79 @@ +# pylint: disable=invalid-name,missing-docstring,pointless-statement +from datetime import datetime +from typing import Union + +value = "Hello World" +value.isoformat() # [no-member] + + +if isinstance(value, datetime): + value.isoformat() +else: + value.isoformat() # [no-member] + + +def func(): + if hasattr(value, "load"): + value.load() + + if getattr(value, "load", None): + value.load + + +if value.none_existent(): # [no-member] + pass + +res = value.isoformat() if isinstance(value, datetime) else value + + +class Base: + _attr_state: Union[str, datetime] = "Unknown" + + @property + def state(self) -> Union[str, datetime]: + return self._attr_state + + def some_function(self) -> str: + state = self.state + if isinstance(state, datetime): + return state.isoformat() + return str(state) + + +# https://github.com/PyCQA/pylint/issues/1990 +# Attribute access after 'isinstance' should not cause 'no-member' error +import subprocess # pylint: disable=wrong-import-position # noqa: E402 + +try: + subprocess.check_call(['ls', '-']) # Deliberately made error in this line +except Exception as err: + if isinstance(err, subprocess.CalledProcessError): + print(f'Subprocess error occured. Return code: {err.returncode}') + else: + print(f'An error occured: {str(err)}') + raise + + +# https://github.com/PyCQA/pylint/issues/4168 +# 'encode' for 'arg' should not cause 'no-member' error +mixed_tuple = (b"a", b"b", b"c", b"d") +byte_tuple = [arg.encode('utf8') if isinstance(arg, str) else arg for arg in mixed_tuple] + +for arg in mixed_tuple: + if isinstance(arg, str): + print(arg.encode('utf8')) + else: + print(arg) + + +# https://github.com/PyCQA/pylint/issues/1162 +# Attribute access after 'isinstance' should not cause 'no-member' error +class FoobarException(Exception): + foobar = None + +try: # noqa: E305 + pass +except Exception as ex: + if isinstance(ex, FoobarException): + ex.foobar + raise diff --git a/tests/functional/n/no/no_member_if_statements.txt b/tests/functional/n/no/no_member_if_statements.txt new file mode 100644 index 0000000000..2bc673b491 --- /dev/null +++ b/tests/functional/n/no/no_member_if_statements.txt @@ -0,0 +1,3 @@ +no-member:6:0::Instance of 'str' has no 'isoformat' member:INFERENCE +no-member:12:4::Instance of 'str' has no 'isoformat' member:INFERENCE +no-member:23:3::Instance of 'str' has no 'none_existent' member:INFERENCE