IntelliJ Elixir, the Elixir plugin for JetBrains IDEs (IntelliJ IDEA, RubyMine, WebStorm, etc), version 11.12.0 has been released. Let’s see what’s new.
I love Open Source!
Within an hour of releasing 11.11.0, Tomoki Odaka had already opened a bug. That bug was perennial class of “read access is allowed from event dispatch thread”, which is fairly easy to fix with a readAction
. It was the next two bugs that shaped the focus of 11.12.0.
It was in #1997 where Tomoki Odaka gave a specific file in gumi/yacto
to reproduce the StackOverflowError
bugs.
By the way, this is an excellent bug report. It has reproduction steps and an open source repository I can use to run those steps the same as the reporter.
Using Yacto as a test project, I ran the Elixir References Inspection over the whole project and manually checked any errors to see if I could make the resolution better.
Lexical scope vs use
While preparing 11.11.0, I had to add a fix:
// descend in modular to check for nested modulars in case their relative name is being used
keepProcessing && match.whileInStabBodyChildExpressions { childExpression ->
execute(childExpression, state)
}
This restored nested modules being resolvable by their relative names, such as in the regression test for #1270:
defmodule Autocomplete do
defmodule State do
def another_test do
end
end
def test do
State.<caret>
end
defp internal_test do
end
end
When I added that fix. I thought, “Oh, this is a bit inefficient” as I only need to descend back into the modular to find nested modulars, but I didn’t expect it to cause bugs. I was wrong.
What happened was that any module with two (or more) use
s would loop. In the test case Tomoki Odaka found
defmodule WorkTest do
use ExUnit.Case, async: true
use Bitwise
test "test" do
assert 1 == 1
Enum.ma█
end
end
The use Bitwise
can ignore itself when it is resolving Bitwise
because a use
is prevented from descending into itself to resolve its alias argument
// don't descend back into `use` when the entrance is the alias to the `use` like `MyAlias` in `use MyAlias`.
if (!useCall.isAncestor(resolveState.get(ENTRANCE))) {
When it gets to use ExUnit.Case, async: true
, the ExUnit.Case
needs to be resolved and so it goes to the defmodule WorkTest
, doesn’t find it, goes back into the body, skips use ExUnit.Case
, but now can descend in use Bitwise
. It keeps alternating back and forth about who is skipped and who can be descended into.
The fix is fairly simple: only descend into children if it is already known to be a modular:
keepProcessing && match.whileInStabBodyChildExpressions { childExpression ->
if (childExpression is Call && isModular(childExpression)) {
execute(childExpression, state)
} else {
true
}
}
Memoize
The Yacto project uses memoize to memoize fetching its config in Yacto.DB.Shard.get_config/1. use Memoize
introduces defmemo
and defmemop
, to introduce memoizing versions of def
and defp
, respectively, so that’s how the plugin treats them now.
ecto_sql
without ecto
yacto
declares ecto_sql
as a dependency, but not ecto
itself while the code in Yacto
uses modules defined only in ecto
. It wasn’t obvious to me at first why ecto_sql
‘s dependency on ecto
wasn’t bringing in Ecto
. It turns out that ecto
is included indirectly through a ecto_dep()
function.
defp deps do
[
ecto_dep(),
{:telemetry, "~> 0.4.0 or ~> 1.0"},
...
The dependency Library management code I had in the plugin could only handle tuples and had no ability to statically follow function calls, so I added it.
Exceptions
defexception
in addition to define the exception struct also defines exception/1
and message/1
, so calls to exception/1
and message/1
resolve to the defexception
.
Callbacks
It can be helpful to know that a function name is being used because it’s a callback in a @behaviour
, so when a function can’t be found directly, it will be matched against callback name and arities.
Arity intervals from unquote_splicing
Functions defined with unquote_splicing
, such as Ecto.Schema.__schema/2
:
for clauses <- Ecto.Schema.__schema__(fields, field_sources, assocs, embeds),
{args, body} <- clauses do
def __schema__(unquote_splicing(args)), do: unquote(body)
end
Need to have their arity not be the number of PsiElement
s in the parentheses. Any call to unquote_splicing(...)
can end up have 0 to infinite parameters, so it means when one is seen, the range of minimum...maximum
should change to an open interval of minimum...
. This required changing IntRange resolvedFinalArityChange()
to ArityInterval resolvedFinalArityInterval()
on all Call
s, which was a large change. It also meant changing a lot of ArityRange
types to ArityInterval
, and NameArityRange
to NameArityInterval
, which influenced the variable names.
Since all Call
s support ArityInterval
s now and not just special forms and Ecto DSLs, exportArity
is changed to always state the ResolveState
, so that the special form changes can be integrated for all callers.
The actual implementation of CallImpl.resolvedFinalArityRange
is changes to fold over the ArityInterval
:
- Normal arguments increase the
minimum
andmaximum
. - Default arguments increase only the
maximum
. unquote_splicing
changes themaximum
tonull
to indicate the interval is half open.
unquote(block)
Some macros, most importantly Yacto’s schema .. do
, unquote the do
block inside a quote block:
defmacro schema(source, do: block) do
quote do
import Yacto.Schema
unquote(block)
For code in the block
, such as the call to field
in one of the test files
defmodule Yacto.ShardingTest.Schema.Player do
use Yacto.Schema
def dbname(), do: :player
schema @auto_source do
field(:name, :string)
end
end
to see the import Yacto.Schema
, which pulls in field
, special handling of unquote(block)
needed to be added to the plugin.
Less noise
The error reporter isn’t terribly smart–it can’t tell if a bug already exists because it doesn’t have access to a GitHub login, so I often get a lot of duplicate issues. Some issues are also configuration issues that I can’t fix, and I can only advise users how to fix.
One of those configuration cases is for the Elixir SDK not being set before trying to run the Dialyzer Inspection, so I made it so it doesn’t throw a reportable error and instead will notify users that they need to setup their SDK.
mix deps options
The missing options to mix deps have been added, so they don’t generate “Don’t know if Mix.Dep option is important for determining location of dependency” anymore. The missing options were env
, manager
, repo
, sparse
, submodules
, and system_env
, so likely affected users of private hex organizations or more complex git dependencies.
Help from JetBrains
Alexandr Evstigneev from JetBrains dropped by with a patch to fix a problem with EEx! If you ever saw “Wrong element created by ASTFactory” when editing EEx, that’s fixed now.
Installation
The plugin itself is free and open source as is IntelliJ Community Edition. You can download IntelliJ and install the plugin inside the IDE.
DockYard ❤️ FOSS
I’m able to work on IntelliJ Elixir because of DockYard Days, a program which provides team members with dedicated time to devote to individual skills and community growth projects, such as contributing to open source.
Supporting IntelliJ Elixir.
If you’d like, you can support the project directly.
If you have a complex Elixir project that you can’t open source, you can contact DockYard about having me work with your team to add missing features and the bug fixes you need to get the most out of your IDE.