Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Omit no-member error if guarded behind if stmt #4764

Merged
merged 5 commits into from
Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions doc/whatsnew/2.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's pretty clever.

# 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

Expand Down
79 changes: 79 additions & 0 deletions tests/functional/n/no/no_member_if_statements.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/functional/n/no/no_member_if_statements.txt
Original file line number Diff line number Diff line change
@@ -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