Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package elixir for openSUSE:Factory checked in at 2024-09-22 11:06:06 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/elixir (Old) and /work/SRC/openSUSE:Factory/.elixir.new.29891 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "elixir" Sun Sep 22 11:06:06 2024 rev:33 rq:1202247 version:1.17.3 Changes: -------- --- /work/SRC/openSUSE:Factory/elixir/elixir.changes 2024-07-08 19:09:28.213395125 +0200 +++ /work/SRC/openSUSE:Factory/.elixir.new.29891/elixir.changes 2024-09-22 11:06:22.757492168 +0200 @@ -1,0 +2,6 @@ +Thu Sep 19 07:35:25 UTC 2024 - Alessio Biancalana <[email protected]> + +- Upgrade to Elixir 1.17.3: + * Changelog available at https://hexdocs.pm/elixir/1.17.3/changelog.html + +------------------------------------------------------------------- Old: ---- elixir-1.17.2-doc.zip elixir-1.17.2.tar.gz New: ---- elixir-1.17.3-doc.zip elixir-1.17.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ elixir.spec ++++++ --- /var/tmp/diff_new_pack.dhpiVC/_old 2024-09-22 11:06:24.021544378 +0200 +++ /var/tmp/diff_new_pack.dhpiVC/_new 2024-09-22 11:06:24.021544378 +0200 @@ -19,7 +19,7 @@ %define elixirdir %{_prefix}/lib/elixir Name: elixir -Version: 1.17.2 +Version: 1.17.3 Release: 0 Summary: Functional meta-programming aware language built atop Erlang License: Apache-2.0 ++++++ elixir-1.17.2-doc.zip -> elixir-1.17.3-doc.zip ++++++ /work/SRC/openSUSE:Factory/elixir/elixir-1.17.2-doc.zip /work/SRC/openSUSE:Factory/.elixir.new.29891/elixir-1.17.3-doc.zip differ: char 11, line 1 ++++++ elixir-1.17.2.tar.gz -> elixir-1.17.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/CHANGELOG.md new/elixir-1.17.3/CHANGELOG.md --- old/elixir-1.17.2/CHANGELOG.md 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/CHANGELOG.md 2024-09-18 14:33:39.000000000 +0200 @@ -70,6 +70,24 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1)) ``` +## v1.17.3 (2024-09-18) + +### 1. Bug fixes + +#### Elixir + + * [Duration] Fix parsing of fractional durations with non-positive seconds + * [Kernel] Do not attempt to group module warnings when they have a large context + +#### IEx + + * [IEx.Helpers] Properly reconsolidate protocols on `recompile` + +#### Mix + + * [mix compile.elixir] Do not verify modules twice + * [mix xref] Respect the `--label` option on stats and cycles + ## v1.17.2 (2024-07-06) ### 1. Bug fixes diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/RELEASE.md new/elixir-1.17.3/RELEASE.md --- old/elixir-1.17.2/RELEASE.md 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/RELEASE.md 2024-09-18 14:33:39.000000000 +0200 @@ -10,13 +10,13 @@ 4. Commit changes above with title "Release vVERSION" and push it -6. Once GitHub actions completes, generate a new tag, and push it +5. Once GitHub actions completes, generate a new tag, and push it -7. Wait until GitHub Actions publish artifacts to the draft release +6. Wait until GitHub Actions publish artifacts to the draft release -8. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one) +7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one) -9. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com` +8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com` ## Creating a new vMAJOR.MINOR branch (before first rc) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/VERSION new/elixir-1.17.3/VERSION --- old/elixir-1.17.2/VERSION 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/VERSION 2024-09-18 14:33:39.000000000 +0200 @@ -1 +1 @@ -1.17.2 +1.17.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/bin/elixir new/elixir-1.17.3/bin/elixir --- old/elixir-1.17.2/bin/elixir 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/bin/elixir 2024-09-18 14:33:39.000000000 +0200 @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.17.2 +ELIXIR_VERSION=1.17.3 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <<USAGE >&2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/bin/elixir.bat new/elixir-1.17.3/bin/elixir.bat --- old/elixir-1.17.2/bin/elixir.bat 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/bin/elixir.bat 2024-09-18 14:33:39.000000000 +0200 @@ -1,6 +1,6 @@ @echo off -set ELIXIR_VERSION=1.17.2 +set ELIXIR_VERSION=1.17.3 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/lib/calendar/iso.ex new/elixir-1.17.3/lib/elixir/lib/calendar/iso.ex --- old/elixir-1.17.2/lib/elixir/lib/calendar/iso.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/lib/calendar/iso.ex 2024-09-18 14:33:39.000000000 +0200 @@ -715,7 +715,15 @@ {second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] -> case parse_microsecond(rest) do {{ms, precision}, "S"} -> - ms = if second > 0, do: ms, else: -ms + ms = + case string do + "-" <> _ -> + -ms + + _ -> + ms + end + {:ok, [second: second, microsecond: {ms, precision}] ++ acc} _ -> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/lib/kernel.ex new/elixir-1.17.3/lib/elixir/lib/kernel.ex --- old/elixir-1.17.2/lib/elixir/lib/kernel.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/lib/kernel.ex 2024-09-18 14:33:39.000000000 +0200 @@ -6197,9 +6197,8 @@ """ @doc since: "1.17.0" - @spec to_timeout([component, ...] | timeout() | Duration.t()) :: timeout() - when component: [{unit, non_neg_integer()}, ...], - unit: :week | :day | :hour | :minute | :second | :millisecond + @spec to_timeout([{unit, non_neg_integer()}] | timeout() | Duration.t()) :: timeout() + when unit: :week | :day | :hour | :minute | :second | :millisecond def to_timeout(duration) def to_timeout(:infinity), do: :infinity diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/lib/macro.ex new/elixir-1.17.3/lib/elixir/lib/macro.ex --- old/elixir-1.17.2/lib/elixir/lib/macro.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/lib/macro.ex 2024-09-18 14:33:39.000000000 +0200 @@ -1832,13 +1832,13 @@ defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom), do: {:filename.dirname(env.file), true} - defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do + defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) and env.context != :match do env = update_in(env.versioned_vars, &maybe_escape_map/1) {maybe_escape_map(env), true} end defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env) - when is_atom(atom) and is_atom(field) do + when is_atom(atom) and is_atom(field) and env.context != :match do if Map.has_key?(env, field) do {maybe_escape_map(Map.get(env, field)), true} else diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/lib/module/parallel_checker.ex new/elixir-1.17.3/lib/elixir/lib/module/parallel_checker.ex --- old/elixir-1.17.2/lib/elixir/lib/module/parallel_checker.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/lib/module/parallel_checker.ex 2024-09-18 14:33:39.000000000 +0200 @@ -291,18 +291,34 @@ ## Warning helpers defp group_warnings(warnings) do - warnings - |> Enum.reduce(%{}, fn {module, warning, location}, acc -> - locations = MapSet.new([location]) - Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location)) - end) - |> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end) - |> Enum.sort() + {ungrouped, grouped} = + Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} -> + %{message: _} = diagnostic = module.format_diagnostic(warning) + + if Map.get(diagnostic, :group, false) do + locations = MapSet.new([location]) + + grouped = + Map.update(grouped, warning, {locations, diagnostic}, fn + {locations, diagnostic} -> {MapSet.put(locations, location), diagnostic} + end) + + {ungrouped, grouped} + else + {[{[location], diagnostic} | ungrouped], grouped} + end + end) + + grouped = + Enum.map(grouped, fn {_warning, {locations, diagnostic}} -> + {Enum.sort(locations), diagnostic} + end) + + Enum.sort(ungrouped ++ grouped) end defp emit_warnings(warnings, log?) do - Enum.flat_map(warnings, fn {module, warning, locations} -> - %{message: _} = diagnostic = module.format_diagnostic(warning) + Enum.flat_map(warnings, fn {locations, diagnostic} -> diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, &1)) log? and print_diagnostics(diagnostics) diagnostics diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/lib/module/types/of.ex new/elixir-1.17.3/lib/elixir/lib/module/types/of.ex --- old/elixir-1.17.2/lib/elixir/lib/module/types/of.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/lib/module/types/of.ex 2024-09-18 14:33:39.000000000 +0200 @@ -176,10 +176,15 @@ # TODO: Use the struct default values to define the default types. def struct(struct, args_types, default_handling, meta, stack, context) do context = remote(struct, :__struct__, 0, meta, stack, context) + + info = + struct.__info__(:struct) || + raise "expected #{inspect(struct)} to return struct metadata, but got none" + term = term() defaults = - for %{field: field} <- struct.__info__(:struct), field != :__struct__ do + for %{field: field} <- info, field != :__struct__ do {field, term} end @@ -700,14 +705,16 @@ " is undefined (module ", inspect(module), " is not available or is yet to be defined)" - ]) + ]), + group: true } end def format_diagnostic({:undefined_function, module, :__struct__, 0, _exports}) do %{ message: - "struct #{inspect(module)} is undefined (there is such module but it does not define a struct)" + "struct #{inspect(module)} is undefined (there is such module but it does not define a struct)", + group: true } end @@ -718,7 +725,8 @@ Exception.format_mfa(module, fun, arity), " is undefined or private", UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports) - ]) + ]), + group: true } end @@ -729,7 +737,8 @@ Exception.format_mfa(module, fun, arity), " is deprecated. ", reason - ]) + ]), + group: true } end @@ -741,7 +750,8 @@ inspect(module), " before invoking the macro ", Exception.format_mfa(module, fun, arity) - ]) + ]), + group: true } end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/lib/system.ex new/elixir-1.17.3/lib/elixir/lib/system.ex --- old/elixir-1.17.2/lib/elixir/lib/system.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/lib/system.ex 2024-09-18 14:33:39.000000000 +0200 @@ -1239,7 +1239,7 @@ This time is monotonically increasing and starts in an unspecified point in time. """ - @spec monotonic_time(time_unit) :: integer + @spec monotonic_time(time_unit | :native) :: integer def monotonic_time(unit) do :erlang.monotonic_time(normalize_time_unit(unit)) end @@ -1265,7 +1265,7 @@ case of time warps although the VM works towards aligning them. This time is not monotonic. """ - @spec system_time(time_unit) :: integer + @spec system_time(time_unit | :native) :: integer def system_time(unit) do :erlang.system_time(normalize_time_unit(unit)) end @@ -1316,7 +1316,7 @@ `monotonic_time/1`), gives the Erlang system time that corresponds to that monotonic time. """ - @spec time_offset(time_unit) :: integer + @spec time_offset(time_unit | :native) :: integer def time_offset(unit) do :erlang.time_offset(normalize_time_unit(unit)) end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/scripts/docs_config.exs new/elixir-1.17.3/lib/elixir/scripts/docs_config.exs --- old/elixir-1.17.2/lib/elixir/scripts/docs_config.exs 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/scripts/docs_config.exs 2024-09-18 14:33:39.000000000 +0200 @@ -5,10 +5,13 @@ skipped = Version.parse!("1.0.3") list_contents = - text_tags - |> String.split() - |> Enum.map(fn "v" <> rest -> Version.parse!(rest) end) - |> Enum.filter(&(Version.compare(&1, skipped) == :gt)) + for( + "v" <> rest <- String.split(text_tags), + not String.ends_with?(rest, "-latest"), + version = Version.parse!(rest), + Version.compare(version, skipped) == :gt, + do: version + ) |> Enum.sort({:desc, Version}) |> Enum.map_intersperse(", ", fn version -> version_string = Version.to_string(version) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/test/elixir/calendar/duration_test.exs new/elixir-1.17.3/lib/elixir/test/elixir/calendar/duration_test.exs --- old/elixir-1.17.2/lib/elixir/test/elixir/calendar/duration_test.exs 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/test/elixir/calendar/duration_test.exs 2024-09-18 14:33:39.000000000 +0200 @@ -237,8 +237,10 @@ assert Duration.from_iso8601("P4Y2W3Y") == {:error, :invalid_date_component} assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_date_component} assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_date_component} + assert Duration.from_iso8601("P0.5Y") == {:error, :invalid_date_component} assert Duration.from_iso8601("PT1D") == {:error, :invalid_time_component} assert Duration.from_iso8601("PT.6S") == {:error, :invalid_time_component} + assert Duration.from_iso8601("PT0.5H") == {:error, :invalid_time_component} assert Duration.from_iso8601("invalid") == {:error, :invalid_duration} end @@ -262,6 +264,9 @@ assert Duration.from_iso8601!("PT6S") == %Duration{second: 6} assert Duration.from_iso8601!("PT1,6S") == %Duration{second: 1, microsecond: {600_000, 1}} assert Duration.from_iso8601!("PT-1.6S") == %Duration{second: -1, microsecond: {-600_000, 1}} + assert Duration.from_iso8601!("PT0,6S") == %Duration{second: 0, microsecond: {600_000, 1}} + assert Duration.from_iso8601!("PT-0,6S") == %Duration{second: 0, microsecond: {-600_000, 1}} + assert Duration.from_iso8601!("-PT-0,6S") == %Duration{second: 0, microsecond: {600_000, 1}} assert Duration.from_iso8601!("-P10DT4H") == %Duration{day: -10, hour: -4} assert Duration.from_iso8601!("-P10DT-4H") == %Duration{day: -10, hour: 4} assert Duration.from_iso8601!("P-10D") == %Duration{day: -10} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/elixir/test/elixir/macro_test.exs new/elixir-1.17.3/lib/elixir/test/elixir/macro_test.exs --- old/elixir-1.17.2/lib/elixir/test/elixir/macro_test.exs 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/elixir/test/elixir/macro_test.exs 2024-09-18 14:33:39.000000000 +0200 @@ -190,6 +190,16 @@ assert Code.eval_quoted(expanded) == {env.versioned_vars, []} end + test "env in :match context does not expand" do + env = %{__ENV__ | line: 0, lexical_tracker: self(), context: :match} + + expanded = Macro.expand_once(quote(do: __ENV__), env) + assert expanded == quote(do: __ENV__) + + expanded = Macro.expand_once(quote(do: __ENV__.file), env) + assert expanded == quote(do: __ENV__.file) + end + defmacro local_macro(), do: raise("ignored") test "vars" do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/ex_unit/test/ex_unit/assertions_test.exs new/elixir-1.17.3/lib/ex_unit/test/ex_unit/assertions_test.exs --- old/elixir-1.17.2/lib/ex_unit/test/ex_unit/assertions_test.exs 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/ex_unit/test/ex_unit/assertions_test.exs 2024-09-18 14:33:39.000000000 +0200 @@ -256,6 +256,28 @@ end end + test "assert match with __ENV__ in the pattern" do + message = + ExUnit.CaptureIO.capture_io(:stderr, fn -> + assert_raise CompileError, fn -> + Code.eval_string(""" + defmodule EnvMatch do + import ExUnit.Assertions + + def run do + assert __ENV__ = %{} + end + end + """) + end + end) + + assert message =~ "invalid pattern in match, __ENV__ is not allowed in matches" + after + :code.purge(EnvMatch) + :code.delete(EnvMatch) + end + test "assert match?" do true = assert match?({2, 1}, Value.tuple()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/iex/lib/iex/helpers.ex new/elixir-1.17.3/lib/iex/lib/iex/helpers.ex --- old/elixir-1.17.2/lib/iex/lib/iex/helpers.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/iex/lib/iex/helpers.ex 2024-09-18 14:33:39.000000000 +0200 @@ -150,7 +150,7 @@ reenable_tasks(config) force? = Keyword.get(options, :force, false) - args = ["--purge-consolidation-path-if-stale", "--return-errors", consolidation] + args = ["--purge-consolidation-path-if-stale", consolidation, "--return-errors"] args = if force?, do: ["--force" | args], else: args {result, _} = Mix.Task.run("compile", args) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/mix/lib/mix/compilers/elixir.ex new/elixir-1.17.3/lib/mix/lib/mix/compilers/elixir.ex --- old/elixir-1.17.2/lib/mix/lib/mix/compilers/elixir.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/mix/lib/mix/compilers/elixir.ex 2024-09-18 14:33:39.000000000 +0200 @@ -1077,24 +1077,24 @@ # We merge stale_modules (which is a map of %{module => true} that the user changed) # into a map of modules we compiled (which is a map of %{module => record}). This is # fine because we only care about the keys. - runtime_modules = Map.merge(modules, stale_modules) + changed_modules = Map.merge(modules, stale_modules) # Now we do a simple pass finding anything that directly depends on the modules that # changed. We don't need to compute a fixpoint, because now only the directly affected # matter. {sources, runtime_modules} = - Enum.reduce(sources, {sources, Map.keys(runtime_modules)}, fn + Enum.reduce(sources, {sources, []}, fn {source_path, source_entry}, {acc_sources, acc_modules} -> source(export_references: export_refs, runtime_references: runtime_refs) = source_entry - if has_any_key?(runtime_modules, export_refs) or - has_any_key?(runtime_modules, runtime_refs) do + if has_any_key?(changed_modules, export_refs) or + has_any_key?(changed_modules, runtime_refs) do acc_sources = Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: [])) new_modules = - Enum.reject(source(source_entry, :modules), &Map.has_key?(runtime_modules, &1)) + Enum.reject(source(source_entry, :modules), &Map.has_key?(changed_modules, &1)) {acc_sources, new_modules ++ acc_modules} else diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/mix/lib/mix/tasks/xref.ex new/elixir-1.17.3/lib/mix/lib/mix/tasks/xref.ex --- old/elixir-1.17.2/lib/mix/lib/mix/tasks/xref.ex 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/mix/lib/mix/tasks/xref.ex 2024-09-18 14:33:39.000000000 +0200 @@ -14,16 +14,111 @@ $ mix xref MODE - All available modes are discussed below. + All available modes are discussed below, after a brief + introduction to xref. This task is automatically re-enabled, so you can print information multiple times in the same Mix invocation. - ## mix xref callers MODULE + ## A brief introduction to xref - Prints all callers of the given module. Example: + The goal of `xref` is to analyze the dependencies between modules + and files. It is most commonly used to find problematic areas where + touching one file in a project causes a large subset of the project + to recompile. The most common cause of these problems are the so-called + "compile-connected" files. Those are files you depend on at compile-time + (for example, by invoking its macro or using it in the body of amodule) + which also have their own dependencies. - $ mix xref callers MyMod + Therefore, if your goal is to reduce recompilations, the first step is to run: + + $ mix xref graph --format stats --label compile-connected + + This command will show general information about the project, but + focus on compile-connected dependencies. In the stats, you will see + the following report: + + Top 10 files with most incoming dependencies: + * lib/livebook_web.ex (97) + * lib/livebook/config.ex (3) + * proto/lib/livebook_proto/deployment_group.pb.ex (2) + * lib/livebook_web/plugs/memory_provider.ex (2) + * proto/lib/livebook_proto/user_connected.pb.ex (1) + + You can see the first file, "lib/livebook_web.ex", is depended on by 97 + other files and, because we are using compile-connected, it also means + that "lib/livebook_web.ex" itself has its own dependencies. We can find + which files depend on "lib/livebook_web.ex" at compile time like this: + + $ mix xref graph --sink lib/livebook_web.ex --label compile --only-nodes + + And you can find the files lib/livebook_web.ex depends on like this: + + $ mix xref graph --source lib/livebook_web.ex --only-nodes + + The trouble here is precisely that, if any of the files in the latter + command changes, all of the files in the first command will be recompiled, + because compile time dependencies are transitive. + + Having compile time dependencies is a common feature in Elixir projects. + However, the modules you depend on at compile-time must avoid runtime + dependencies within the same project. You can understand all of the + dependencies of a given file by running: + + $ mix xref trace lib/livebook_web.ex + + The command above will output three types of dependencies, which we + detail next. + + ### Dependency types + + Elixir tracks three types of dependencies between modules: compile, + exports, and runtime. If a module has a compile time dependency on + another module, the caller module has to be recompiled whenever the + callee changes (or any runtime dependency of the callee changes). + Let's see an example: + + # lib/a.ex + defmodule A do + @hello B.hello() + def hello, do: @hello + end + + # lib/b.ex + defmodule B do + def hello, do: "hello" + def world, do: C.world() + end + + # lib/c.ex + defmodule C do + def world, do: "world" + end + + If `C.world/0` changes, `B` is marked as stale. `B` does not need to + be recompiled, because it depends on `C` at runtime, but anything that + depends on `B` at compile-time has to recompile, and that includes `A`. + + Compile-time dependencies are typically added when using macros or + when invoking functions in the module body (outside of functions). + This type of transitive compile-time dependencies, such as `A` + depending on `C` at compile-time through `B`, are called compile-connected. + + Export dependencies are compile time dependencies on the module API, + namely structs and its public definitions. For example, if you import + a module but only use its functions, it is an export dependency. If + you use a struct, it is an export dependency too. Export dependencies + are only recompiled if the module API changes. Note, however, that compile + time dependencies have higher precedence than exports. Therefore if + you import a module and use its macros, it is a compile time dependency. + + Runtime dependencies are added whenever you invoke another module + inside a function. Modules with runtime dependencies do not have + to be compiled when the callee changes, unless there is a transitive + compile or an outdated export time dependency between them. + + Over the next sections, we will explain what which `mix xref` command + does in detail. ## mix xref trace FILE @@ -87,14 +182,13 @@ * `--exclude` - path to exclude. Can be repeated to exclude multiple paths. * `--label` - only shows relationships with the given label. - The labels are "compile", "export" and "runtime". By default, - the `--label` option simply filters the printed graph to show - only relationships with the given label. You can pass `--only-direct` - to trim the graph to only the nodes that have the direct - relationship given by label. There is also a special label - called "compile-connected" that keeps only compile-time files - with at least one transitive dependency. See "Dependency types" - section below. + The labels are "compile", "export" and "runtime". By default, the `--label` + option does not change how the graph is computed, it simply filters the + printed graph to show only relationships with the given label. However, + you can pass `--only-direct` to trim the graph to only the nodes that + have the direct relationship given by label. There is also a special + label called "compile-connected" that keeps only compile-time files with + at least one transitive dependency. See "Dependency types" section below. * `--group` - provide comma-separated paths to consider as a group. Dependencies from and into multiple files of the group are considered a single dependency. @@ -148,8 +242,8 @@ those options with `--label` and `--only-nodes` to get all files that exhibit a certain property, for example: - # To show all compile-time relationships - $ mix xref graph --label compile + # To show all compile-connected relationships + $ mix xref graph --label compile-connected # To get the tree that depend on lib/foo.ex at compile time $ mix xref graph --label compile --sink lib/foo.ex @@ -163,6 +257,9 @@ # To show general statistics about the graph $ mix xref graph --format stats + # To show all cycles with at least one compile-time dependency + $ mix xref graph --format cycles --label compile-connected + ### Understanding the printed graph When `mix xref graph` runs, it will print a tree of the following @@ -204,10 +301,9 @@ The `--label compile` flag removes all non-compile dependencies. However, this can be misleading because having direct compile time dependencies is - not necessarily an issue. The biggest concern, as mentioned above, are the - transitive compile time dependencies. You can get all compile time - dependencies that cause transitive compile time dependencies by using - `--label compile-connected`: + not necessarily an issue. The biggest concern are the transitive compile + time dependencies. You can get all compile time dependencies that cause + transitive compile time dependencies by using `--label compile-connected`: $ mix xref graph --label compile-connected lib/a.ex @@ -235,56 +331,35 @@ command will list all files from all umbrella children, without any namespacing. - ### Dependency types + ### Understanding the printed cycle - Elixir tracks three types of dependencies between modules: compile, - exports, and runtime. If a module has a compile time dependency on - another module, the caller module has to be recompiled whenever the - callee changes (or any runtime dependency of the callee changes). - Let's see an example: + If you run `mix xref graph --format cycle`, Elixir will print cycles + of shape: - # lib/a.ex - defmodule A do - @hello B.hello() - def hello, do: @hello - end + Cycle of length 3: - # lib/b.ex - defmodule B do - def hello, do: "hello" - def world, do: C.world() - end + lib/c.ex + lib/b.ex + lib/a.ex - # lib/c.ex - defmodule C do - def world, do: "world" - end + The cycles are given in order: `c.ex` depends on `b.ex` which depends + on `a.ex` which depends on `c.ex`. In particular, you want to avoid + cycles with compile dependencies in there. You can find those cycles + with: - If `C.world/0` changes, `B` is marked as stale. `B` does not need to - be recompiled, because it depends on `C` at runtime, but anything that - depends on `B` at compile-time has to recompile, and that includes `A`. + $ mix xref graph --format cycles --label compile-connected - Compile-time dependencies are typically added when using macros or - when invoking functions in the module body (outside of functions). - You can list all dependencies in a file by running - `mix xref trace path/to/file.ex`. This type of transitive compile-time - dependencies, such as `A` depending on `C` at compile-time through `B`, - can be found with the "compile-connected" label, as in - `mix xref graph --label compile-connected`. + Which may look like this: - Export dependencies are compile time dependencies on the module API, - namely structs and its public definitions. For example, if you import - a module but only use its functions, it is an export dependency. If - you use a struct, it is an export dependency too. Export dependencies - are only recompiled if the module API changes. Note, however, that compile - time dependencies have higher precedence than exports. Therefore if - you import a module and use its macros, it is a compile time dependency. + Cycle of length 3: - Runtime dependencies are added whenever you invoke another module - inside a function. Modules with runtime dependencies do not have - to be compiled when the callee changes, unless there is a transitive - compile or an outdated export time dependency between them. The option - `--label compile-connected` can be used to find the first case. + lib/c.ex + lib/b.ex (compile) + lib/a.ex + + This means `c.ex` depends on `b.ex` at compile time. Any compile dependency + in a cycle is by definition a compile-connected dependency, which must be + generally avoided, as explained earlier in the module documentation. ## Shared options @@ -846,44 +921,15 @@ true -> file_references end - # Filter according to non direct label - file_references = filter(file_references, filter) - - # If a label is given, remove empty root nodes - file_references = - if opts[:label] do - for {_, [_ | _]} = pair <- file_references, into: %{}, do: pair - else - file_references - end - - roots = - if sources do - Enum.map(sources, &{&1, nil}) - else - file_references - |> Map.drop(sinks || []) - |> Enum.map(&{elem(&1, 0), nil}) - end - - callback = fn {file, type} -> - children = if opts[:only_nodes], do: [], else: Map.get(file_references, file, []) - type = type && "(#{type})" - {{file, type}, Enum.sort(children)} - end - {found, count} = case opts[:format] do "dot" -> + {roots, callback, count} = + roots_and_callback(file_references, filter, sources, sinks, opts) + path = Keyword.get(opts, :output, "xref_graph.dot") - Mix.Utils.write_dot_graph!( - path, - "xref graph", - Enum.sort(roots), - callback, - opts - ) + Mix.Utils.write_dot_graph!(path, "xref graph", Enum.sort(roots), callback, opts) if path != "-" do png_path = (path |> Path.rootname() |> Path.basename()) <> ".png" @@ -899,19 +945,22 @@ |> Mix.shell().info() end - {:references, count_references(file_references)} + {:references, count} "stats" -> - print_stats(file_references, opts) + print_stats(file_references, filter, opts) {:stats, 0} "cycles" -> - {:cycles, print_cycles(file_references, opts)} + {:cycles, print_cycles(file_references, filter, opts)} other when other in [nil, "plain", "pretty"] -> + {roots, callback, count} = + roots_and_callback(file_references, filter, sources, sinks, opts) + Mix.Utils.print_tree(Enum.sort(roots), callback, opts) - {:references, count_references(file_references)} + {:references, count} other -> Mix.raise("Unknown --format #{other} in mix xref graph") @@ -920,28 +969,6 @@ check_failure(found, count, opts[:fail_above]) end - defp count_references(file_references) do - Enum.reduce(file_references, 0, fn {_, refs}, total -> total + length(refs) end) - end - - defp filter_fn(file_references, :compile_connected), - do: fn {key, type} -> - type == :compile and match?([_ | _], file_references[key] || []) - end - - defp filter_fn(_file_references, filter), - do: fn {_key, type} -> type == filter end - - defp filter(file_references, :all), do: file_references - - defp filter(file_references, filter) do - filter_fn = filter_fn(file_references, filter) - - for {key, children} <- file_references, - into: %{}, - do: {key, Enum.filter(children, filter_fn)} - end - defp source_tree(file_references, keys) do keys |> Enum.reduce({%{}, %{}}, fn key, {acc, seen} -> @@ -980,7 +1007,59 @@ end) end - defp print_stats(references, opts) do + defp roots_and_callback(file_references, filter, sources, sinks, opts) do + # Filter according to non direct label + file_references = transitive_filter(file_references, filter) + + # If a label is given, remove empty root nodes + file_references = + if opts[:label] do + for {_, [_ | _]} = pair <- file_references, into: %{}, do: pair + else + file_references + end + + roots = + if sources do + Enum.map(sources, &{&1, nil}) + else + file_references + |> Map.drop(sinks || []) + |> Enum.map(&{elem(&1, 0), nil}) + end + + callback = fn {file, type} -> + children = if opts[:only_nodes], do: [], else: Map.get(file_references, file, []) + type = type && "(#{type})" + {{file, type}, Enum.sort(children)} + end + + {roots, callback, count_references(file_references)} + end + + defp count_references(file_references) do + Enum.reduce(file_references, 0, fn {_, refs}, total -> total + length(refs) end) + end + + defp transitive_filter_fn(file_references, :compile_connected), + do: fn {key, type} -> + type == :compile and match?([_ | _], file_references[key] || []) + end + + defp transitive_filter_fn(_file_references, filter), + do: fn {_key, type} -> type == filter end + + defp transitive_filter(file_references, :all), do: file_references + + defp transitive_filter(file_references, filter) do + filter_fn = transitive_filter_fn(file_references, filter) + + for {key, children} <- file_references, + into: %{}, + do: {key, Enum.filter(children, filter_fn)} + end + + defp print_stats(references, filter, opts) do with_digraph(references, fn graph -> shell = Mix.shell() @@ -995,11 +1074,11 @@ shell.info("Compile dependencies: #{counters.compile} (edges)") shell.info("Exports dependencies: #{counters.export} (edges)") shell.info("Runtime dependencies: #{counters.nil} (edges)") - shell.info("Cycles: #{length(cycles(graph, opts))}") + shell.info("Cycles: #{length(cycles(graph, filter, opts))}") outgoing = references - |> Enum.map(fn {file, _} -> {:digraph.out_degree(graph, file), file} end) + |> Enum.map(fn {file, _} -> {out_stats_filter(references, graph, file, filter), file} end) |> Enum.sort(:desc) |> Enum.take(10) @@ -1008,7 +1087,7 @@ incoming = references - |> Enum.map(fn {file, _} -> {:digraph.in_degree(graph, file), file} end) + |> Enum.map(fn {file, _} -> {in_stats_filter(references, graph, file, filter), file} end) |> Enum.sort(:desc) |> Enum.take(10) @@ -1017,6 +1096,32 @@ end) end + defp out_stats_filter(_references, graph, file, :all), do: :digraph.out_degree(graph, file) + + defp out_stats_filter(references, graph, file, filter) do + filter_fn = transitive_filter_fn(references, filter) + + graph + |> :digraph.out_neighbours(file) + |> Enum.count(fn v -> + {_edge, _v1, _v2, label} = :digraph.edge(graph, {file, v}) + filter_fn.({file, label}) + end) + end + + defp in_stats_filter(_references, graph, file, :all), do: :digraph.in_degree(graph, file) + + defp in_stats_filter(references, graph, file, filter) do + filter_fn = transitive_filter_fn(references, filter) + + graph + |> :digraph.in_neighbours(file) + |> Enum.count(fn v -> + {_edge, _v1, _v2, label} = :digraph.edge(graph, {v, file}) + filter_fn.({file, label}) + end) + end + defp with_digraph(references, callback) do graph = :digraph.new() @@ -1026,7 +1131,7 @@ end for {file, deps} <- references, {dep, label} <- deps do - :digraph.add_edge(graph, file, dep, label) + :digraph.add_edge(graph, {file, dep}, file, dep, label) end callback.(graph) @@ -1035,7 +1140,7 @@ end end - defp cycles(graph, opts) do + defp cycles(graph, filter, opts) do # Vertices order in cyclic_strong_components/1 return is arbitrary and changes between # OTP versions, sorting is necessary to make the output stable across versions. cycles = @@ -1044,21 +1149,47 @@ |> Enum.reduce([], &inner_cycles(graph, Enum.sort(&1), &2)) |> Enum.map(&{length(&1), &1}) - if min = opts[:min_cycle_size], do: Enum.filter(cycles, &(elem(&1, 0) > min)), else: cycles + cycles = + if min = opts[:min_cycle_size] do + Enum.filter(cycles, &(elem(&1, 0) > min)) + else + cycles + end + + # :compile_connected is the same + if cycle_fn = cycle_filter_fn(filter) do + Enum.filter(cycles, fn {_length, cycle} -> Enum.any?(cycle, cycle_fn) end) + else + cycles + end end + # In cycles, a compile connected is compile + defp cycle_filter_fn(:all), do: nil + defp cycle_filter_fn(:compile_connected), do: cycle_filter_fn(:compile) + defp cycle_filter_fn(filter), do: fn {_node, type} -> type == filter end + defp inner_cycles(_graph, [], acc), do: acc defp inner_cycles(graph, [v | vertices], acc) do cycle = :digraph.get_cycle(graph, v) - inner_cycles(graph, vertices -- cycle, [cycle | acc]) + inner_cycles(graph, vertices -- cycle, [label_cycle(cycle, graph) | acc]) + end + + defp label_cycle([from, to | cycle], graph) do + {_edge, _v1, _v2, label} = :digraph.edge(graph, {from, to}) + [{to, label} | label_cycle([to | cycle], graph)] + end + + defp label_cycle([_from], _graph) do + [] end - defp print_cycles(references, opts) do + defp print_cycles(references, filter, opts) do with_digraph(references, fn graph -> shell = Mix.shell() - case graph |> cycles(opts) |> Enum.sort(:desc) do + case graph |> cycles(filter, opts) |> Enum.sort(:desc) do [] -> shell.info("No cycles found") 0 @@ -1069,8 +1200,9 @@ for {length, cycle} <- cycles do shell.info("Cycle of length #{length}:\n") - for node <- cycle do - shell.info(" " <> node) + for {node, type} <- cycle do + type = if type, do: " (#{type})", else: "" + shell.info(" " <> node <> type) end shell.info("") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.17.2/lib/mix/test/mix/tasks/xref_test.exs new/elixir-1.17.3/lib/mix/test/mix/tasks/xref_test.exs --- old/elixir-1.17.2/lib/mix/test/mix/tasks/xref_test.exs 2024-07-06 23:10:25.000000000 +0200 +++ new/elixir-1.17.3/lib/mix/test/mix/tasks/xref_test.exs 2024-09-18 14:33:39.000000000 +0200 @@ -439,14 +439,85 @@ """) end + test "stats with compile label" do + assert_graph(["--format", "stats", "--label", "compile"], """ + Tracked files: 5 (nodes) + Compile dependencies: 3 (edges) + Exports dependencies: 0 (edges) + Runtime dependencies: 3 (edges) + Cycles: 1 + + Top 5 files with most outgoing dependencies: + * lib/c.ex (1) + * lib/b.ex (1) + * lib/a.ex (1) + * lib/e.ex (0) + * lib/d.ex (0) + + Top 5 files with most incoming dependencies: + * lib/e.ex (1) + * lib/d.ex (1) + * lib/b.ex (1) + * lib/c.ex (0) + * lib/a.ex (0) + """) + end + + test "stats with compile-connected label" do + assert_graph(["--format", "stats", "--label", "compile-connected"], """ + Tracked files: 5 (nodes) + Compile dependencies: 3 (edges) + Exports dependencies: 0 (edges) + Runtime dependencies: 3 (edges) + Cycles: 1 + + Top 5 files with most outgoing dependencies: + * lib/c.ex (1) + * lib/b.ex (1) + * lib/a.ex (1) + * lib/e.ex (0) + * lib/d.ex (0) + + Top 5 files with most incoming dependencies: + * lib/d.ex (1) + * lib/b.ex (1) + * lib/e.ex (0) + * lib/c.ex (0) + * lib/a.ex (0) + """) + end + test "cycles" do assert_graph(["--format", "cycles"], """ 1 cycles found. Showing them in decreasing size: - Cycle of length 3: + Cycle of length 2: + lib/b.ex (compile) lib/a.ex - lib/b.ex + + """) + end + + test "cycles with compile label require at least one of such type" do + assert_graph(["--format", "cycles", "--label", "compile"], """ + 1 cycles found. Showing them in decreasing size: + + Cycle of length 2: + + lib/b.ex (compile) + lib/a.ex + + """) + end + + test "cycles with compile-connected label is the same as compile" do + assert_graph(["--format", "cycles", "--label", "compile-connected"], """ + 1 cycles found. Showing them in decreasing size: + + Cycle of length 2: + + lib/b.ex (compile) lib/a.ex """)
