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
 
       """)

Reply via email to