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
Suggestion: Improve typechecker error for refinement on container keys #8620
Comments
This is hard. I believe we could support refinements like this for shapes, although we currently don't: function example2(shape('bar' => mixed) $foo): void {
if ($foo['bar'] is Traversable<_>) {
foreach ($foo['bar'] as $it) {
// do something
}
}
} This does work for classes: class MyClass {
public function __construct(public mixed $m) {}
}
function example3(MyClass $foo): void {
if ($foo->m is Traversable<_>) {
foreach ($foo->m as $it) {
// do something
}
}
} This technique is called "fake members" in the typechecker, because the definition of function totally_unrelated(): void {}
class MyClass {
public function __construct(public mixed $m) {}
}
function example3(MyClass $foo): void {
if ($foo->m is Traversable<_>) {
totally_unrelated(); // Maybe this modified $foo somehow?
foreach ($foo->m as $it) {
// do something
}
}
} For your example, we don't want to refine a We could potentially look at making the error message better though. Something like "Consider assigning to a local and then using |
Are you only interested in this error on bad usage of function takes_traversable(Traversable<mixed> $_): void {}
function example(dict<string, mixed> $foo): void {
if ($foo['bar'] is Traversable<_>) {
takes_traversable($foo['bar']);
}
} |
I'm interested in the more general case of expecting refinement. I can see how refining dict keys would not make sense in the type system. An error message that guides people to using a local variable would be quite welcome! |
I poked at this a little bit today (proof-of-concept internally D18872824, should be exported as a pull request in the next few minutes). This does roughly what you want for
I'm not sure if the error message is always a usability win though. It's currently firing for any array or object access, so the code like this would fire:
In this case, saying "consider assigning to a variable" is actively misleading -- the user has presumably used entirely the wrong variable or type. There's nothing they can do with Do you have any ideas for good heuristics for when this should fire? I don't want logic that looks for a |
Urgh, PR export failed. Here's the change: diff --git a/hphp/hack/src/typing/typing.ml b/hphp/hack/src/typing/typing.ml
--- a/hphp/hack/src/typing/typing.ml
+++ b/hphp/hack/src/typing/typing.ml
@@ -444,6 +444,12 @@
Typing_reactivity.disallow_atmost_rx_as_rxfunc_on_non_functions env param ty;
(env, ty)
+let expr_could_be_refined (e : ('a, 'b, 'c, 'd) Aast.expr) =
+ match snd e with
+ | Aast.Array_get (_, _) -> true
+ | Aast.Class_get (_, _) -> true
+ | _ -> false
+
(* Return a map describing all the fields in this record, including
inherited fields, and whether they have a default value. *)
let all_record_fields (rd : Decl_provider.record_def_decl) :
@@ -1246,7 +1252,7 @@
let (env, (te1, te2, tb)) =
LEnv.stash_and_do env [C.Continue; C.Break] (fun env ->
let env = LEnv.save_and_merge_next_in_cont env C.Continue in
- let (env, tk, tv) = as_expr env ty1 (fst e1) e2 in
+ let (env, tk, tv) = as_expr env ty1 e1 e2 in
let alias_depth =
if env.in_loop then
1
@@ -1497,7 +1503,7 @@
let (env, tb) = block env b in
(env, (env.lenv, (sid, exn, tb)))
-and as_expr env ty1 pe e =
+and as_expr env ty1 ((pe, _) as e_iter) e =
let env = Env.open_tyvars env pe in
(fun (env, expected_ty, tk, tv) ->
let rec distribute_union env ty =
@@ -1511,7 +1517,7 @@
let env = SubType.sub_type env ty tv Errors.unify_error in
env
else
- let ur = Reason.URforeach in
+ let ur = Reason.URforeach (expr_could_be_refined e_iter) in
Type.sub_type pe ur env ty expected_ty Errors.unify_error
in
let env = distribute_union env ty1 in
diff --git a/hphp/hack/src/typing/typing_reason.ml b/hphp/hack/src/typing/typing_reason.ml
--- a/hphp/hack/src/typing/typing_reason.ml
+++ b/hphp/hack/src/typing/typing_reason.ml
@@ -717,7 +717,7 @@
| URassign_inout
| URhint
| URreturn
- | URforeach
+ | URforeach of bool
| URthrow
| URvector
| URkey
@@ -764,7 +764,13 @@
| URassign -> "Invalid assignment"
| URassign_branch -> "Invalid assignment in this branch"
| URassign_inout -> "Invalid assignment to an inout parameter"
- | URforeach -> "Invalid foreach"
+ | URforeach b ->
+ "Invalid foreach"
+ ^
+ if b then
+ ", consider assigning this value to a variable and using is/as"
+ else
+ ""
| URthrow -> "Invalid exception"
| URvector -> "Some elements in this Vector are incompatible"
| URkey -> "The keys of this Map are incompatible"
-- |
HHVM Version
Standalone code, or other way to reproduce the problem
Actual result
Expected result
This is something that consistently trips up new Hack developers as they learn about type refinement and
is/as
. The most ideal state would be that the typechecker could remember the refinement on a sub-key of a container. But I expect that's a significant change. A perhaps much smaller change would be to provide a clearer error message about how to fix this problem, such as recommending the user pull$foo['bar']
into a local variable before refining its type with the type test.The text was updated successfully, but these errors were encountered: