Skip to content

Commit 0bc60b1

Browse files
committed
improvement: add and manage a module index cache
1 parent 193509a commit 0bc60b1

3 files changed

Lines changed: 450 additions & 39 deletions

File tree

lib/igniter.ex

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,17 +184,41 @@ defmodule Igniter do
184184

185185
source = Rewrite.source!(igniter.rewrite, from)
186186

187-
if Rewrite.Source.from?(source, :string) do
188-
rewrite =
189-
igniter.rewrite
190-
|> Rewrite.drop([source.path])
191-
|> Rewrite.put!(%{source | path: to})
187+
igniter =
188+
if Rewrite.Source.from?(source, :string) do
189+
rewrite =
190+
igniter.rewrite
191+
|> Rewrite.drop([source.path])
192+
|> Rewrite.put!(%{source | path: to})
193+
194+
%{igniter | rewrite: rewrite}
195+
else
196+
%{igniter | moves: Map.put(igniter.moves, from, to)}
197+
end
192198

193-
%{igniter | rewrite: rewrite}
194-
else
195-
%{igniter | moves: Map.put(igniter.moves, from, to)}
196-
end
199+
update_module_index_path(igniter, from, to)
200+
end
201+
end
202+
end
203+
204+
defp update_module_index_path(igniter, from, to) do
205+
case get_in(igniter.assigns, [:private, :module_index]) do
206+
index when is_map(index) and map_size(index) > 0 ->
207+
updated =
208+
Map.new(index, fn
209+
{mod, ^from} -> {mod, to}
210+
entry -> entry
211+
end)
212+
213+
if updated == index do
214+
igniter
215+
else
216+
private = Map.put(igniter.assigns[:private] || %{}, :module_index, updated)
217+
%{igniter | assigns: Map.put(igniter.assigns, :private, private)}
197218
end
219+
220+
_ ->
221+
igniter
198222
end
199223
end
200224

lib/igniter/project/module.ex

Lines changed: 253 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,12 @@ defmodule Igniter.Project.Module do
165165
path
166166
end
167167

168-
Igniter.create_new_file(igniter, location, contents)
168+
igniter
169+
|> Igniter.create_new_file(location, contents)
170+
|> put_module_index(
171+
module_name,
172+
Igniter.Util.BackwardsCompat.relative_to_cwd(location, force: true)
173+
)
169174
end
170175

171176
@doc "Checks if a module is defined somewhere in the project. The returned igniter should not be discarded."
@@ -291,7 +296,37 @@ defmodule Igniter.Project.Module do
291296
@spec find_module(Igniter.t(), module()) ::
292297
{:ok, {Igniter.t(), Rewrite.Source.t(), Zipper.t()}} | {:error, Igniter.t()}
293298
def find_module(igniter, module_name) do
294-
check_first =
299+
with {:miss, igniter} <- check_module_index(igniter, module_name),
300+
{:miss, igniter} <- try_compiled_source(igniter, module_name),
301+
{:miss, igniter} <- try_conventional_path(igniter, module_name),
302+
{:miss, igniter, searched} <- try_filename_match(igniter, module_name),
303+
{:miss, igniter, searched} <- try_directory_search(igniter, module_name, searched),
304+
{:miss, igniter} <- try_full_scan(igniter, module_name, searched) do
305+
{:error, igniter}
306+
else
307+
{:ok, {igniter, source, zipper}} ->
308+
{:ok, {put_module_index(igniter, module_name, source.path), source, zipper}}
309+
end
310+
end
311+
312+
defp check_module_index(igniter, module_name) do
313+
case get_in(igniter.assigns, [:private, :module_index, module_name]) do
314+
path when is_binary(path) ->
315+
case check_file_for_module(igniter, path, module_name) do
316+
{:ok, {igniter, source, zipper}} ->
317+
{:ok, {log_find_module_strategy(igniter, module_name, :module_index), source, zipper}}
318+
319+
_ ->
320+
{:miss, igniter}
321+
end
322+
323+
_ ->
324+
{:miss, igniter}
325+
end
326+
end
327+
328+
defp try_compiled_source(igniter, module_name) do
329+
path =
295330
with true <- Code.ensure_loaded?(module_name),
296331
source when not is_nil(source) <- module_name.module_info()[:compile][:source],
297332
path =
@@ -302,38 +337,226 @@ defmodule Igniter.Project.Module do
302337
_ -> nil
303338
end
304339

305-
with check_first when not is_nil(check_first) <- check_first,
306-
igniter <- Igniter.include_existing_file(igniter, check_first),
307-
{:ok, source} <- Rewrite.source(igniter.rewrite, check_first),
308-
{:ok, zipper} <-
309-
source
340+
case path do
341+
nil ->
342+
{:miss, igniter}
343+
344+
path ->
345+
case check_file_for_module(igniter, path, module_name) do
346+
{:ok, {igniter, source, zipper}} ->
347+
{:ok,
348+
{log_find_module_strategy(igniter, module_name, :compiled_source), source, zipper}}
349+
350+
_ ->
351+
{:miss, igniter}
352+
end
353+
end
354+
end
355+
356+
defp try_conventional_path(igniter, module_name) do
357+
paths = conventional_paths(igniter, module_name)
358+
359+
Enum.reduce_while(paths, {:miss, igniter}, fn path, {:miss, igniter} ->
360+
case check_file_for_module(igniter, path, module_name) do
361+
{:ok, {igniter, source, zipper}} ->
362+
{:halt,
363+
{:ok,
364+
{log_find_module_strategy(igniter, module_name, :conventional_path), source, zipper}}}
365+
366+
{:miss, igniter} ->
367+
{:cont, {:miss, igniter}}
368+
end
369+
end)
370+
end
371+
372+
defp try_filename_match(igniter, module_name) do
373+
last_segment =
374+
module_name
375+
|> Module.split()
376+
|> List.last()
377+
|> to_string()
378+
|> Macro.underscore()
379+
380+
source_folders = Igniter.Project.IgniterConfig.get(igniter, :source_folders)
381+
382+
igniter =
383+
Enum.reduce(source_folders, igniter, fn source_folder, igniter ->
384+
Igniter.include_glob(igniter, Path.join(source_folder, "**/#{last_segment}.{ex,exs}"))
385+
end)
386+
387+
igniter = Igniter.include_glob(igniter, "test/**/#{last_segment}.{ex,exs}")
388+
389+
igniter.rewrite
390+
|> Enum.filter(fn source ->
391+
match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, source) and
392+
Path.basename(source.path, Path.extname(source.path)) == last_segment
393+
end)
394+
|> search_sources_for_module(igniter, module_name)
395+
|> case do
396+
{:ok, {igniter, source, zipper}} ->
397+
{:ok, {log_find_module_strategy(igniter, module_name, :filename_match), source, zipper}}
398+
399+
miss ->
400+
miss
401+
end
402+
end
403+
404+
defp try_directory_search(igniter, module_name, searched) do
405+
segments =
406+
module_name
407+
|> Module.split()
408+
|> Enum.map(fn s -> s |> to_string() |> Macro.underscore() end)
409+
|> Enum.reverse()
410+
|> Enum.drop(1)
411+
412+
source_folders = Igniter.Project.IgniterConfig.get(igniter, :source_folders)
413+
414+
Enum.reduce_while(segments, {:miss, igniter, searched}, fn segment,
415+
{:miss, igniter, searched} ->
416+
igniter =
417+
Enum.reduce(source_folders, igniter, fn source_folder, igniter ->
418+
Igniter.include_glob(
419+
igniter,
420+
Path.join(source_folder, "**/#{segment}/**/*.{ex,exs}")
421+
)
422+
end)
423+
424+
igniter.rewrite
425+
|> Enum.filter(fn source ->
426+
match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, source) and
427+
not MapSet.member?(searched, source.path) and
428+
String.contains?(source.path, "/#{segment}/")
429+
end)
430+
|> search_sources_for_module(igniter, module_name, searched)
431+
|> case do
432+
{:ok, {igniter, source, zipper}} ->
433+
{:halt,
434+
{:ok,
435+
{log_find_module_strategy(igniter, module_name, :directory_search), source, zipper}}}
436+
437+
{:miss, igniter, searched} ->
438+
{:cont, {:miss, igniter, searched}}
439+
end
440+
end)
441+
end
442+
443+
defp try_full_scan(igniter, module_name, searched) do
444+
igniter = Igniter.include_all_elixir_files(igniter)
445+
446+
igniter
447+
|> Map.get(:rewrite)
448+
|> Enum.filter(fn source ->
449+
match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, source) and
450+
not MapSet.member?(searched, source.path)
451+
end)
452+
|> Task.async_stream(
453+
fn source ->
454+
{source
455+
|> Rewrite.Source.get(:quoted)
456+
|> Zipper.zip()
457+
|> Igniter.Code.Module.move_to_defmodule(module_name), source}
458+
end,
459+
timeout: :infinity
460+
)
461+
|> Enum.find_value(fn
462+
{:ok, {{:ok, zipper}, source}} ->
463+
{:ok, {igniter, source, zipper}}
464+
465+
_other ->
466+
false
467+
end)
468+
|> case do
469+
{:ok, {igniter, source, zipper}} ->
470+
{:ok, {log_find_module_strategy(igniter, module_name, :full_scan), source, zipper}}
471+
472+
nil ->
473+
{:miss, igniter}
474+
end
475+
end
476+
477+
defp check_file_for_module(igniter, path, module_name) do
478+
igniter = Igniter.include_existing_file(igniter, path)
479+
480+
case Rewrite.source(igniter.rewrite, path) do
481+
{:ok, source} ->
482+
case source
483+
|> Rewrite.Source.get(:quoted)
484+
|> Zipper.zip()
485+
|> Igniter.Code.Module.move_to_defmodule(module_name) do
486+
{:ok, zipper} -> {:ok, {igniter, source, zipper}}
487+
_ -> {:miss, igniter}
488+
end
489+
490+
_ ->
491+
{:miss, igniter}
492+
end
493+
end
494+
495+
defp search_sources_for_module(sources, igniter, module_name, searched \\ MapSet.new()) do
496+
Enum.reduce_while(sources, {:miss, igniter, searched}, fn source,
497+
{:miss, igniter, searched} ->
498+
searched = MapSet.put(searched, source.path)
499+
500+
case source
310501
|> Rewrite.Source.get(:quoted)
311502
|> Zipper.zip()
312503
|> Igniter.Code.Module.move_to_defmodule(module_name) do
313-
{:ok, {igniter, source, zipper}}
314-
else
315-
_ ->
316-
igniter = Igniter.include_all_elixir_files(igniter)
504+
{:ok, zipper} ->
505+
{:halt, {:ok, {igniter, source, zipper}}}
317506

318-
igniter
319-
|> Map.get(:rewrite)
320-
|> Enum.filter(&match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, &1))
321-
|> Task.async_stream(
322-
fn source ->
323-
{source
324-
|> Rewrite.Source.get(:quoted)
325-
|> Zipper.zip()
326-
|> Igniter.Code.Module.move_to_defmodule(module_name), source}
327-
end,
328-
timeout: :infinity
329-
)
330-
|> Enum.find_value({:error, igniter}, fn
331-
{:ok, {{:ok, zipper}, source}} ->
332-
{:ok, {igniter, source, zipper}}
333-
334-
_other ->
335-
false
336-
end)
507+
_ ->
508+
{:cont, {:miss, igniter, searched}}
509+
end
510+
end)
511+
end
512+
513+
defp conventional_paths(igniter, module_name) do
514+
segments =
515+
case Module.split(module_name) do
516+
["Mix", "Tasks" | rest] ->
517+
suffix =
518+
rest
519+
|> Enum.map(&to_string/1)
520+
|> Enum.map_join(".", &Macro.underscore/1)
521+
522+
["mix", "tasks", suffix]
523+
524+
_other ->
525+
module_name
526+
|> Module.split()
527+
|> Enum.map(&to_string/1)
528+
|> Enum.map(&Macro.underscore/1)
529+
end
530+
531+
last = List.last(segments)
532+
leading = :lists.droplast(segments)
533+
534+
source_folders = Igniter.Project.IgniterConfig.get(igniter, :source_folders)
535+
536+
standard = for sf <- source_folders, do: Path.join([sf | leading] ++ ["#{last}.ex"])
537+
538+
inside_folder =
539+
for sf <- source_folders, do: Path.join([sf | leading] ++ [last, "#{last}.ex"])
540+
541+
standard ++ inside_folder
542+
end
543+
544+
defp put_module_index(igniter, module_name, path) do
545+
private = igniter.assigns[:private] || %{}
546+
module_index = Map.get(private, :module_index, %{})
547+
module_index = Map.put(module_index, module_name, path)
548+
private = Map.put(private, :module_index, module_index)
549+
%{igniter | assigns: Map.put(igniter.assigns, :private, private)}
550+
end
551+
552+
defp log_find_module_strategy(igniter, module_name, strategy) do
553+
if igniter.assigns[:test_mode?] do
554+
private = igniter.assigns[:private] || %{}
555+
log = Map.get(private, :find_module_log, [])
556+
private = Map.put(private, :find_module_log, log ++ [{module_name, strategy}])
557+
%{igniter | assigns: Map.put(igniter.assigns, :private, private)}
558+
else
559+
igniter
337560
end
338561
end
339562

0 commit comments

Comments
 (0)