Why does Code.compile_string report deprecation warnings only sometimes?

I’m having trouble understanding why function deprecation warnings don’t always show up when I compile code that uses deprecated functions.

Let me explain with an example: HashDict.new/0 is a deprecated function. Let’s say I have a file called deprecated_test.ex. The file contains a module and a function definition that uses the deprecated function:

# deprecated_test.ex
defmodule Foo do
  def foo, do: HashDict.new()
end

If I compile the file with elixirc, I get a nice deprecation warning:

angelika in ~/Documents
$ elixirc deprecated_test.ex
warning: HashDict.new/0 is deprecated. Use maps and the Map module instead
  deprecated_test.ex:2: Foo.foo/0

However, if I start iex, load the same file and compile it with Code.compile_string/1, I don’t get the warning:

iex(2)> code = File.read!("./deprecated_test.ex")
"defmodule Foo do\n  def foo, do: HashDict.new()\nend\n"

iex(3)> Code.compile_string(code)
[
  {Foo,
   <<70, 79, 82, 49, 0, 0, 4, 208, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0,
     153, 0, 0, 0, 16, 10, 69, 108, 105, 120, 105, 114, 46, 70, 111, 111, 8, 95,
     95, 105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>}
]

I thought this means that Code.compile_string/1 won’t give me deprecation warnings at all, but that’s not true.

I get a deprecation warning about HashDict.new/0 if I try different code snippets:

iex(4)> Code.compile_string("fn -> HashDict.new() end")
warning: HashDict.new/0 is deprecated. Use maps and the Map module instead
  nofile:1

[]

or

iex(15)> Code.compile_string("ref = &HashDict.new/0")
warning: HashDict.new/0 is deprecated. Use maps and the Map module instead
  nofile:1

warning: variable "ref" is unused (if the variable is not meant to be used, prefix it with an underscore)
  nofile:1

[]

Why is this happening? Can I somehow ensure that I will always get exactly the same deprecation warnings when compiling code with Code.compile_string/1 and/or Code.compile_quoted/1 as I would get with elixirc?

5 Likes

It seems that compile_file/2 call the private function verify_loaded/1 in Code which verifies some things (e.g. deprecations) (see code.ex#L1503).
compile_string/2 doens’t (see code.ex#L1466).

Not sure why only the compile_file/2 checks this. Maybe performance reasons?

1 Like

Thanks a lot! Based on your hints, I was able to get the deprecation checks working fully.

The only purpose of my code is to get all the compilation warnings. Here’s what I had before:

warnings =
  capture_io(:stderr, fn ->
    Code.compile_quoted(code_ast, Path.basename(code_path))
    |> Enum.each(fn {module, _binary} ->
      :code.delete(module)
      :code.purge(module)
    end)
  end)

And here’s what I have now:

warnings =
  capture_io(:stderr, fn ->
    :elixir_compiler.quoted(code_ast, code_path, fn _, _ -> :ok end)
    |> Enum.each(fn {module, map, binary} ->
      Module.ParallelChecker.verify([{map, binary}], [])
      :code.delete(module)
      :code.purge(module)
    end)
  end)

I’m just not sure if it’s a great idea to use a private module (Module.ParallelChecker) like this.

1 Like

Both :elixir_compiler and Module.ParallelChecker are private, so please don’t use them. :slight_smile: Please open up an issue and we will make sure to also check for deprecations from compile_string. :slight_smile:

8 Likes

That would be great :purple_heart: I opened the issue on GitHub.