Skip to content

EEx typings mismatch causing dialyzer errors on 1.19 #14834

@dkulchenko

Description

@dkulchenko

Elixir and Erlang/OTP versions

Erlang/OTP 28 [erts-16.1] [source] [64-bit] [smp:28:28] [ds:28:28:10] [async-threads:1] [jit:ns]

Elixir 1.19.0 (compiled with Erlang/OTP 28)

Operating system

Debian 13

Current behavior

I was trying to track down why mjml_eex (https://github.com/akoutmos/mjml_eex) started throwing up Dialyzer errors on 1.19, and got it down to a minimal repro using only stdlib.

The following file (incorrectly) reports a dialyzer error on 1.19 but not on 1.18:

defmodule EExDialyzerBug do
  @moduledoc """
  Minimal reproduction for Elixir 1.19 dialyzer issue with EEx.compile_string/2.

  Running `elixir --version` shows: Elixir 1.19.0 (compiled with Erlang/OTP 28)

  ## Issue

  EEx.compile_string/2 accepts [compile_opt] which includes {:engine, module()},
  but internally passes all options to tokenize/2 which only accepts [tokenize_opt].

  1.19's enhanced type checking detects this spec inconsistency.

  ## To reproduce

  1. Create a new Mix project: `mix new test_eex_bug`
  2. Add dialyxir to mix.exs deps:
     {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}
  3. Add to mix.exs project/0 function:
     dialyzer: [plt_add_apps: [:eex]]
  4. Place this file in lib/
  5. Run: `mix deps.get && mix compile && mix dialyzer`

  ## Expected result

  No dialyzer warnings (the code works correctly at runtime).

  ## Actual result

  Dialyzer reports:
  "Function MACRO-__using__/2 has no local return."
  "The call 'Elixir.EEx':compile_string(...,[{'engine', ...}]) will never return
  since it differs in the 2nd argument from the success typing arguments"
  """

  defmacro __using__(_opts) do
    # This call inside the macro triggers the dialyzer error
    template_ast = EEx.compile_string("<p>Hello <%= @name %>!</p>", engine: EEx.SmartEngine)

    quote do
      def render(assigns) do
        var!(assigns) = assigns
        unquote(template_ast)
      end
    end
  end
end

defmodule EExDialyzerBug.User do
  use EExDialyzerBug
end

On 1.19, shows:

lib/dialyzer_eex_bug.ex:35:24: The call 'Elixir.EEx':compile_string(<<60,112,62,72,101,108,108,111,32,60,37,61,32,64,110,97,109,101,32,37,62,33,60,47,112,62>>,[{'engine', 'Elixir.EEx.SmartEngine'}]) will never return since it differs in the 2nd argument from the success typing arguments: (binary(),[{'column',non_neg_integer()} | {'file',binary()} | {'indentation',non_neg_integer()} | {'line',non_neg_integer()} | {'trim',boolean()}])

This is because EEx.compile_string/2 accepts [compile_opt] which includes {:engine, module()}, but internally passes all options to tokenize/2 which only accepts [tokenize_opt].

Expected behavior

The same code on 1.18.4, reports:

Total errors: 0, Skipped: 0, Unnecessary Skips: 0
done in 0m1.5s
done (passed successfully)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions