@@ -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