Repository: Parquery/pyicontract-lint

Issue:
Error linting file w/o icontract
I'm trying out icontract and this linter. I get a fatal error in [this file](https://github.com/pymor/pymor/blob/contracts_type-hints/src/pymor/algorithms/rules.py)

[pip freeze output](https://github.com/Parquery/pyicontract-lint/files/5429910/freeze.txt)
```
pyicontract-lint ./src/pymor/algorithms/rules.py

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/astroid/decorators.py", line 132, in raise_if_nothing_inferred
    yield next(generator)
StopIteration: {'node': <Attribute.instancemethod l.209 at 0x7f2f85040350>, 'context': <astroid.context.InferenceContext object at 0x7f2f850a8470>}

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/bin/pyicontract-lint", line 8, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/main.py", line 70, in main
    return _main(args=args, stream=sys.stdout)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/main.py", line 49, in _main
    errors = icontract_lint.check_paths(paths=args.paths)
  File "/usr/local/lib/python3.7/site-packages/icontract/_checkers.py", line 396, in wrapper
    result = func(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 576, in check_paths
    errs.extend(check_recursively(path=pth))
  File "/usr/local/lib/python3.7/site-packages/icontract/_checkers.py", line 396, in wrapper
    result = func(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 556, in check_recursively
    errs.extend(check_file(pth))
  File "/usr/local/lib/python3.7/site-packages/icontract/_checkers.py", line 396, in wrapper
    result = func(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 541, in check_file
    lint_visitor.visit(node=tree)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 109, in visit
    return func(node)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 114, in visit_generic
    self.visit(child)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 109, in visit
    return func(node)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 483, in visit_ClassDef
    self.visit(child)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 109, in visit
    return func(node)
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 397, in visit_FunctionDef
    decorators = [self._infer_decorator(node=decorator_node) for decorator_node in node.decorators.nodes]
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 397, in <listcomp>
    decorators = [self._infer_decorator(node=decorator_node) for decorator_node in node.decorators.nodes]
  File "/usr/local/lib/python3.7/site-packages/icontract_lint/__init__.py", line 364, in _infer_decorator
    decorator = next(node.infer())
  File "/usr/local/lib/python3.7/site-packages/astroid/decorators.py", line 137, in raise_if_nothing_inferred
    raise exceptions.InferenceError(**error.args[0])
astroid.exceptions.InferenceError: Inference failed for <Attribute.instancemethod l.209 at 0x7f2f85040350>.
```

It's reproducable on mybinder if you click `new -> terminal` and run `pyicontract-lint /pymor/src/pymor/algorithms/rules.py`
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pymor/pymor/contracts_type-hints)

Discussion:
coveralls: ## Pull Request Test Coverage Report for [Build 102](https://coveralls.io/builds/34419591)

* **8** of **9**   **(88.89%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **91.525%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [icontract_lint/__init__.py](https://coveralls.io/builds/34419591/source?filename=icontract_lint%2F__init__.py#L392) | 8 | 9 | 88.89%
<!-- | **Total:** | **8** | **9** | **88.89%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/34419591/badge)](https://coveralls.io/builds/34419591) |
| :-- | --: |
| Change from base [Build 99](https://coveralls.io/builds/33110514): |  0.0% |
| Covered Lines: | 270 |
| Relevant Lines: | 295 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/icontract_lint/__init__.py b/icontract_lint/__init__.py
index 8639113..9dced7a 100644
--- a/icontract_lint/__init__.py
+++ b/icontract_lint/__init__.py
@@ -351,25 +351,6 @@ class _LintVisitor(_AstroidVisitor):
         else:
             raise NotImplementedError("Unhandled pytype: {}".format(pytype))
 
-    def _infer_decorator(self, node: astroid.nodes.Call) -> Optional[astroid.bases.Instance]:
-        """
-        Try to infer the decorator as instance of a class.
-
-        :param node: decorator AST node
-        :return: instance of the decorator or None if decorator instance could not be inferred
-        """
-        # While this function does not use ``self``, keep it close to the usage to improve the readability.
-        # pylint: disable=no-self-use
-        try:
-            decorator = next(node.infer())
-        except astroid.exceptions.NameInferenceError:
-            return None
-
-        if decorator is astroid.Uninferable:
-            return None
-
-        return decorator
-
     def visit_FunctionDef(self, node: astroid.nodes.FunctionDef) -> None:  # pylint: disable=invalid-name
         """Lint the function definition."""
         if node.decorators is None:
@@ -394,7 +375,25 @@ class _LintVisitor(_AstroidVisitor):
                 pass
 
         # Infer the decorator instances
-        decorators = [self._infer_decorator(node=decorator_node) for decorator_node in node.decorators.nodes]
+
+        def infer_decorator(a_node: astroid.nodes.Call) -> Optional[astroid.bases.Instance]:
+            """
+            Try to infer the decorator as instance of a class.
+
+            :param a_node: decorator AST node
+            :return: instance of the decorator or None if decorator instance could not be inferred
+            """
+            try:
+                decorator = next(a_node.infer())
+            except (astroid.exceptions.NameInferenceError, astroid.exceptions.InferenceError):
+                return None
+
+            if decorator is astroid.Uninferable:
+                return None
+
+            return decorator
+
+        decorators = [infer_decorator(a_node=decorator_node) for decorator_node in node.decorators.nodes]
 
         # Check the decorators individually
         for decorator, decorator_node in zip(decorators, node.decorators.nodes):


Test patch:
diff --git a/tests/test_icontract_lint.py b/tests/test_icontract_lint.py
index be91d62..d09c398 100644
--- a/tests/test_icontract_lint.py
+++ b/tests/test_icontract_lint.py
@@ -82,22 +82,33 @@ class TestCheckUnreadableFile(unittest.TestCase):
             self.assertEqual(str(pth), errors[0].filename)
 
 
-class TestCheckFile(unittest.TestCase):
-    def test_wo_contracts(self):
+class TestUninferrableDecorator(unittest.TestCase):
+    def test_astroid_name_inference_error(self):
         text = textwrap.dedent("""\
+                @some_uninferrable_decorator
                 def some_func(x: int) -> int:
                     pass
-                    
-                class SomeClass:
-                    def some_method(self, x: int) -> int:
-                        pass
-                        
-                    @classmethod
-                    def some_class_method(self, x: int) -> int:
+                """)
+
+        with tempfile.TemporaryDirectory() as tmp:
+            tmp_path = pathlib.Path(tmp)
+            pth = tmp_path / "some_module.py"
+            pth.write_text(text)
+
+            with sys_path_with(tmp_path):
+                errors = icontract_lint.check_file(path=pth)
+                self.assertListEqual([], errors)
+
+    def test_astroid_inferrence_error(self):
+        # This example was adapted from the issue https://github.com/Parquery/pyicontract-lint/issues/27.
+        text = textwrap.dedent("""\
+                class RuleTable:
+                    @classinstancemethod
+                    def insert_rule(cls, index, rule_):
                         pass
-                        
-                    @staticmethod
-                    def some_static_method(self, x: int) -> int:
+        
+                    @insert_rule.instancemethod
+                    def insert_rule(self, index, rule_):
                         pass
                 """)
 
@@ -108,14 +119,26 @@ class TestCheckFile(unittest.TestCase):
 
             with sys_path_with(tmp_path):
                 errors = icontract_lint.check_file(path=pth)
-
                 self.assertListEqual([], errors)
 
-    def test_uninferrable_decorator(self):
+
+class TestCheckFile(unittest.TestCase):
+    def test_wo_contracts(self):
         text = textwrap.dedent("""\
-                @some_uninferrable_decorator
                 def some_func(x: int) -> int:
                     pass
+                    
+                class SomeClass:
+                    def some_method(self, x: int) -> int:
+                        pass
+                        
+                    @classmethod
+                    def some_class_method(self, x: int) -> int:
+                        pass
+                        
+                    @staticmethod
+                    def some_static_method(self, x: int) -> int:
+                        pass
                 """)
 
         with tempfile.TemporaryDirectory() as tmp: