Repository: quora/pyanalyze
Pull request #370 (2021-12-26 12:15:42)

NoReturnGuard


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index a0de561..d8f4121 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Add `pyanalyze.extensions.NoReturnGuard` (#370)
 - Infer call signatures for `Type[X]` (#369)
 - Support configuration in a `pyproject.toml` file (#368)
 - Require `typeshed_client` 2.0 (#361)
diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py
index d6e53d7..4325e4e 100644
--- a/pyanalyze/annotations.py
+++ b/pyanalyze/annotations.py
@@ -54,6 +54,7 @@ from .extensions import (
     CustomCheck,
     ExternalType,
     HasAttrGuard,
+    NoReturnGuard,
     ParameterTypeGuard,
     TypeGuard,
 )
@@ -71,6 +72,7 @@ from .value import (
     KnownValue,
     MultiValuedValue,
     NO_RETURN_VALUE,
+    NoReturnGuardExtension,
     ParameterTypeGuardExtension,
     TypeGuardExtension,
     TypedValue,
@@ -1068,6 +1070,10 @@ def _value_from_metadata(entry: Value, ctx: Context) -> Union[Value, Extension]:
             return ParameterTypeGuardExtension(
                 entry.val.varname, _type_from_runtime(entry.val.guarded_type, ctx)
             )
+        elif isinstance(entry.val, NoReturnGuard):
+            return NoReturnGuardExtension(
+                entry.val.varname, _type_from_runtime(entry.val.guarded_type, ctx)
+            )
         elif isinstance(entry.val, HasAttrGuard):
             return HasAttrGuardExtension(
                 entry.val.varname,
diff --git a/pyanalyze/extensions.py b/pyanalyze/extensions.py
index 0f52f13..fad52dd 100644
--- a/pyanalyze/extensions.py
+++ b/pyanalyze/extensions.py
@@ -205,20 +205,20 @@ class AsynqCallable(metaclass=_AsynqCallableMeta):
         raise TypeError(f"{self} is not callable")
 
 
-class _ParameterTypeGuardMeta(type):
-    def __getitem__(self, params: Tuple[str, object]) -> "ParameterTypeGuard":
+class _ParameterGuardMeta(type):
+    def __getitem__(self, params: Tuple[str, object]) -> Any:
         if not isinstance(params, tuple) or len(params) != 2:
             raise TypeError(
-                "ParameterTypeGuard[...] should be instantiated "
+                f"{self.__name__}[...] should be instantiated "
                 "with two arguments (a variable name and a type)."
             )
         if not isinstance(params[0], str):
-            raise TypeError("The first argument to ParameterTypeGuard must be a string")
-        return ParameterTypeGuard(params[0], params[1])
+            raise TypeError(f"The first argument to {self.__name__} must be a string")
+        return self(params[0], params[1])
 
 
 @dataclass(frozen=True)
-class ParameterTypeGuard(metaclass=_ParameterTypeGuardMeta):
+class ParameterTypeGuard(metaclass=_ParameterGuardMeta):
     """A guard on an arbitrary parameter. Used with ``Annotated``.
 
     Example usage::
@@ -232,6 +232,23 @@ class ParameterTypeGuard(metaclass=_ParameterTypeGuardMeta):
     guarded_type: object
 
 
+@dataclass(frozen=True)
+class NoReturnGuard(metaclass=_ParameterGuardMeta):
+    """A no-return guard on an arbitrary parameter. Used with ``Annotated``.
+
+    If the function returns, then the condition is true.
+
+    Example usage::
+
+        def assert_is_int(arg: object) -> Annotated[bool, NoReturnGuard["arg", int]]:
+            assert isinstance(arg, int)
+
+    """
+
+    varname: str
+    guarded_type: object
+
+
 class _HasAttrGuardMeta(type):
     def __getitem__(self, params: Tuple[str, str, object]) -> "HasAttrGuard":
         if not isinstance(params, tuple) or len(params) != 3:
diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py
index e171ead..e30cd75 100644
--- a/pyanalyze/implementation.py
+++ b/pyanalyze/implementation.py
@@ -894,7 +894,7 @@ def _set_add_impl(ctx: CallContext) -> ImplReturn:
 
 def _remove_annotated(val: Value) -> Value:
     if isinstance(val, AnnotatedValue):
-        return val.value
+        return _remove_annotated(val.value)
     elif isinstance(val, MultiValuedValue):
         return unite_values(*[_remove_annotated(subval) for subval in val.vals])
     return val
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index 72f539e..b9adb7e 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -115,7 +115,6 @@ from .signature import (
     ANY_SIGNATURE,
     BoundMethodSignature,
     ConcreteSignature,
-    ImplReturn,
     MaybeSignature,
     OverloadedSignature,
     Signature,
@@ -145,6 +144,7 @@ from .value import (
     KnownValueWithTypeVars,
     UNINITIALIZED_VALUE,
     NO_RETURN_VALUE,
+    NoReturnConstraintExtension,
     kv_pairs_from_mapping,
     make_weak,
     unannotate_value,
@@ -1557,6 +1557,10 @@ class NameCheckVisitor(
             and not info.is_overload
             and expected_return_value is not None
             and expected_return_value != KnownNone
+            and not (
+                isinstance(expected_return_value, AnnotatedValue)
+                and expected_return_value.value == KnownNone
+            )
             and not any(
                 decorator == KnownValue(abstractmethod)
                 for _, decorator in info.decorators
@@ -4373,7 +4377,7 @@ class NameCheckVisitor(
         if extended_argspec is ANY_SIGNATURE:
             # don't bother calling it
             extended_argspec = None
-            impl_ret = ImplReturn(AnyValue(AnySource.from_another))
+            return_value = AnyValue(AnySource.from_another)
 
         elif extended_argspec is None:
             self._show_error_if_checking(
@@ -4381,7 +4385,7 @@ class NameCheckVisitor(
                 f"{callee_wrapped} is not callable",
                 error_code=ErrorCode.not_callable,
             )
-            impl_ret = ImplReturn(AnyValue(AnySource.error))
+            return_value = AnyValue(AnySource.error)
 
         else:
             arguments = [
@@ -4394,16 +4398,16 @@ class NameCheckVisitor(
                 for keyword, value in keywords
             ]
             if self._is_checking():
-                impl_ret = extended_argspec.check_call(arguments, self, node)
+                return_value = extended_argspec.check_call(arguments, self, node)
             else:
                 with self.catch_errors():
-                    impl_ret = extended_argspec.check_call(arguments, self, node)
-
-        return_value = impl_ret.return_value
-        constraint = impl_ret.constraint
+                    return_value = extended_argspec.check_call(arguments, self, node)
 
-        if impl_ret.no_return_unless is not NULL_CONSTRAINT:
-            self.add_constraint(node, impl_ret.no_return_unless)
+        return_value, nru_extensions = unannotate_value(
+            return_value, NoReturnConstraintExtension
+        )
+        for extension in nru_extensions:
+            self.add_constraint(node, extension.constraint)
 
         if (
             extended_argspec is not None
@@ -4441,22 +4445,16 @@ class NameCheckVisitor(
             callee_wrapped.val
         ):
             async_fn = callee_wrapped.val.__self__
-            return AsyncTaskIncompleteValue(
-                _get_task_cls(async_fn),
-                annotate_with_constraint(return_value, constraint),
-            )
+            return AsyncTaskIncompleteValue(_get_task_cls(async_fn), return_value)
         elif isinstance(
             callee_wrapped, UnboundMethodValue
         ) and callee_wrapped.secondary_attr_name in ("async", "asynq"):
             async_fn = callee_wrapped.get_method()
-            return AsyncTaskIncompleteValue(
-                _get_task_cls(async_fn),
-                annotate_with_constraint(return_value, constraint),
-            )
+            return AsyncTaskIncompleteValue(_get_task_cls(async_fn), return_value)
         elif isinstance(callee_wrapped, UnboundMethodValue) and asynq.is_pure_async_fn(
             callee_wrapped.get_method()
         ):
-            return annotate_with_constraint(return_value, constraint)
+            return return_value
         else:
             if (
                 isinstance(return_value, AnyValue)
@@ -4466,7 +4464,7 @@ class NameCheckVisitor(
                 task_cls = _get_task_cls(callee_wrapped.val)
                 if isinstance(task_cls, type):
                     return TypedValue(task_cls)
-            return annotate_with_constraint(return_value, constraint)
+            return return_value
 
     def signature_from_value(
         self, value: Value, node: Optional[ast.AST] = None
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index 7295b6b..33b540f 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -25,12 +25,15 @@ from .value import (
     AsyncTaskIncompleteValue,
     CallableValue,
     CanAssignContext,
+    ConstraintExtension,
     GenericValue,
     HasAttrExtension,
     HasAttrGuardExtension,
     KVPair,
     KnownValue,
     MultiValuedValue,
+    NoReturnConstraintExtension,
+    NoReturnGuardExtension,
     ParameterTypeGuardExtension,
     SequenceIncompleteValue,
     DictIncompleteValue,
@@ -42,11 +45,13 @@ from .value import (
     TypeVarMap,
     CanAssign,
     CanAssignError,
+    annotate_value,
     concrete_values_from_iterable,
     extract_typevars,
     flatten_values,
     replace_known_sequence_value,
     stringify_object,
+    unannotate_value,
     unify_typevar_maps,
     unite_values,
     NO_RETURN_VALUE,
@@ -128,6 +133,23 @@ class ActualArguments:
     kwargs_required: bool
 
 
+class CallReturn(NamedTuple):
+    """Return value of a preprocessed call.
+
+    This returns data that is useful for overload resolution.
+
+    """
+
+    return_value: Value
+    """The return value of the function."""
+    is_error: bool = False
+    """Whether there was an error in this call. Used only for overload resolutioon."""
+    used_any_for_match: bool = False
+    """Whether Any was used for this match. Used only for overload resolution."""
+    remaining_arguments: Optional[ActualArguments] = None
+    """Arguments that still need to be processed. Used only for overload resolution."""
+
+
 class ImplReturn(NamedTuple):
     """Return value of :term:`impl` functions.
 
@@ -144,12 +166,6 @@ class ImplReturn(NamedTuple):
     no_return_unless: AbstractConstraint = NULL_CONSTRAINT
     """A :class:`pyanalyze.stacked_scopes.Constraint` indicating things that are true
     unless the function does not return."""
-    is_error: bool = False
-    """Whether there was an error in this call. Used only for overload resolutioon."""
-    used_any_for_match: bool = False
-    """Whether Any was used for this match. Used only for overload resolution."""
-    remaining_arguments: Optional[ActualArguments] = None
-    """Arguments that still need to be processed. Used only for overload resolution."""
 
     @classmethod
     def unite_impl_rets(cls, rets: Sequence["ImplReturn"]) -> "ImplReturn":
@@ -437,16 +453,19 @@ class Signature:
 
     def _apply_annotated_constraints(
         self, raw_return: Union[Value, ImplReturn], composites: Dict[str, Composite]
-    ) -> ImplReturn:
+    ) -> Value:
         if isinstance(raw_return, Value):
             ret = ImplReturn(raw_return)
         else:
             ret = raw_return
         constraints = [ret.constraint]
-        if isinstance(ret.return_value, AnnotatedValue):
-            for guard in ret.return_value.get_metadata_of_type(
-                ParameterTypeGuardExtension
-            ):
+        return_value = ret.return_value
+        no_return_unless = ret.no_return_unless
+        if isinstance(return_value, AnnotatedValue):
+            return_value, ptg = unannotate_value(
+                return_value, ParameterTypeGuardExtension
+            )
+            for guard in ptg:
                 if guard.varname in composites:
                     composite = composites[guard.varname]
                     if composite.varname is not None:
@@ -457,7 +476,8 @@ class Signature:
                             guard.guarded_type,
                         )
                         constraints.append(constraint)
-            for guard in ret.return_value.get_metadata_of_type(TypeGuardExtension):
+            return_value, tg = unannotate_value(return_value, TypeGuardExtension)
+            for guard in tg:
                 # This might miss some cases where we should use the second argument instead. We'll
                 # have to come up with additional heuristics if that comes up.
                 if isinstance(self.callable, MethodType) or (
@@ -478,7 +498,8 @@ class Signature:
                             guard.guarded_type,
                         )
                         constraints.append(constraint)
-            for guard in ret.return_value.get_metadata_of_type(HasAttrGuardExtension):
+            return_value, hag = unannotate_value(return_value, HasAttrGuardExtension)
+            for guard in hag:
                 if guard.varname in composites:
                     composite = composites[guard.varname]
                     if composite.varname is not None:
@@ -491,8 +512,30 @@ class Signature:
                             ),
                         )
                         constraints.append(constraint)
+
+            return_value, nrg = unannotate_value(return_value, NoReturnGuardExtension)
+            extra_nru = []
+            for guard in nrg:
+                if guard.varname in composites:
+                    composite = composites[guard.varname]
+                    if composite.varname is not None:
+                        constraint = Constraint(
+                            composite.varname,
+                            ConstraintType.is_value_object,
+                            True,
+                            guard.guarded_type,
+                        )
+                        extra_nru.append(constraint)
+            if extra_nru:
+                no_return_unless = AndConstraint.make([no_return_unless, *extra_nru])
+
         constraint = AndConstraint.make(constraints)
-        return ImplReturn(ret.return_value, constraint, ret.no_return_unless)
+        extensions = []
+        if constraint is not NULL_CONSTRAINT:
+            extensions.append(ConstraintExtension(constraint))
+        if no_return_unless is not NULL_CONSTRAINT:
+            extensions.append(NoReturnConstraintExtension(no_return_unless))
+        return annotate_value(return_value, extensions)
 
     def bind_arguments(
         self, actual_args: ActualArguments, visitor: "NameCheckVisitor", node: ast.AST
@@ -687,16 +730,16 @@ class Signature:
             message = f"In call to {stringify_object(self.callable)}: {message}"
         visitor.show_error(node, message, ErrorCode.incompatible_call, detail=detail)
 
-    def get_default_return(self, source: AnySource = AnySource.error) -> ImplReturn:
+    def get_default_return(self, source: AnySource = AnySource.error) -> CallReturn:
         return_value = self.return_value
         if self._return_key in self.typevars_of_params:
             typevar_values = {tv: AnyValue(source) for tv in self.all_typevars}
             return_value = return_value.substitute_typevars(typevar_values)
-        return ImplReturn(return_value, is_error=True)
+        return CallReturn(return_value, is_error=True)
 
     def check_call(
         self, args: Iterable[Argument], visitor: "NameCheckVisitor", node: ast.AST
-    ) -> ImplReturn:
+    ) -> Value:
         """Type check a call to this Signature with the given arguments.
 
         This may call the :term:`impl` function or the underlying callable,
@@ -706,8 +749,8 @@ class Signature:
         args = list(args)
         preprocessed = preprocess_args(args, visitor, node)
         if preprocessed is None:
-            return self.get_default_return()
-        return self.check_call_preprocessed(preprocessed, visitor, node)
+            return self.get_default_return().return_value
+        return self.check_call_preprocessed(preprocessed, visitor, node).return_value
 
     def check_call_preprocessed(
         self,
@@ -715,13 +758,13 @@ class Signature:
         visitor: "NameCheckVisitor",
         node: ast.AST,
         is_overload: bool = False,
-    ) -> ImplReturn:
+    ) -> CallReturn:
         if self.is_ellipsis_args:
             if self.allow_call:
                 runtime_return = self._maybe_perform_call(preprocessed, visitor, node)
                 if runtime_return is not None:
-                    return ImplReturn(runtime_return)
-            return ImplReturn(self.return_value, used_any_for_match=True)
+                    return CallReturn(runtime_return)
+            return CallReturn(self.return_value, used_any_for_match=True)
 
         bound_args = self.bind_arguments(preprocessed, visitor, node)
         if bound_args is None:
@@ -818,7 +861,8 @@ class Signature:
                 else:
                     return_value = runtime_return
         ret = self._apply_annotated_constraints(return_value, composites)
-        return ret._replace(
+        return CallReturn(
+            ret,
             is_error=had_error,
             used_any_for_match=used_any,
             remaining_arguments=new_args,
@@ -1553,7 +1597,7 @@ class OverloadedSignature:
 
     def check_call(
         self, args: Iterable[Argument], visitor: "NameCheckVisitor", node: ast.AST
-    ) -> ImplReturn:
+    ) -> Value:
         """Check a call to an overloaded function.
 
         The way overloads are handled is not well specified in any PEPs. Useful resources
@@ -1602,12 +1646,12 @@ class OverloadedSignature:
         """
         actual_args = preprocess_args(args, visitor, node)
         if actual_args is None:
-            return ImplReturn(AnyValue(AnySource.error))
+            return AnyValue(AnySource.error)
 
         errors_per_overload = []
-        any_rets: List[ImplReturn] = []
-        union_rets: List[ImplReturn] = []
-        union_and_any_rets: List[ImplReturn] = []
+        any_rets: List[CallReturn] = []
+        union_rets: List[CallReturn] = []
+        union_and_any_rets: List[CallReturn] = []
         last = len(self.signatures) - 1
         for i, sig in enumerate(self.signatures):
             with visitor.catch_errors() as caught_errors:
@@ -1658,15 +1702,15 @@ class OverloadedSignature:
         visitor.show_error(
             node, "Cannot call overloaded function", error_code, detail=str(detail)
         )
-        return ImplReturn(AnyValue(AnySource.error))
+        return AnyValue(AnySource.error)
 
     def _unite_rets(
         self,
-        any_rets: Sequence[ImplReturn],
-        union_and_any_rets: Sequence[ImplReturn],
-        union_rets: Sequence[ImplReturn],
-        clean_ret: Optional[ImplReturn] = None,
-    ) -> ImplReturn:
+        any_rets: Sequence[CallReturn],
+        union_and_any_rets: Sequence[CallReturn],
+        union_rets: Sequence[CallReturn],
+        clean_ret: Optional[CallReturn] = None,
+    ) -> Value:
         if any_rets or union_and_any_rets:
             if (
                 len(any_rets) == 1
@@ -1674,14 +1718,16 @@ class OverloadedSignature:
                 and not union_and_any_rets
                 and clean_ret is None
             ):
-                return any_rets[0]
-            return ImplReturn(AnyValue(AnySource.multiple_overload_matches))
+                return any_rets[0].return_value
+            return AnyValue(AnySource.multiple_overload_matches)
         elif union_rets:
             if clean_ret is not None:
-                return ImplReturn.unite_impl_rets([*union_rets, clean_ret])
-            return ImplReturn.unite_impl_rets(union_rets)
+                rets = [*union_rets, clean_ret]
+            else:
+                rets = union_rets
+            return unite_values(*[r.return_value for r in rets])
         assert clean_ret is not None
-        return clean_ret
+        return clean_ret.return_value
 
     def _make_detail(
         self, errors_per_overload: Sequence[Sequence[Dict[str, Any]]]
@@ -1761,14 +1807,14 @@ class BoundMethodSignature:
 
     def check_call(
         self, args: Iterable[Argument], visitor: "NameCheckVisitor", node: ast.AST
-    ) -> ImplReturn:
+    ) -> Value:
         ret = self.signature.check_call(
             [(self.self_composite, None), *args], visitor, node
         )
         if self.return_override is not None and not self.signature.has_return_value():
-            return ImplReturn(
-                self.return_override, ret.constraint, ret.no_return_unless
-            )
+            if isinstance(ret, AnnotatedValue):
+                return annotate_value(self.return_override, ret.metadata)
+            return self.return_override
         return ret
 
     def get_signature(
@@ -1808,7 +1854,7 @@ class PropertyArgSpec:
 
     def check_call(
         self, args: Iterable[Argument], visitor: "NameCheckVisitor", node: ast.AST
-    ) -> ImplReturn:
+    ) -> Value:
         raise TypeError("property object is not callable")
 
     def has_return_value(self) -> bool:
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index 4b1536d..21c4995 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -1587,6 +1587,27 @@ class ParameterTypeGuardExtension(Extension):
         yield from self.guarded_type.walk_values()
 
 
+@dataclass(frozen=True)
+class NoReturnGuardExtension(Extension):
+    """An :class:`Extension` used in a function return type. Used to
+    indicate that unless the parameter named `varname` is of type `guarded_type`,
+    the function does not return.
+
+    Corresponds to :class:`pyanalyze.extensions.NoReturnGuard`.
+
+    """
+
+    varname: str
+    guarded_type: Value
+
+    def substitute_typevars(self, typevars: TypeVarMap) -> Extension:
+        guarded_type = self.guarded_type.substitute_typevars(typevars)
+        return NoReturnGuardExtension(self.varname, guarded_type)
+
+    def walk_values(self) -> Iterable[Value]:
+        yield from self.guarded_type.walk_values()
+
+
 @dataclass(frozen=True)
 class TypeGuardExtension(Extension):
     """An :class:`Extension` used in a function return type. Used to
@@ -1675,6 +1696,18 @@ class ConstraintExtension(Extension):
         return id(self)
 
 
+@dataclass(frozen=True, eq=False)
+class NoReturnConstraintExtension(Extension):
+    """Encapsulates a Constraint. If the value is evaluated and completes, the
+    constraint must be True."""
+
+    constraint: "pyanalyze.stacked_scopes.AbstractConstraint"
+
+    # Comparing them can get too expensive
+    def __hash__(self) -> int:
+        return id(self)
+
+
 @dataclass(frozen=True)
 class WeakExtension(Extension):
     """Used to indicate that a generic argument to a container may be widened.
@@ -1843,7 +1876,13 @@ def flatten_values(val: Value, *, unwrap_annotated: bool = False) -> Iterable[Va
     if isinstance(val, MultiValuedValue):
         yield from val.vals
     elif isinstance(val, AnnotatedValue) and isinstance(val.value, MultiValuedValue):
-        yield from val.value.vals
+        if unwrap_annotated:
+            yield from val.value.vals
+        else:
+            subvals = [
+                annotate_value(subval, val.metadata) for subval in val.value.vals
+            ]
+            yield from subvals
     elif unwrap_annotated and isinstance(val, AnnotatedValue):
         yield val.value
     else:
@@ -1865,10 +1904,6 @@ def make_weak(val: Value) -> Value:
 def annotate_value(origin: Value, metadata: Sequence[Union[Value, Extension]]) -> Value:
     if not metadata:
         return origin
-    if isinstance(origin, MultiValuedValue):
-        return MultiValuedValue(
-            [annotate_value(subval, metadata) for subval in origin.vals]
-        )
     if isinstance(origin, AnnotatedValue):
         # Flatten it
         metadata = (*origin.metadata, *metadata)
@@ -1888,9 +1923,12 @@ def annotate_value(origin: Value, metadata: Sequence[Union[Value, Extension]]) -
     return AnnotatedValue(origin, metadata)
 
 
+ExtensionT = TypeVar("ExtensionT", bound=Extension)
+
+
 def unannotate_value(
-    origin: Value, extension: Type[Extension]
-) -> Tuple[Value, Sequence[Extension]]:
+    origin: Value, extension: Type[ExtensionT]
+) -> Tuple[Value, Sequence[ExtensionT]]:
     if not isinstance(origin, AnnotatedValue):
         return origin, []
     matches = [


Test patch:
diff --git a/pyanalyze/test_annotations.py b/pyanalyze/test_annotations.py
index 574b724..5c387d8 100644
--- a/pyanalyze/test_annotations.py
+++ b/pyanalyze/test_annotations.py
@@ -1062,6 +1062,20 @@ class TestParameterTypeGuard(TestNameCheckVisitorBase):
                 assert_is_value(union, TypedValue(B))
 
 
+class TestNoReturnGuard(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_basic(self):
+        from pyanalyze.extensions import NoReturnGuard
+        from typing_extensions import Annotated
+
+        def assert_is_int(x: object) -> Annotated[None, NoReturnGuard["x", int]]:
+            assert isinstance(x, int)
+
+        def capybara(x):
+            assert_is_int(x)
+            assert_is_value(x, TypedValue(int))
+
+
 class TestTypeGuard(TestNameCheckVisitorBase):
     @assert_passes()
     def test_typing_extesions(self):
diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py
index 3e2fa28..c33c639 100644
--- a/pyanalyze/test_signature.py
+++ b/pyanalyze/test_signature.py
@@ -496,26 +496,6 @@ class TestCalls(TestNameCheckVisitorBase):
             capybara = int("3")
             assert_is_value(capybara, TypedValue(int))
 
-    @assert_passes()
-    def test_return_value(self):
-        from pyanalyze.value import HasAttrGuardExtension
-        from qcore.testing import Anything
-        from typing import Any, cast
-
-        val = AnnotatedValue(
-            TypedValue(bool),
-            [
-                HasAttrGuardExtension(
-                    "object", KnownValue("foo"), AnyValue(AnySource.inference)
-                ),
-                cast(Any, Anything),
-            ],
-        )
-
-        def capybara(x):
-            l = hasattr(x, "foo")
-            assert_is_value(l, val)
-
     @assert_passes()
     def test_required_kwonly_args(self):
         from pyanalyze.tests import takes_kwonly_argument
@@ -602,30 +582,29 @@ class TestCalls(TestNameCheckVisitorBase):
 
     @assert_passes()
     def test_hasattr(self):
-        from pyanalyze.value import HasAttrGuardExtension
         from typing import Any, cast
         from qcore.testing import Anything
+        from dataclasses import dataclass
 
+        @dataclass
         class Quemisia(object):
             def gravis(self):
                 if hasattr(self, "xaymaca"):
                     print(self.xaymaca)
 
+        def now_it_has_it(o: object):
+            o.xaymaca  # E: undefined_attribute
+            if hasattr(o, "xaymaca"):
+                print(o.xaymaca)
+
         def wrong_args():
             hasattr()  # E: incompatible_call
 
         def mistyped_args():
             hasattr(True, False)  # E: incompatible_argument
 
-        inferred = AnnotatedValue(
-            TypedValue(bool),
-            [
-                HasAttrGuardExtension(
-                    "object", KnownValue("__qualname__"), AnyValue(AnySource.inference)
-                ),
-                cast(Any, Anything),
-            ],
-        )
+        # The HasAttrGuard gets stripped.
+        inferred = AnnotatedValue(TypedValue(bool), [cast(Any, Anything)])
 
         def only_on_class(o: object):
             val = hasattr(o, "__qualname__")