Repository: quora/pyanalyze (python)

Issue:
Catch calling Any()
I found some code that did `Any(...)` instead of `cast(Any, ...)`. Pyanalyze didn't catch it because `Any.__call__` does exist, it just always throws an error. Let's add a special case to show a type checker error for this.

Pull request:
Error for call to Any
Fixes #686


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 5ec6a91..9f2b7bd 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,13 +2,14 @@
 
 ## Unreleased
 
+- Show an error on calls to `typing.Any` (#688)
 - Add command-line option `-c`/`--code` to typecheck code from
   the command line (#685)
 - Add a `pyanalyze.extensions.EnumName` predicate and infer it
   as the value for the `.name` attribute on enums. Also fix
   type inference for enum "properties" on Python 3.11 and up. (#682)
 - Allow `pyanalyze.runtime.is_compatible` to be used to narrow
-  types (#681)
+  types (#681, #687)
 - Fix usage of `assert_type()` with `Any` and with unions of
   `Annotated` objects (#680)
 - Support inferring `MinLen` and `MaxLen` annotations based
diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py
index ba3714a..57fcdbd 100644
--- a/pyanalyze/implementation.py
+++ b/pyanalyze/implementation.py
@@ -1426,6 +1426,12 @@ def _bool_impl(ctx: CallContext) -> Value:
     return annotate_with_constraint(TypedValue(bool), constraint)
 
 
+# Any has a __call__ method at runtime that always raises.
+def _any_impl(ctx: CallContext) -> Value:
+    ctx.show_error("Any is not callable. Maybe you meant cast(Any, ...)?")
+    return AnyValue(AnySource.error)
+
+
 _POS_ONLY = ParameterKind.POSITIONAL_ONLY
 _ENCODING_PARAMETER = SigParameter(
     "encoding", annotation=TypedValue(str), default=KnownValue("")
@@ -1455,7 +1461,7 @@ def get_default_argspecs() -> Dict[object, Signature]:
             callable=assert_is_value,
         ),
         Signature.make(
-            [SigParameter("typ"), SigParameter("value")],
+            [SigParameter("value"), SigParameter("typ")],
             return_annotation=TypedValue(bool),
             impl=_is_compatible_impl,
             callable=is_compatible,
@@ -1888,6 +1894,15 @@ def get_default_argspecs() -> Dict[object, Signature]:
             impl=_bool_impl,
             return_annotation=TypedValue(bool),
         ),
+        Signature.make(
+            [
+                SigParameter("args", ParameterKind.VAR_POSITIONAL),
+                SigParameter("kwargs", ParameterKind.VAR_KEYWORD),
+            ],
+            callable=typing.Any,
+            impl=_any_impl,
+            return_annotation=AnyValue(AnySource.error),
+        ),
         # Typeshed has it as TypeGuard[Callable[..., object]], which causes some
         # false positives.
         Signature.make(
diff --git a/pyanalyze/runtime.py b/pyanalyze/runtime.py
index 04f193b..6f1ec71 100644
--- a/pyanalyze/runtime.py
+++ b/pyanalyze/runtime.py
@@ -19,16 +19,16 @@ def _get_checker() -> "pyanalyze.checker.Checker":
 
 
 @used
-def is_compatible(typ: object, value: object) -> bool:
+def is_compatible(value: object, typ: object) -> bool:
     """Return whether ``value`` is compatible with ``type``.
 
     Examples::
 
-        >>> is_compatible(list[int], 42)
+        >>> is_compatible(42, list[int])
         False
-        >>> is_compatible(list[int], [])
+        >>> is_compatible([], list[int])
         True
-        >>> is_compatible(list[int], ["x"])
+        >>> is_compatible(["x"], list[int])
         False
 
     """
@@ -38,18 +38,18 @@ def is_compatible(typ: object, value: object) -> bool:
 
 
 @used
-def get_compatibility_error(typ: object, value: object) -> Optional[str]:
+def get_compatibility_error(value: object, typ: object) -> Optional[str]:
     """Return an error message explaining why ``value`` is not
     compatible with ``type``, or None if they are compatible.
 
     Examples::
 
-        >>> print(get_compatibility_error(list[int], 42))
+        >>> print(get_compatibility_error(42, list[int]))
         Cannot assign Literal[42] to list
 
-        >>> print(get_compatibility_error(list[int], []))
+        >>> print(get_compatibility_error([], list[int]))
         None
-        >>> print(get_compatibility_error(list[int], ["x"]))
+        >>> print(get_compatibility_error(["x"], list[int]))
         In element 0
           Cannot assign Literal['x'] to int
 


Test patch:
diff --git a/pyanalyze/test_implementation.py b/pyanalyze/test_implementation.py
index 8de90a7..24e4e13 100644
--- a/pyanalyze/test_implementation.py
+++ b/pyanalyze/test_implementation.py
@@ -1345,3 +1345,12 @@ class TestAssertType(TestNameCheckVisitorBase):
             assert_type(explicit_any, Any)
 
             assert_type(x, int)  # E: inference_failure
+
+
+class TestAny(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_call(self):
+        from typing import Any
+
+        def capybara():
+            Any(42)  # E: incompatible_call
diff --git a/pyanalyze/test_runtime.py b/pyanalyze/test_runtime.py
index c9ca939..92c661e 100644
--- a/pyanalyze/test_runtime.py
+++ b/pyanalyze/test_runtime.py
@@ -7,22 +7,22 @@ from .test_node_visitor import assert_passes
 
 
 def test_is_compatible() -> None:
-    assert not is_compatible(List[int], 42)
-    assert is_compatible(List[int], [])
-    assert not is_compatible(List[int], ["x"])
-    assert is_compatible(List[int], [1, 2, 3])
+    assert not is_compatible(42, List[int])
+    assert is_compatible([], List[int])
+    assert not is_compatible(["x"], List[int])
+    assert is_compatible([1, 2, 3], List[int])
 
 
 def test_get_compatibility_error() -> None:
     assert (
-        get_compatibility_error(List[int], 42) == "Cannot assign Literal[42] to list\n"
+        get_compatibility_error(42, List[int]) == "Cannot assign Literal[42] to list\n"
     )
-    assert get_compatibility_error(List[int], []) is None
+    assert get_compatibility_error([], List[int]) is None
     assert (
-        get_compatibility_error(List[int], ["x"])
+        get_compatibility_error(["x"], List[int])
         == "In element 0\n  Cannot assign Literal['x'] to int\n"
     )
-    assert get_compatibility_error(List[int], [1, 2, 3]) is None
+    assert get_compatibility_error([1, 2, 3], List[int]) is None
 
 
 class TestRuntimeTypeGuard(TestNameCheckVisitorBase):
@@ -39,9 +39,9 @@ class TestRuntimeTypeGuard(TestNameCheckVisitorBase):
 
         def capybara(s: str) -> None:
             want_lowercase(s)  # E: incompatible_argument
-            if is_compatible(IsLower, s):
+            if is_compatible(s, IsLower):
                 want_lowercase(s)
 
         def asserting_capybara(s: str) -> None:
-            assert is_compatible(IsLower, s)
+            assert is_compatible(s, IsLower)
             want_lowercase(s)