What's New in IntelliJ Elixir 11.12.0

Open laptop on table next to backpack
Elle Imhoff

Engineer

Elle Imhoff

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) uses 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.maend
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 PsiElements 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 Calls, 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 Calls support ArityIntervals 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 and maximum.
  • Default arguments increase only the maximum.
  • unquote_splicing changes the maximum to null 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.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box