Skip to content

Commit 0e12228

Browse files
committed
improvement: address review feedback on rename_module
Use find_module/2 and AST-based multi-alias detection instead of string-matching helpers. Fix an ordering bug where the AST pass ran before the string replace (Example → NewExample → NewNewExample). Add import/require tests.
1 parent ce19337 commit 0e12228

2 files changed

Lines changed: 59 additions & 66 deletions

File tree

lib/igniter/refactors/rename.ex

Lines changed: 33 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,17 @@ defmodule Igniter.Refactors.Rename do
6060

6161
igniter = Igniter.include_all_elixir_files(igniter)
6262

63-
old_path =
64-
find_module_file(igniter, old_module_str) ||
65-
Igniter.Project.Module.proper_location(igniter, old_module)
63+
{old_path, igniter} =
64+
case Igniter.Project.Module.find_module(igniter, old_module) do
65+
{:ok, {igniter, source, _zipper}} ->
66+
{Rewrite.Source.get(source, :path), igniter}
67+
68+
{:error, igniter} ->
69+
{Igniter.Project.Module.proper_location(igniter, old_module), igniter}
70+
end
6671

6772
new_path = Igniter.Project.Module.proper_location(igniter, new_module)
68-
affected_files = find_affected_files(igniter, old_module_str, old_parts)
73+
affected_files = find_affected_files(igniter, old_module_str, old_aliases, old_short)
6974

7075
igniter
7176
|> rewrite_affected_files(
@@ -935,51 +940,29 @@ defmodule Igniter.Refactors.Rename do
935940

936941
## rename_module helpers
937942

938-
defp find_module_file(igniter, module_str) do
939-
igniter.rewrite
940-
|> Rewrite.sources()
941-
|> Enum.find_value(fn source ->
942-
path = Rewrite.Source.get(source, :path)
943-
content = Rewrite.Source.get(source, :content)
944-
945-
if String.ends_with?(path, [".ex", ".exs"]) &&
946-
String.contains?(content, "defmodule #{module_str}"),
947-
do: path
948-
end)
949-
end
943+
# Substring check catches most references; AST fallback catches
944+
# `alias Ns.{..., Short, ...}` where the full name never appears literally.
945+
defp find_affected_files(igniter, old_module_str, old_aliases, old_short) do
946+
namespace_segs = Enum.drop(old_aliases, -1)
947+
multi_alias_prefix = Enum.join(namespace_segs, ".") <> ".{"
950948

951-
defp find_affected_files(igniter, module_str, old_parts) do
952949
igniter.rewrite
953950
|> Rewrite.sources()
954951
|> Enum.filter(fn source ->
955952
path = Rewrite.Source.get(source, :path)
956953
content = Rewrite.Source.get(source, :content)
957954

958955
String.ends_with?(path, [".ex", ".exs"]) &&
959-
(String.contains?(content, module_str) ||
960-
multi_alias_in_content?(content, old_parts))
956+
(String.contains?(content, old_module_str) ||
957+
(namespace_segs != [] && String.contains?(content, multi_alias_prefix) &&
958+
source
959+
|> Rewrite.Source.get(:quoted)
960+
|> Zipper.zip()
961+
|> has_multi_alias_for?(namespace_segs, old_short)))
961962
end)
962963
|> Enum.map(&Rewrite.Source.get(&1, :path))
963964
end
964965

965-
# Detects `alias Foo.{Bar, Baz}` style references where the full module name
966-
# (e.g. "Foo.Bar") is not present as a substring. Checks for the namespace
967-
# prefix followed by ".{" and the short segment name anywhere in the content.
968-
# This is a heuristic and may include false positives, but the subsequent AST
969-
# passes are no-ops when nothing actually matches.
970-
defp multi_alias_in_content?(content, old_parts) do
971-
case Enum.split(old_parts, -1) do
972-
{[], _} ->
973-
false
974-
975-
{namespace_parts, [short]} ->
976-
namespace_str = Enum.join(namespace_parts, ".")
977-
978-
String.contains?(content, "#{namespace_str}.{") &&
979-
String.contains?(content, short)
980-
end
981-
end
982-
983966
defp rewrite_affected_files(
984967
igniter,
985968
files,
@@ -991,31 +974,22 @@ defmodule Igniter.Refactors.Rename do
991974
new_str
992975
) do
993976
Enum.reduce(files, igniter, fn path, igniter ->
977+
# String replace must run before the AST pass, or "Example" inside
978+
# a freshly-rendered "NewExample" gets replaced again → "NewNewExample".
994979
igniter
980+
|> replace_module_strings_in_file(path, old_str, new_str)
995981
|> Igniter.update_elixir_file(path, fn zipper ->
996982
zipper
997-
# Short-form rename must run first: expand_alias reads the original
998-
# alias declarations, which apply_module_renames would overwrite afterward.
999983
|> rename_aliased_short_forms(old_aliases, old_short, new_short)
1000984
|> apply_module_renames(renames)
1001985
|> then(&{:ok, &1})
1002986
end)
1003-
|> replace_module_strings_in_file(path, old_str, new_str)
1004987
end)
1005988
end
1006989

1007-
# Renames short-form call sites (e.g. Bar.foo()) that resolve to old_aliases
1008-
# via an alias declaration. Uses Igniter's expand_alias, which delegates to
1009-
# Macro.Env.expand_alias/3 — the Elixir compiler itself. This correctly
1010-
# handles plain `alias Foo.Bar` and `alias Foo.Bar, as: B` (the last form is
1011-
# deliberately excluded: B.foo() stays B.foo() since the as: clause is
1012-
# preserved by apply_module_renames).
1013-
#
1014-
# For multi-alias (`alias Foo.{Bar, Baz}`): Spitfire does not macro-expand
1015-
# this form, so expand_alias cannot resolve the short name. We fall back to
1016-
# an explicit AST scan via has_multi_alias_for?. The fallback also renames the
1017-
# short name inside the declaration itself ([:Bar] → [:Baz] inside the braces),
1018-
# which is correct because apply_module_renames only matches full alias lists.
990+
# Renames short-form call sites that resolve via `alias Foo.Bar` (plain or
991+
# multi-alias). `alias Foo.Bar, as: B` is intentionally left alone — the
992+
# as: clause keeps resolving after apply_module_renames updates the target.
1019993
defp rename_aliased_short_forms(zipper, old_aliases, old_short, new_short) do
1020994
namespace_segs = Enum.drop(old_aliases, -1)
1021995
has_multi = has_multi_alias_for?(Zipper.top(zipper), namespace_segs, old_short)
@@ -1029,8 +1003,6 @@ defmodule Igniter.Refactors.Rename do
10291003
true
10301004

10311005
_ ->
1032-
# Fallback for multi-alias: if the file declares
1033-
# alias Ns.{..., OldShort, ...} we rename by convention.
10341006
has_multi
10351007
end
10361008

@@ -1046,8 +1018,7 @@ defmodule Igniter.Refactors.Rename do
10461018
end
10471019
end
10481020

1049-
# Returns true when the zipper contains a multi-alias declaration of the form
1050-
# alias <namespace_segs>.{..., <old_short>, ...}
1021+
# Matches `alias <namespace_segs>.{..., <old_short>, ...}`.
10511022
defp has_multi_alias_for?(zipper, namespace_segs, old_short) do
10521023
Zipper.find(zipper, fn
10531024
{:alias, _, [{{:., _, [{:__aliases__, _, ^namespace_segs}, :{}]}, _, short_nodes}]} ->
@@ -1089,8 +1060,8 @@ defmodule Igniter.Refactors.Rename do
10891060
end
10901061

10911062
defp move_submodule_files(igniter, old_path, new_path) do
1092-
old_dir = module_file_dir(old_path)
1093-
new_dir = module_file_dir(new_path)
1063+
old_dir = Path.rootname(old_path) <> "/"
1064+
new_dir = Path.rootname(new_path) <> "/"
10941065

10951066
igniter.rewrite
10961067
|> Rewrite.sources()
@@ -1102,17 +1073,13 @@ defmodule Igniter.Refactors.Rename do
11021073
end)
11031074
end
11041075

1105-
defp module_file_dir(path) do
1106-
path
1107-
|> String.replace_suffix(".exs", "")
1108-
|> String.replace_suffix(".ex", "")
1109-
|> Kernel.<>("/")
1110-
end
1111-
11121076
defp move_module_file(igniter, same, same), do: igniter
11131077

11141078
defp move_module_file(igniter, old_path, new_path) do
1115-
Igniter.move_file(igniter, old_path, new_path)
1079+
case Rewrite.source(igniter.rewrite, old_path) do
1080+
{:ok, _} -> Igniter.move_file(igniter, old_path, new_path)
1081+
{:error, _} -> igniter
1082+
end
11161083
end
11171084

11181085
defp module_aliases_node_matches?({:__aliases__, _meta, aliases}, target_aliases) do

test/igniter/refactors/renames_test.exs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,32 @@ defmodule Igniter.Refactors.RenameTest do
357357
|> assert_has_patch("lib/consumer.ex", "+ | use NewExample")
358358
end
359359

360+
test "updates import in another file" do
361+
test_project()
362+
|> Igniter.create_new_file("lib/example.ex", "defmodule Example do\nend\n")
363+
|> Igniter.create_new_file("lib/consumer.ex", """
364+
defmodule Consumer do
365+
import Example
366+
end
367+
""")
368+
|> apply_igniter!()
369+
|> Igniter.Refactors.Rename.rename_module(Example, NewExample)
370+
|> assert_has_patch("lib/consumer.ex", "+ | import NewExample")
371+
end
372+
373+
test "updates require in another file" do
374+
test_project()
375+
|> Igniter.create_new_file("lib/example.ex", "defmodule Example do\nend\n")
376+
|> Igniter.create_new_file("lib/consumer.ex", """
377+
defmodule Consumer do
378+
require Example
379+
end
380+
""")
381+
|> apply_igniter!()
382+
|> Igniter.Refactors.Rename.rename_module(Example, NewExample)
383+
|> assert_has_patch("lib/consumer.ex", "+ | require NewExample")
384+
end
385+
360386
test "renames a submodule along with the parent" do
361387
test_project()
362388
|> Igniter.create_new_file("lib/example.ex", "defmodule Example do\nend\n")

0 commit comments

Comments
 (0)