-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Proposal: Improve error messages for dot syntax #12003
Proposal: Improve error messages for dot syntax #12003
Conversation
lib/elixir/src/elixir_erl_pass.erl
Outdated
@@ -242,11 +243,22 @@ translate({{'.', _, [Left, Right]}, Meta, []}, _Ann, S) when is_tuple(Left), is_ | |||
{clause, Generated, | |||
[TVar], | |||
[[?remote(Generated, erlang, is_map, [TVar])]], | |||
[?remote(Ann, erlang, error, [TError])]}, | |||
[?remote(Ann, erlang, error, [TBadKeyError])]}, | |||
{clause, Generated, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm creating more clauses directly in the generated code: should I be delegating to some dot_apply
function instead? (that would be @doc false
maybe?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We definitely want to avoid generating additional code and we want to avoid changing the semantics of the failure. My suggestion is the following:
-
If no_parens is true, we generate the existing bad key error (and we can improve the BadKeyError message as a whole)
-
If no_parens is false, we raise the undefined function error (and we can improve the UndefinedFunctionError as a whole)
I know there will still be some ambiguity in relation to apply or Map.fetch(nil, ...) but I think it is important to not add new exception types.
In the future, we can use the error_info from EEP54 to add some special annotation (and annotate that we were indeed in a dot or a similar).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future, we can use the error_info from EEP54 to add some special annotation (and annotate that we were indeed in a dot or a similar).
Oh, I see, this is the piece I was missing to be able to propagate this information without making a mess. Will wait for EEP54 for the dot-aware messages.
we want to avoid changing the semantics of the failure
Makes perfect sense, will try the approach you suggested 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EEP54 requires OTP 24, so we should add that particular bit later.
assert blame_message(nil, fn _ -> nil.foo() end) == | ||
"could not find function :foo on nil. When using the module.function() " <> | ||
"syntax, make sure the left side of the dot is a module" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- this is actually going in a different branch as
blame_message(nil, & &1.foo())
, which is why I added a test - this triggers a compile-time warning, any idea how I could silence it?
warning: nil.foo/0 is undefined
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to use eval.
lib/elixir/lib/exception.ex
Outdated
%ArgumentError{ | ||
message: | ||
"could not find key #{inspect(key)} on #{inspect(map)}. " <> | ||
"When using the map.field syntax, make sure the left side of the dot is a map" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a native speaker here, but I've mostly seen this referred to as
"When using the map.field syntax, make sure the left side of the dot is a map" | |
"When using the map.field syntax, make sure the left-hand side of the dot is a map" |
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wikipedia agrees, sounds good.
I copied this from the existing hint, after grepping through the codebase it seems we are currently using all of the following:
- left side
- left hand side
- left-hand side (the correct one)
We might want to fix the existing ones as well in a separate PR, WDYT?
lib/elixir/lib/exception.ex
Outdated
def normalize({:baddot, false, key, module}, _stacktrace) do | ||
%ArgumentError{ | ||
message: | ||
"could not find function #{inspect(key)} on #{inspect(module)}. " <> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we generally talk about functions being in modules?
"could not find function #{inspect(key)} on #{inspect(module)}. " <> | |
"could not find function #{inspect(key)} in #{inspect(module)}. " <> |
f38b14a
to
d341950
Compare
{clause, Generated, | ||
[{map, Ann, [{map_field_exact, Ann, TRight, TVar}]}], | ||
[], | ||
[TVar]}, | ||
{clause, Generated, | ||
[TVar], | ||
[], | ||
[{call, Generated, {remote, Generated, TVar, TRight}, []}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I removed the is_map -> badkey
clause here:
iex(1)> x = %{}; x.foo()
** (ArgumentError) you attempted to apply a function named :foo on %{}. If you are using Kernel.apply/3, make sure the module is an atom. If you are using the dot syntax, such as module.function(), make sure the left-hand side of the dot is a module atom
The happy path is still supported:
iex(1)> x = %{foo: 1}; x.foo()
1
but we don't assume this is what the user was trying to achieve when it doesn't. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have an issue to make x.foo()
on a map warn (see #9510). We can work on it as soon as we branch out for v1.14 (hopefully this week).
Current behavior:
Proposed behavior:
|
100% agreed on the proposed behaviour! Great summary! |
lib/elixir/src/elixir_erl_pass.erl
Outdated
translate_remote(Left, Right, Meta, [], S) | ||
when (Left =:= nil orelse is_boolean(Left)), is_atom(Right) -> | ||
Ann = ?ann(Meta), | ||
TLeft = {atom, Ann, Left}, | ||
TRight = {atom, Ann, Right}, | ||
case proplists:get_value(no_parens, Meta, false) of | ||
true -> | ||
TError = {tuple, Ann, [{atom, Ann, badkey}, TRight, TLeft]}, | ||
{?remote(Ann, erlang, error, [TError]), S}; | ||
false -> | ||
{{call, Ann, {remote, Ann, TLeft, TRight}, []}, S} | ||
end; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't need this, correct? we can just emit the original code and let it fail as usual?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the case where nil.id
is being called (as opposed to x = nil; x.id
).
But maybe there's a better way: we can replace the translate
clause above to check for these earlier: 55a3e64
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tradeoff is that we generate a bit of a garbage case
for nil.id
, but this should not really be a thing for production code anyway, so it probably doesn't have to be optimal, it just have to be correct when you try it out in IEx?
💚 💙 💜 💛 ❤️ |
Current behavior
The message looks very obvious to an Elixir dev used to it, but it could quite a lot to unpack and some prior knowledge is assumed:
nil
is an atomnil
gets interpreted as a moduleuser.id
anduser.id()
because of how its AST worksBesides, this message might be slightly sub-optimal:
nil
,true
andfalse
are not actually allowed module names, despite being atomsapply/3
, but in this case we know the dot syntax was usedno_parens
, so it might be better to assume it was an attempt to access a map key than an undefined function? (and maybe mentionnil.id/0
as a second scenario to be exhaustive)Proposed behaviour
Exact error messages and strategy TBD :)