LoginSignup
4
0

More than 1 year has passed since last update.

[Elixir] mix xref trace を真似して compilation tracer を使ってみる

Last updated at Posted at 2021-12-21

株式会社ACCESS Advent Calendar 2021 22日目の記事だよー!

はじめに

環境

$ iex
Erlang/OTP 22 [erts-10.7.2.15] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]

Interactive Elixir (1.13.0) - press Ctrl+C to exit (type h() ENTER for help)

Tracer module を作ってみる

defmodule CompilationTracer do
  def run(file, apps) do
    set =
      for app <- apps,
          modules = Application.spec(app, :modules),
          module <- modules,
          into: MapSet.new(),
          do: module

    old = Code.compiler_options(ignore_module_conflict: true, tracers: [__MODULE__])
    ets = :ets.new(__MODULE__, [:named_table, :duplicate_bag, :public])
    :ets.insert(ets, [{:config, set, trace_label(nil)}])

    try do
      Code.compile_file(file)
    else
      _ ->
        :ets.delete(ets, :modules)
        print_traces(Enum.sort(:ets.lookup_element(__MODULE__, :entry, 2)))
    after
      :ets.delete(ets)
      Code.compiler_options(old)
    end
  end

  # 以下に下記のコードを丸ごとコピーする
  # https://github.com/elixir-lang/elixir/blob/v1.13.0/lib/mix/lib/mix/tasks/xref.ex#L509-L585

ほとんど mix xref trace のコードから持ってきています。
元のコードと違う点は以下です。

  • 検出対象の application を設定できるようにしています。
    • apps で指定した application 内の関数や macro の呼び出しが検出されます。
    • mix xref trace では自分の Mix project で管理している application が対象になります。
      • --include-siblings オプションにより、同じ umbrella projects 内の application を対象にすることもできます。
  • 使わないオプションに関連するコードを削除してあります。

Tracer module を使ってみる

テスト用の Mix project

Macro を多用している gettext を使った Mix project を準備します。
$ mix new gettext_example して、gettext を deps に入れて、以下 2 つのファイルを書きます。

# lib/gettext_example/gettext.ex
defmodule GettextExample.Gettext do
  use Gettext, otp_app: :gettext_example
end
# lib/gettext_example.ex
defmodule GettextExample do
  import GettextExample.Gettext

  def hello do
    gettext("Here is one string to translate")
  end
end

Compilation tracing してみる

少しは面白い結果になるように、gettext のバージョンを v0.17.1v0.18.2 の 2 通り使って実行してみます。

# gettext = v0.17.1 の場合
iex(2)> Mix.Project.config()[:elixirc_paths] |> Mix.Utils.extract_files([:ex]) |> Enum.each(&CompilationTracer.run(&1, [:gettext]))
lib/gettext_example.ex:9: call Gettext.dgettext/4 (runtime)
lib/gettext_example/gettext.ex:1: alias Gettext.Backend (compile)
lib/gettext_example/gettext.ex:1: require Gettext.Backend (export)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.__before_compile__/1 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.append_extracted_comment/1 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.get_and_flush_extracted_comments/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.get_and_flush_extracted_comments/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/5 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/5 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.ExtractorAgent.add_backend/1 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.ExtractorAgent.add_backend/1 (compile)
lib/gettext_example/gettext.ex:2: require Gettext (export)
lib/gettext_example/gettext.ex:2: require Gettext.Interpolation (export)
lib/gettext_example/gettext.ex:2: require Gettext.Interpolation (export)
lib/gettext_example/gettext.ex:2: call Gettext.__using__/1 (compile)
lib/gettext_example/gettext.ex:2: call Gettext.Compiler.warn_if_domain_contains_slashes/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Compiler.warn_if_domain_contains_slashes/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.to_interpolatable/1 (runtime)
# gettext = v0.18.2 の場合
iex(2)> Mix.Project.config()[:elixirc_paths] |> Mix.Utils.extract_files([:ex]) |> Enum.each(&CompilationTracer.run(&1, [:gettext]))
lib/gettext_example.ex:9: call Gettext.dpgettext/5 (runtime)
lib/gettext_example/gettext.ex:1: alias Gettext.Backend (compile)
lib/gettext_example/gettext.ex:1: require Gettext.Backend (export)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.__before_compile__/1 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.append_extracted_comment/1 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.get_and_flush_extracted_comments/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Compiler.get_and_flush_extracted_comments/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/6 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/6 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extracting?/0 (runtime)
lib/gettext_example/gettext.ex:1: call Gettext.ExtractorAgent.add_backend/1 (compile)
lib/gettext_example/gettext.ex:1: call Gettext.ExtractorAgent.add_backend/1 (compile)
lib/gettext_example/gettext.ex:2: require Gettext (export)
lib/gettext_example/gettext.ex:2: require Gettext.Interpolation (export)
lib/gettext_example/gettext.ex:2: require Gettext.Interpolation (export)
lib/gettext_example/gettext.ex:2: call Gettext.__using__/1 (compile)
lib/gettext_example/gettext.ex:2: call Gettext.Compiler.warn_if_domain_contains_slashes/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Compiler.warn_if_domain_contains_slashes/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.interpolate/2 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: call Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.to_interpolatable/1 (runtime)
lib/gettext_example/gettext.ex:2: import Gettext.Interpolation.to_interpolatable/1 (runtime)

gettext のバージョンを更新したことにより、呼び出している関数や macro が変わったことが分かります。

1c1
< lib/gettext_example.ex:9: call Gettext.dgettext/4 (runtime)
---
> lib/gettext_example.ex:9: call Gettext.dpgettext/5 (runtime)
11a12,13
> lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
> lib/gettext_example/gettext.ex:1: call Gettext.Compiler.expand_to_binary/4 (runtime)
14,15c16,17
< lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/5 (runtime)
< lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/5 (runtime)
---
> lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/6 (runtime)
> lib/gettext_example/gettext.ex:1: call Gettext.Extractor.extract/6 (runtime)

できたこと

ライブラリの macro を使っている場合、自分のコードは全く変えていないのに、呼び出している関数や macro が変わっているケースがあります。
もちろんコードを追えばどのような変化があったのか分かるのですが、compilation tracers を利用することによって、苦労せずに見やすい形でその変化を確認することができました。

(実際にこのような変化を確認したいケース自体があまりありませんが。。。)

(おまけ)ある application の関数と macro の一覧を取得する

mfa_list =
  for m <- Application.spec(:gettext, :modules),
      {f, a} <- m.module_info()[:exports],
      do: {m, f, a}

gettextv0.17.1v0.18.2 で diff を取ってみると、以下のようになりました。

5d4
< {Gettext, :dngettext, 5}
6a6,8
> {Gettext, :dpgettext, 4}
> {Gettext, :dpngettext, 6}
> {Gettext, :dpngettext, 7}
13a16,18
> {Gettext, :pgettext, 3}
> {Gettext, :pgettext, 4}
> {Gettext, :pngettext, 6}
19a25
> {Gettext, :dpgettext, 5}
31,34d36
< {Gettext.Compiler, :append_extracted_comment, 1}
< {Gettext.Compiler, :expand_to_binary, 4}
< {Gettext.Compiler, :get_and_flush_extracted_comments, 0}
< {Gettext.Compiler, :warn_if_domain_contains_slashes, 1}
36a39,42
> {Gettext.Compiler, :warn_if_domain_contains_slashes, 1}
> {Gettext.Compiler, :append_extracted_comment, 1}
> {Gettext.Compiler, :get_and_flush_extracted_comments, 0}
> {Gettext.Compiler, :expand_to_binary, 4}
40d45
< {Gettext.Error, :exception, 1}
43a49
> {Gettext.Error, :exception, 1}
47,48d52
< {Gettext.Extractor, :extract, 5}
< {Gettext.Extractor, :extracting?, 0}
53a58,59
> {Gettext.Extractor, :extract, 6}
> {Gettext.Extractor, :extracting?, 0}
75,77d80
< {Gettext.Interpolation, :interpolate, 2}
< {Gettext.Interpolation, :keys, 1}
< {Gettext.Interpolation, :to_interpolatable, 1}
79a83,85
> {Gettext.Interpolation, :keys, 1}
> {Gettext.Interpolation, :interpolate, 2}
> {Gettext.Interpolation, :to_interpolatable, 1}
96d101
< {Gettext.PO, :dump, 2}
98,99d102
< {Gettext.PO, :parse_file!, 1}
< {Gettext.PO, :parse_string, 1}
102a106,108
> {Gettext.PO, :parse_string, 1}
> {Gettext.PO, :dump, 2}
> {Gettext.PO, :parse_file!, 1}
115d120
< {Gettext.PO.SyntaxError, :exception, 1}
118a124
> {Gettext.PO.SyntaxError, :exception, 1}
120d125
< {Gettext.PO.Tokenizer, :tokenize, 1}
122a128
> {Gettext.PO.Tokenizer, :tokenize, 1}
129,132d134
< {Gettext.PO.Translations, :autogenerated?, 1}
< {Gettext.PO.Translations, :find, 2}
< {Gettext.PO.Translations, :mark_as_fuzzy, 1}
< {Gettext.PO.Translations, :protected?, 2}
135a138,141
> {Gettext.PO.Translations, :mark_as_fuzzy, 1}
> {Gettext.PO.Translations, :autogenerated?, 1}
> {Gettext.PO.Translations, :protected?, 2}
> {Gettext.PO.Translations, :find, 2}
137a144
> {Gettext.Plural, :nplurals, 1}
142d148
< {Gettext.Plural, :nplurals, 1}
146d151
< {Gettext.Plural.UnknownLocaleError, :exception, 1}
149a155,162
> {Gettext.Plural.UnknownLocaleError, :exception, 1}
> {Gettext.PluralFormError, :__info__, 1}
> {Gettext.PluralFormError, :__struct__, 0}
> {Gettext.PluralFormError, :__struct__, 1}
> {Gettext.PluralFormError, :exception, 1}
> {Gettext.PluralFormError, :message, 1}
> {Gettext.PluralFormError, :module_info, 0}
> {Gettext.PluralFormError, :module_info, 1}

例えば、バージョンアップで Gettext.Extractor.extract/5 が無くなっているので、v0.17.1 で compile した gettext_examplev0.18.2 の環境で使おうとしても無理ということが分かります。

おわりに

Compilation tracers は色々な使い方ができそうですね!

23 日の担当は @Hiroshi_Tsukamoto さんです。
投稿楽しみにしています。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0