Repository: quora/pyanalyze
Pull request #339 (2021-12-14 16:14:22)

Move location of param error


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 4b0fabc..93566a5 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## Unreleased
+
+- Correct location of error for incompatible parameter (#339)
+
 ## Version 0.5.0 (December 12, 2021)
 
 - Recognize code following an infinite while loop as unreachable (#337)
@@ -58,6 +62,7 @@
   - `DictIncompleteValue` and `AnnotatedValue` use tuples instead of lists internally.
   - `DictIncompleteValue` now stores a sequence of `KVPair` object instead
     of just key-value pairs, enabling more granular information.
+  - The type of a `TypedValue` may now be a string
 
 ## Version 0.4.0 (November 18, 2021)
 
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index b8a5a39..209d0b5 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -352,7 +352,7 @@ class Signature:
     def _check_param_type_compatibility(
         self,
         param: SigParameter,
-        var_value: Value,
+        composite: Composite,
         visitor: "NameCheckVisitor",
         node: ast.AST,
         typevar_map: Optional[TypeVarMap] = None,
@@ -367,21 +367,22 @@ class Signature:
 
         """
         if param.annotation is not EMPTY and not (
-            isinstance(param.default, Composite) and var_value is param.default.value
+            isinstance(param.default, Composite)
+            and composite.value is param.default.value
         ):
             if typevar_map:
                 param_typ = param.annotation.substitute_typevars(typevar_map)
             else:
                 param_typ = param.annotation
             tv_map, used_any = self._can_assign_and_used_any(
-                param_typ, var_value, visitor
+                param_typ, composite.value, visitor
             )
             if isinstance(tv_map, CanAssignError):
-                if is_overload and isinstance(var_value, MultiValuedValue):
+                if is_overload and isinstance(composite.value, MultiValuedValue):
                     tv_maps = []
                     remaining_values = []
                     union_used_any = False
-                    for val in var_value.vals:
+                    for val in composite.value.vals:
                         can_assign, subval_used_any = self._can_assign_and_used_any(
                             param_typ, val, visitor
                         )
@@ -395,13 +396,13 @@ class Signature:
                         tv_map = unify_typevar_maps(tv_maps)
                         assert remaining_values, (
                             f"all union members matched between {param_typ} and"
-                            f" {var_value}"
+                            f" {composite.value}"
                         )
                         return tv_map, union_used_any, unite_values(*remaining_values)
                 visitor.show_error(
-                    node,
+                    composite.node if composite.node is not None else node,
                     f"Incompatible argument type for {param.name}: expected {param_typ}"
-                    f" but got {var_value}",
+                    f" but got {composite.value}",
                     ErrorCode.incompatible_argument,
                     detail=str(tv_map),
                 )
@@ -730,10 +731,9 @@ class Signature:
             for param_name in self.typevars_of_params:
                 if param_name == self._return_key:
                     continue
-                var_value = variables[param_name]
                 param = self.signature.parameters[param_name]
                 tv_map, _, _ = self._check_param_type_compatibility(
-                    param, var_value, visitor, node
+                    param, bound_args[param_name][1], visitor, node
                 )
                 if tv_map is None:
                     return self.get_default_return()
@@ -763,7 +763,7 @@ class Signature:
                 remaining_value,
             ) = self._check_param_type_compatibility(
                 param,
-                composite.value,
+                composite,
                 visitor,
                 node,
                 typevar_values,


Test patch:
diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py
index d5506a7..47bca8e 100644
--- a/pyanalyze/test_signature.py
+++ b/pyanalyze/test_signature.py
@@ -377,6 +377,18 @@ class TestShadowing(TestNameCheckVisitorBase):
 
 
 class TestCalls(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_error_location(self):
+        def two_args(x: str, y: int) -> None:
+            pass
+
+        def capybara():
+            two_args(
+                "one very long string so long that it goes on its own line is this"
+                " enough",
+                "not an int",  # E: incompatible_argument
+            )
+
     @assert_passes()
     def test_too_few_args(self):
         def fn(x, y):