Skip to content

Commit d642b2a

Browse files
committed
improvement: check compiled manifest and simple defmodule patterns early
1 parent 0bc60b1 commit d642b2a

1 file changed

Lines changed: 211 additions & 41 deletions

File tree

lib/igniter/project/module.ex

Lines changed: 211 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,29 @@ defmodule Igniter.Project.Module do
296296
@spec find_module(Igniter.t(), module()) ::
297297
{:ok, {Igniter.t(), Rewrite.Source.t(), Zipper.t()}} | {:error, Igniter.t()}
298298
def find_module(igniter, module_name) do
299+
igniter = ensure_module_index_initialized(igniter)
300+
301+
manifest_known = get_in(igniter.assigns, [:private, :manifest_known_files]) || MapSet.new()
302+
303+
manifest_searched =
304+
if MapSet.size(manifest_known) == 0 do
305+
manifest_known
306+
else
307+
modified_paths =
308+
igniter.rewrite
309+
|> Enum.filter(fn source ->
310+
MapSet.member?(manifest_known, source.path) and
311+
(Rewrite.Source.updated?(source) or Rewrite.Source.from?(source, :string))
312+
end)
313+
|> MapSet.new(& &1.path)
314+
315+
MapSet.difference(manifest_known, modified_paths)
316+
end
317+
299318
with {:miss, igniter} <- check_module_index(igniter, module_name),
300319
{:miss, igniter} <- try_compiled_source(igniter, module_name),
301320
{:miss, igniter} <- try_conventional_path(igniter, module_name),
302-
{:miss, igniter, searched} <- try_filename_match(igniter, module_name),
321+
{:miss, igniter, searched} <- try_filename_match(igniter, module_name, manifest_searched),
303322
{:miss, igniter, searched} <- try_directory_search(igniter, module_name, searched),
304323
{:miss, igniter} <- try_full_scan(igniter, module_name, searched) do
305324
{:error, igniter}
@@ -312,12 +331,24 @@ defmodule Igniter.Project.Module do
312331
defp check_module_index(igniter, module_name) do
313332
case get_in(igniter.assigns, [:private, :module_index, module_name]) do
314333
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}}
334+
igniter = Igniter.include_existing_file(igniter, path)
335+
336+
case Rewrite.source(igniter.rewrite, path) do
337+
{:ok, source} ->
338+
case source
339+
|> Rewrite.Source.get(:quoted)
340+
|> Zipper.zip()
341+
|> Igniter.Code.Module.move_to_defmodule(module_name) do
342+
{:ok, zipper} ->
343+
{:ok,
344+
{log_find_module_strategy(igniter, module_name, :module_index), source, zipper}}
345+
346+
_ ->
347+
{:miss, evict_module_index(igniter, module_name)}
348+
end
318349

319350
_ ->
320-
{:miss, igniter}
351+
{:miss, evict_module_index(igniter, module_name)}
321352
end
322353

323354
_ ->
@@ -369,7 +400,7 @@ defmodule Igniter.Project.Module do
369400
end)
370401
end
371402

372-
defp try_filename_match(igniter, module_name) do
403+
defp try_filename_match(igniter, module_name, searched) do
373404
last_segment =
374405
module_name
375406
|> Module.split()
@@ -388,10 +419,10 @@ defmodule Igniter.Project.Module do
388419

389420
igniter.rewrite
390421
|> Enum.filter(fn source ->
391-
match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, source) and
422+
searchable_source?(source, module_name, searched) and
392423
Path.basename(source.path, Path.extname(source.path)) == last_segment
393424
end)
394-
|> search_sources_for_module(igniter, module_name)
425+
|> search_sources_for_module(igniter, module_name, searched)
395426
|> case do
396427
{:ok, {igniter, source, zipper}} ->
397428
{:ok, {log_find_module_strategy(igniter, module_name, :filename_match), source, zipper}}
@@ -423,8 +454,7 @@ defmodule Igniter.Project.Module do
423454

424455
igniter.rewrite
425456
|> Enum.filter(fn source ->
426-
match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, source) and
427-
not MapSet.member?(searched, source.path) and
457+
searchable_source?(source, module_name, searched) and
428458
String.contains?(source.path, "/#{segment}/")
429459
end)
430460
|> search_sources_for_module(igniter, module_name, searched)
@@ -443,29 +473,55 @@ defmodule Igniter.Project.Module do
443473
defp try_full_scan(igniter, module_name, searched) do
444474
igniter = Igniter.include_all_elixir_files(igniter)
445475

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}}
476+
to_search =
477+
igniter
478+
|> Map.get(:rewrite)
479+
|> Enum.filter(&searchable_source?(&1, module_name, searched))
464480

465-
_other ->
466-
false
467-
end)
468-
|> case do
481+
{string_matches, rest} =
482+
Enum.split_with(to_search, &source_might_define_module?(&1, module_name))
483+
484+
result =
485+
string_matches
486+
|> Task.async_stream(
487+
fn source ->
488+
{source
489+
|> Rewrite.Source.get(:quoted)
490+
|> Zipper.zip()
491+
|> Igniter.Code.Module.move_to_defmodule(module_name), source}
492+
end,
493+
timeout: :infinity
494+
)
495+
|> Enum.find_value(fn
496+
{:ok, {{:ok, zipper}, source}} ->
497+
{:ok, {igniter, source, zipper}}
498+
499+
_other ->
500+
false
501+
end)
502+
503+
result =
504+
result ||
505+
rest
506+
|> Enum.filter(&multiple_defmodules?/1)
507+
|> Task.async_stream(
508+
fn source ->
509+
{source
510+
|> Rewrite.Source.get(:quoted)
511+
|> Zipper.zip()
512+
|> Igniter.Code.Module.move_to_defmodule(module_name), source}
513+
end,
514+
timeout: :infinity
515+
)
516+
|> Enum.find_value(fn
517+
{:ok, {{:ok, zipper}, source}} ->
518+
{:ok, {igniter, source, zipper}}
519+
520+
_other ->
521+
false
522+
end)
523+
524+
case result do
469525
{:ok, {igniter, source, zipper}} ->
470526
{:ok, {log_find_module_strategy(igniter, module_name, :full_scan), source, zipper}}
471527

@@ -474,6 +530,31 @@ defmodule Igniter.Project.Module do
474530
end
475531
end
476532

533+
defp source_might_define_module?(source, module_name) do
534+
content = Rewrite.Source.get(source, :content)
535+
String.contains?(content, "defmodule #{inspect(module_name)} do")
536+
end
537+
538+
defp multiple_defmodules?(source) do
539+
content = Rewrite.Source.get(source, :content)
540+
541+
case :binary.match(content, "defmodule ") do
542+
:nomatch ->
543+
false
544+
545+
{pos, len} ->
546+
:binary.match(content, "defmodule ", scope: {pos + len, byte_size(content) - pos - len}) !=
547+
:nomatch
548+
end
549+
end
550+
551+
defp searchable_source?(source, module_name, searched) do
552+
match?(%Rewrite.Source{filetype: %Rewrite.Source.Ex{}}, source) and
553+
not MapSet.member?(searched, source.path) and
554+
(not String.ends_with?(source.path, "_test.exs") or
555+
String.ends_with?(inspect(module_name), "Test"))
556+
end
557+
477558
defp check_file_for_module(igniter, path, module_name) do
478559
igniter = Igniter.include_existing_file(igniter, path)
479560

@@ -492,20 +573,35 @@ defmodule Igniter.Project.Module do
492573
end
493574
end
494575

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)
576+
defp search_sources_for_module(sources, igniter, module_name, searched) do
577+
searched = Enum.reduce(sources, searched, fn source, acc -> MapSet.put(acc, source.path) end)
578+
579+
{string_matches, rest} =
580+
Enum.split_with(sources, &source_might_define_module?(&1, module_name))
581+
582+
case zipper_search(string_matches, module_name) do
583+
{:ok, {source, zipper}} ->
584+
{:ok, {igniter, source, zipper}}
585+
586+
nil ->
587+
case zipper_search(Enum.filter(rest, &multiple_defmodules?/1), module_name) do
588+
{:ok, {source, zipper}} ->
589+
{:ok, {igniter, source, zipper}}
499590

591+
nil ->
592+
{:miss, igniter, searched}
593+
end
594+
end
595+
end
596+
597+
defp zipper_search(sources, module_name) do
598+
Enum.find_value(sources, fn source ->
500599
case source
501600
|> Rewrite.Source.get(:quoted)
502601
|> Zipper.zip()
503602
|> Igniter.Code.Module.move_to_defmodule(module_name) do
504-
{:ok, zipper} ->
505-
{:halt, {:ok, {igniter, source, zipper}}}
506-
507-
_ ->
508-
{:cont, {:miss, igniter, searched}}
603+
{:ok, zipper} -> {:ok, {source, zipper}}
604+
_ -> nil
509605
end
510606
end)
511607
end
@@ -541,6 +637,69 @@ defmodule Igniter.Project.Module do
541637
standard ++ inside_folder
542638
end
543639

640+
defp ensure_module_index_initialized(igniter) do
641+
if igniter.assigns[:test_mode?] ||
642+
get_in(igniter.assigns, [:private, :module_index_initialized?]) do
643+
igniter
644+
else
645+
{module_to_file, known_files} = read_manifest_index()
646+
private = igniter.assigns[:private] || %{}
647+
648+
private =
649+
private
650+
|> Map.put(:module_index, module_to_file)
651+
|> Map.put(:manifest_known_files, known_files)
652+
|> Map.put(:module_index_initialized?, true)
653+
654+
%{igniter | assigns: Map.put(igniter.assigns, :private, private)}
655+
end
656+
end
657+
658+
# Mix.Compilers.Elixir.read_manifest/1 is semi-public API ("for external consumption").
659+
# Returns {%{module => info}, %{path => source_info}} when manifest exists,
660+
# or {[], []} when it doesn't.
661+
#
662+
# Returns {module_to_file, known_files} where known_files is a MapSet of non-stale
663+
# source paths whose module mappings are authoritative and don't need to be searched.
664+
defp read_manifest_index do
665+
manifest_path = Path.join(Mix.Project.manifest_path(), "compile.elixir")
666+
667+
case Mix.Compilers.Elixir.read_manifest(manifest_path) do
668+
{modules, sources} when is_map(modules) and is_map(sources) ->
669+
stale_files =
670+
sources
671+
|> Enum.filter(fn {path, {:source, size, mtime, _, _, _, _, _, _, _, _, _}} ->
672+
case File.stat(path, time: :posix) do
673+
{:ok, %{size: ^size, mtime: ^mtime}} -> false
674+
_ -> true
675+
end
676+
end)
677+
|> MapSet.new(fn {path, _} -> path end)
678+
679+
known_files =
680+
sources
681+
|> Map.keys()
682+
|> Enum.reject(&MapSet.member?(stale_files, &1))
683+
|> MapSet.new()
684+
685+
module_to_file =
686+
modules
687+
|> Enum.reject(fn {_mod, {:module, _kind, [file | _], _, _, _}} ->
688+
MapSet.member?(stale_files, file)
689+
end)
690+
|> Map.new(fn {mod, {:module, _kind, [file | _], _, _, _}} ->
691+
{mod, file}
692+
end)
693+
694+
{module_to_file, known_files}
695+
696+
_ ->
697+
{%{}, MapSet.new()}
698+
end
699+
rescue
700+
_ -> {%{}, MapSet.new()}
701+
end
702+
544703
defp put_module_index(igniter, module_name, path) do
545704
private = igniter.assigns[:private] || %{}
546705
module_index = Map.get(private, :module_index, %{})
@@ -549,6 +708,17 @@ defmodule Igniter.Project.Module do
549708
%{igniter | assigns: Map.put(igniter.assigns, :private, private)}
550709
end
551710

711+
defp evict_module_index(igniter, module_name) do
712+
case get_in(igniter.assigns, [:private, :module_index]) do
713+
index when is_map(index) ->
714+
private = Map.put(igniter.assigns[:private], :module_index, Map.delete(index, module_name))
715+
%{igniter | assigns: Map.put(igniter.assigns, :private, private)}
716+
717+
_ ->
718+
igniter
719+
end
720+
end
721+
552722
defp log_find_module_strategy(igniter, module_name, strategy) do
553723
if igniter.assigns[:test_mode?] do
554724
private = igniter.assigns[:private] || %{}

0 commit comments

Comments
 (0)