2024-05-28 15:30:41 +12:00
defmodule Igniter do
@moduledoc """
2024-06-13 10:22:08 +12:00
Tools for generating and patching code into an Elixir project .
2024-05-28 15:30:41 +12:00
"""
2024-06-28 15:15:51 +12:00
defstruct [ :rewrite , issues : [ ] , tasks : [ ] , warnings : [ ] , assigns : %{ } , moves : %{ } ]
2024-06-01 14:09:38 +12:00
2024-06-13 11:16:03 +12:00
alias Sourceror.Zipper
2024-06-01 14:09:38 +12:00
@type t :: % __MODULE__ {
rewrite : Rewrite . t ( ) ,
issues : [ String . t ( ) ] ,
2024-06-04 15:14:36 +12:00
tasks : [ { String . t ( ) | list ( String . t ( ) ) } ] ,
warnings : [ String . t ( ) ] ,
2024-06-28 15:15:51 +12:00
assigns : map ( ) ,
moves : %{ optional ( String . t ( ) ) = > String . t ( ) }
2024-06-01 14:09:38 +12:00
}
2024-05-28 15:30:41 +12:00
2024-06-13 10:22:08 +12:00
@type zipper_updater :: ( Zipper . t ( ) -> { :ok , Zipper . t ( ) } | { :error , String . t ( ) | [ String . t ( ) ] } )
@doc " Returns a new igniter "
@spec new ( ) :: t ( )
2024-06-01 14:59:36 +12:00
def new do
2024-05-28 15:30:41 +12:00
% __MODULE__ { rewrite : Rewrite . new ( ) }
2024-06-28 15:15:51 +12:00
|> include_existing_elixir_file ( " .igniter.exs " , required? : false )
|> parse_igniter_config ( )
end
def move_file ( igniter , from , from , opts \\ [ ] )
def move_file ( igniter , from , from , _opts ) , do : igniter
def move_file ( igniter , from , to , opts ) do
case Enum . find ( igniter . moves , fn { _key , value } -> value == from end ) do
{ key , _ } ->
move_file ( igniter . moves , key , to )
_ ->
if File . exists? ( to ) || match? ( { :ok , _ } , Rewrite . source ( igniter . rewrite , to ) ) do
if Keyword . get ( opts , :error_if_exists? , true ) do
add_issue ( igniter , " Cannot move #{ from } to #{ to } , as #{ to } already exists. " )
else
igniter
end
else
igniter = include_existing_file ( igniter , from )
source = Rewrite . source! ( igniter . rewrite , from )
if Rewrite.Source . from? ( source , :string ) do
rewrite =
igniter . rewrite
|> Rewrite . drop ( [ source . path ] )
|> Rewrite . put! ( %{ source | path : to } )
%{ igniter | rewrite : rewrite }
else
%{ igniter | moves : Map . put ( igniter . moves , from , to ) }
end
end
end
2024-05-28 15:30:41 +12:00
end
2024-06-13 10:22:08 +12:00
@doc " Stores the key/value pair in `igniter.assigns` "
@spec assign ( t , atom , term ( ) ) :: t ( )
2024-06-04 15:14:36 +12:00
def assign ( igniter , key , value ) do
%{ igniter | assigns : Map . put ( igniter . assigns , key , value ) }
end
def assign ( igniter , key_vals ) do
Enum . reduce ( key_vals , igniter , fn { key , value } , igniter ->
assign ( igniter , key , value )
end )
end
2024-06-28 15:15:51 +12:00
def update_assign ( igniter , key , default , fun ) do
%{ igniter | assigns : Map . update ( igniter . assigns , key , default , fun ) }
end
2024-07-03 05:22:22 +12:00
defp assign_private ( igniter , key , value ) do
%{
igniter
| assigns : Map . update ( igniter . assigns , :private , %{ key = > value } , & Map . put ( &1 , key , value ) )
}
end
2024-06-13 10:22:08 +12:00
@doc " Includes all files matching the given glob, expecting them all (for now) to be elixir files. "
2024-06-13 11:16:03 +12:00
@spec include_glob ( t , Path . t ( ) | GlobEx . t ( ) ) :: t ( )
2024-06-11 01:58:20 +12:00
def include_glob ( igniter , glob ) do
2024-07-03 15:26:39 +12:00
paths =
glob
|> case do
2024-07-15 11:22:37 +12:00
%{ __struct__ : GlobEx } = glob -> glob
2024-07-03 15:26:39 +12:00
string -> GlobEx . compile! ( Path . expand ( string ) )
2024-06-11 01:58:20 +12:00
end
2024-07-03 15:26:39 +12:00
|> GlobEx . ls ( )
|> Stream . filter ( fn path ->
if Path . extname ( path ) in [ " .ex " , " .exs " ] do
true
else
raise ArgumentError ,
" Cannot include #{ inspect ( path ) } because it is not an Elixir file. This can be supported in the future, but the work hasn't been done yet. "
end
end )
|> Stream . map ( & Path . relative_to_cwd / 1 )
|> Enum . reject ( fn path ->
Rewrite . has_source? ( igniter . rewrite , path )
2024-06-28 15:15:51 +12:00
end )
2024-07-03 15:26:39 +12:00
paths
|> Task . async_stream ( fn path ->
read_ex_source! ( path )
end )
|> Enum . reduce ( igniter , fn { :ok , source } , igniter ->
%{ igniter | rewrite : Rewrite . put! ( igniter . rewrite , source ) }
2024-06-28 15:15:51 +12:00
end )
2024-06-11 01:58:20 +12:00
end
2024-06-13 10:22:08 +12:00
@doc """
Updates all files matching the given glob with the given zipper function .
Adds any new files matching that glob to the igniter first .
"""
@spec update_glob (
t ,
2024-06-20 07:35:06 +12:00
Path . t ( ) | GlobEx . t ( ) ,
2024-06-13 10:22:08 +12:00
zipper_updater
) :: t ( )
2024-06-11 01:58:20 +12:00
def update_glob ( igniter , glob , func ) do
2024-06-20 07:35:06 +12:00
glob =
case glob do
2024-07-15 11:22:37 +12:00
%{ __struct__ : GlobEx } = glob -> glob
2024-06-28 16:36:04 +12:00
string -> GlobEx . compile! ( Path . expand ( string ) )
2024-06-20 07:35:06 +12:00
end
2024-06-11 01:58:20 +12:00
igniter = include_glob ( igniter , glob )
2024-06-04 15:14:36 +12:00
Enum . reduce ( igniter . rewrite , igniter , fn source , igniter ->
path = Rewrite.Source . get ( source , :path )
if GlobEx . match? ( glob , path ) do
update_elixir_file ( igniter , path , func )
else
igniter
end
end )
end
2024-06-13 10:22:08 +12:00
@doc " Adds an issue to the issues list. Any issues will prevent writing and be displayed to the user. "
2024-06-15 12:09:20 +12:00
@spec add_issue ( t , term | list ( term ) ) :: t ( )
2024-05-28 15:30:41 +12:00
def add_issue ( igniter , issue ) do
2024-06-15 12:09:20 +12:00
%{ igniter | issues : List . wrap ( issue ) ++ igniter . issues }
2024-05-28 15:30:41 +12:00
end
2024-06-13 10:22:08 +12:00
@doc " Adds a warning to the warnings list. Warnings will not prevent writing, but will be displayed to the user. "
2024-06-15 12:09:20 +12:00
@spec add_warning ( t , term | list ( term ) ) :: t ( )
2024-06-04 15:14:36 +12:00
def add_warning ( igniter , warning ) do
2024-06-15 12:09:20 +12:00
%{ igniter | warnings : List . wrap ( warning ) ++ igniter . warnings }
2024-06-04 15:14:36 +12:00
end
2024-06-13 10:22:08 +12:00
@doc " Adds a task to the tasks list. Tasks will be run after all changes have been commited "
2024-06-01 14:09:38 +12:00
def add_task ( igniter , task , argv \\ [ ] ) when is_binary ( task ) do
%{ igniter | tasks : igniter . tasks ++ [ { task , argv } ] }
end
2024-06-27 03:40:58 +12:00
@doc """
Finds the ` Igniter.Mix.Task ` task by name and composes it ( calls its ` igniter / 2 ` ) into the current igniter .
If the task doesn ' t exist, a fallback implementation may be provided as the last argument.
"""
def compose_task ( igniter , task , argv \\ [ ] , fallback \\ nil )
2024-06-13 11:37:32 +12:00
2024-06-27 03:40:58 +12:00
def compose_task ( igniter , task , argv , fallback ) when is_atom ( task ) do
2024-06-01 14:09:38 +12:00
Code . ensure_compiled! ( task )
if function_exported? ( task , :igniter , 2 ) do
if ! task . supports_umbrella? ( ) && Mix.Project . umbrella? ( ) do
add_issue ( igniter , " Cannot run #{ inspect ( task ) } in an umbrella project. " )
else
task . igniter ( igniter , argv )
end
else
2024-06-27 03:40:58 +12:00
if is_function ( fallback ) do
fallback . ( igniter , argv )
else
add_issue (
igniter ,
" #{ inspect ( task ) } does not implement `Igniter.igniter/2` and no alternative implementation was provided. "
)
end
2024-06-01 14:09:38 +12:00
end
end
2024-06-27 03:40:58 +12:00
def compose_task ( igniter , task_name , argv , fallback ) do
2024-05-28 15:30:41 +12:00
if igniter . issues == [ ] do
task_name
|> Mix.Task . get ( )
|> case do
nil ->
2024-06-27 03:40:58 +12:00
if is_function ( fallback ) do
fallback . ( igniter , argv )
else
igniter
end
2024-05-28 15:30:41 +12:00
task ->
2024-06-27 03:40:58 +12:00
compose_task ( igniter , task , argv , fallback )
2024-05-28 15:30:41 +12:00
end
else
igniter
end
end
2024-06-13 10:22:08 +12:00
@doc """
Updates the source code of the given elixir file
"""
@spec update_elixir_file ( t ( ) , Path . t ( ) , zipper_updater ( ) ) :: Igniter . t ( )
2024-06-04 05:13:49 +12:00
def update_elixir_file ( igniter , path , func ) do
if Rewrite . has_source? ( igniter . rewrite , path ) do
2024-06-15 12:09:20 +12:00
source = Rewrite . source! ( igniter . rewrite , path )
igniter
|> apply_func_with_zipper ( source , func )
2024-06-06 02:12:07 +12:00
|> format ( path )
2024-06-04 05:13:49 +12:00
else
if File . exists? ( path ) do
2024-06-28 15:15:51 +12:00
source = read_ex_source! ( path )
2024-06-04 05:13:49 +12:00
%{ igniter | rewrite : Rewrite . put! ( igniter . rewrite , source ) }
|> format ( path )
2024-06-15 12:09:20 +12:00
|> apply_func_with_zipper ( source , func )
2024-06-04 05:13:49 +12:00
else
add_issue ( igniter , " Required #{ path } but it did not exist " )
end
end
end
2024-06-13 10:22:08 +12:00
@doc """
Updates a given file ' s `Rewrite.Source`
"""
@spec update_file ( t ( ) , Path . t ( ) , ( Rewrite.Source . t ( ) -> Rewrite.Source . t ( ) ) ) :: t ( )
2024-06-20 07:35:06 +12:00
def update_file ( igniter , path , updater ) do
2024-05-28 15:30:41 +12:00
if Rewrite . has_source? ( igniter . rewrite , path ) do
2024-06-20 07:35:06 +12:00
%{ igniter | rewrite : Rewrite . update! ( igniter . rewrite , path , updater ) }
2024-05-28 15:30:41 +12:00
else
2024-06-01 14:09:38 +12:00
if File . exists? ( path ) do
2024-06-28 15:15:51 +12:00
source = read_ex_source! ( path )
2024-06-01 14:09:38 +12:00
%{ igniter | rewrite : Rewrite . put! ( igniter . rewrite , source ) }
|> format ( path )
|> Map . update! ( :rewrite , fn rewrite ->
source = Rewrite . source! ( rewrite , path )
2024-06-20 07:35:06 +12:00
Rewrite . update! ( rewrite , path , updater . ( source ) )
2024-06-01 14:09:38 +12:00
end )
else
add_issue ( igniter , " Required #{ path } but it did not exist " )
end
2024-05-28 15:30:41 +12:00
end
end
2024-06-28 15:15:51 +12:00
@doc " Includes the given elixir file in the project, expecting it to exist. Does nothing if its already been added. "
@spec include_existing_elixir_file ( t ( ) , Path . t ( ) , opts :: Keyword . t ( ) ) :: t ( )
def include_existing_elixir_file ( igniter , path , opts \\ [ ] ) do
required? = Keyword . get ( opts , :required? , false )
if Rewrite . has_source? ( igniter . rewrite , path ) do
igniter
else
if File . exists? ( path ) do
source = read_ex_source! ( path )
%{ igniter | rewrite : Rewrite . put! ( igniter . rewrite , source ) }
|> then ( fn igniter ->
if opts [ :format? ] do
format ( igniter , path )
else
igniter
end
end )
else
if required? do
add_issue ( igniter , " Required #{ path } but it did not exist " )
else
igniter
end
end
end
end
2024-06-13 10:22:08 +12:00
@doc " Includes the given file in the project, expecting it to exist. Does nothing if its already been added. "
2024-06-28 15:15:51 +12:00
@spec include_existing_file ( t ( ) , Path . t ( ) , opts :: Keyword . t ( ) ) :: t ( )
def include_existing_file ( igniter , path , opts \\ [ ] ) do
required? = Keyword . get ( opts , :required? , false )
2024-05-28 15:30:41 +12:00
if Rewrite . has_source? ( igniter . rewrite , path ) do
igniter
else
if File . exists? ( path ) do
2024-06-28 15:15:51 +12:00
source = Rewrite.Source . read! ( path )
2024-06-22 08:07:26 +12:00
%{ igniter | rewrite : Rewrite . put! ( igniter . rewrite , source ) }
2024-06-01 14:09:38 +12:00
|> format ( path )
2024-05-28 15:30:41 +12:00
else
2024-06-28 15:15:51 +12:00
if required? do
add_issue ( igniter , " Required #{ path } but it did not exist " )
else
igniter
end
2024-05-28 15:30:41 +12:00
end
end
end
2024-06-13 10:22:08 +12:00
@doc " Includes or creates the given file in the project with the provided contents. Does nothing if its already been added. "
@spec include_or_create_elixir_file ( t ( ) , Path . t ( ) , contents :: String . t ( ) ) :: t ( )
2024-05-28 15:30:41 +12:00
def include_or_create_elixir_file ( igniter , path , contents \\ " " ) do
if Rewrite . has_source? ( igniter . rewrite , path ) do
igniter
else
2024-06-28 15:15:51 +12:00
source =
2024-05-28 15:30:41 +12:00
try do
2024-06-22 08:07:26 +12:00
read_ex_source! ( path )
2024-05-28 15:30:41 +12:00
rescue
_ ->
2024-06-28 15:15:51 +12:00
" "
|> Rewrite.Source.Ex . from_string ( path )
|> Rewrite.Source . update ( :file_creator , :content , contents )
2024-05-28 15:30:41 +12:00
end
%{ igniter | rewrite : Rewrite . put! ( igniter . rewrite , source ) }
2024-06-01 14:09:38 +12:00
|> format ( path )
2024-05-28 15:30:41 +12:00
end
end
2024-06-14 11:10:20 +12:00
@spec exists? ( t ( ) , Path . t ( ) ) :: boolean ( )
def exists? ( igniter , path ) do
Rewrite . has_source? ( igniter . rewrite , path ) || File . exists? ( path )
end
2024-06-20 07:35:06 +12:00
@doc " Creates the given file in the project with the provided string contents, or updates it with a function of type `zipper_updater()` if it already exists. "
2024-06-13 10:22:08 +12:00
@spec create_or_update_elixir_file ( t ( ) , Path . t ( ) , String . t ( ) , zipper_updater ( ) ) :: Igniter . t ( )
2024-06-20 07:35:06 +12:00
def create_or_update_elixir_file ( igniter , path , contents , updater ) do
2024-06-06 02:12:07 +12:00
if Rewrite . has_source? ( igniter . rewrite , path ) do
igniter
2024-06-20 07:35:06 +12:00
|> update_elixir_file ( path , updater )
2024-06-06 02:12:07 +12:00
else
2024-06-28 15:15:51 +12:00
{ created? , source } =
2024-06-06 02:12:07 +12:00
try do
2024-06-22 08:07:26 +12:00
{ false , read_ex_source! ( path ) }
2024-06-06 02:12:07 +12:00
rescue
_ ->
{ true ,
2024-06-28 15:15:51 +12:00
" "
|> Rewrite.Source.Ex . from_string ( path )
|> Rewrite.Source . update ( :file_creator , :content , contents ) }
2024-06-06 02:12:07 +12:00
end
%{ igniter | rewrite : Rewrite . put! ( igniter . rewrite , source ) }
|> format ( path )
|> then ( fn igniter ->
if created? do
igniter
else
2024-06-20 07:35:06 +12:00
update_elixir_file ( igniter , path , updater )
2024-06-06 02:12:07 +12:00
end
end )
end
end
2024-06-20 07:35:06 +12:00
@doc " Creates a new elixir file in the project with the provided string contents. Adds an error if it already exists. "
2024-06-13 10:22:08 +12:00
@spec create_new_elixir_file ( t ( ) , Path . t ( ) , String . t ( ) ) :: Igniter . t ( )
2024-05-28 15:30:41 +12:00
def create_new_elixir_file ( igniter , path , contents \\ " " ) do
2024-06-28 15:15:51 +12:00
source =
2024-05-28 15:30:41 +12:00
try do
2024-06-28 15:15:51 +12:00
source = read_ex_source! ( path )
Rewrite.Source . add_issue ( source , " File already exists " )
2024-05-28 15:30:41 +12:00
rescue
_ ->
2024-06-28 15:15:51 +12:00
" "
|> Rewrite.Source.Ex . from_string ( path )
|> Rewrite.Source . update ( :file_creator , :content , contents )
2024-05-28 15:30:41 +12:00
end
%{ igniter | rewrite : Rewrite . put! ( igniter . rewrite , source ) }
2024-06-01 14:09:38 +12:00
|> format ( path )
end
2024-07-02 07:29:51 +12:00
@doc """
Applies the current changes to the ` mix . exs ` in the Igniter and fetches dependencies .
Returns the remaining changes in the Igniter if successful .
## Options
* ` :error_on_abort? ` - If ` true ` , raises an error if the user aborts the operation . Returns the original igniter if not .
2024-07-10 08:38:57 +12:00
* ` :yes ` - If ` true ` , automatically applies the changes without prompting the user .
2024-07-02 07:29:51 +12:00
"""
def apply_and_fetch_dependencies ( igniter , opts \\ [ ] ) do
2024-07-03 05:22:22 +12:00
if ! igniter . assigns [ :private ] [ :refused_fetch_dependencies? ] &&
has_changes? ( igniter , [ " mix.exs " ] ) do
2024-07-15 11:22:37 +12:00
source = Rewrite . source! ( igniter . rewrite , " mix.exs " )
original_quoted = Rewrite.Source . get ( source , :quoted , 1 )
original_zipper = Zipper . zip ( original_quoted )
quoted = Rewrite.Source . get ( source , :quoted )
zipper = Zipper . zip ( quoted )
with { :ok , original_zipper } <-
Igniter.Code.Function . move_to_defp ( original_zipper , :deps , 0 ) ,
{ :ok , zipper } <- Igniter.Code.Function . move_to_defp ( zipper , :deps , 0 ) do
quoted_with_only_deps_change =
original_zipper
|> Igniter.Code.Common . replace_code ( zipper . node )
|> Zipper . topmost ( )
|> Zipper . node ( )
source = Rewrite.Source . update ( source , :quoted , quoted_with_only_deps_change )
rewrite = Rewrite . update! ( igniter . rewrite , source )
2024-07-16 13:05:39 +12:00
if changed? ( source ) do
display_diff ( [ source ] , opts )
2024-07-10 08:38:57 +12:00
2024-07-16 13:05:39 +12:00
message =
if opts [ :error_on_abort? ] do
" These dependencies #{ IO.ANSI . red ( ) } must #{ IO.ANSI . reset ( ) } be installed before continuing. Modify mix.exs and install? "
else
" These dependencies #{ IO.ANSI . yellow ( ) } should #{ IO.ANSI . reset ( ) } be installed before continuing. Modify mix.exs and install? "
2024-07-15 11:22:37 +12:00
end
2024-07-02 07:29:51 +12:00
2024-07-16 13:05:39 +12:00
if opts [ :yes ] || Mix . shell ( ) . yes? ( message ) do
rewrite =
case Rewrite . write ( rewrite , " mix.exs " , :force ) do
{ :ok , rewrite } -> rewrite
{ :error , error } -> raise error
end
2024-07-15 11:22:37 +12:00
2024-07-16 13:05:39 +12:00
Igniter.Util.Install . get_deps! ( )
2024-07-15 11:22:37 +12:00
2024-07-16 13:05:39 +12:00
source = Rewrite . source! ( rewrite , " mix.exs " )
source = Rewrite.Source . update ( source , :quoted , quoted )
%{ igniter | rewrite : Rewrite . update! ( rewrite , source ) }
2024-07-15 11:22:37 +12:00
else
2024-07-16 13:05:39 +12:00
if opts [ :error_on_abort? ] do
raise " Aborted by the user. "
else
assign_private ( igniter , :refused_fetch_dependencies? , true )
end
2024-07-15 11:22:37 +12:00
end
2024-07-16 13:05:39 +12:00
else
igniter
2024-07-15 11:22:37 +12:00
end
else
2024-07-02 07:29:51 +12:00
_ ->
2024-07-15 11:22:37 +12:00
display_diff ( [ source ] , opts )
2024-07-02 07:29:51 +12:00
message =
if opts [ :error_on_abort? ] do
2024-07-10 04:05:15 +12:00
" These dependencies #{ IO.ANSI . red ( ) } must #{ IO.ANSI . reset ( ) } be installed before continuing. Modify mix.exs and install? "
2024-07-02 07:29:51 +12:00
else
2024-07-10 04:05:15 +12:00
" These dependencies #{ IO.ANSI . yellow ( ) } should #{ IO.ANSI . reset ( ) } be installed before continuing. Modify mix.exs and install? "
2024-07-02 07:29:51 +12:00
end
2024-07-15 11:22:37 +12:00
if Mix . shell ( ) . yes? ( message ) do
rewrite =
case Rewrite . write ( igniter . rewrite , " mix.exs " , :force ) do
{ :ok , rewrite } -> rewrite
{ :error , error } -> raise error
end
2024-07-02 07:29:51 +12:00
2024-07-15 11:22:37 +12:00
Igniter.Util.Install . get_deps! ( )
2024-07-02 07:29:51 +12:00
2024-07-15 11:22:37 +12:00
%{ igniter | rewrite : rewrite }
2024-07-02 07:29:51 +12:00
else
if opts [ :error_on_abort? ] do
raise " Aborted by the user. "
else
2024-07-03 05:22:22 +12:00
assign_private ( igniter , :refused_fetch_dependencies? , true )
2024-07-02 07:29:51 +12:00
end
end
end
else
igniter
end
end
2024-07-03 05:50:06 +12:00
@doc " This function stores in the igniter if its been run before, so it is only run once, which is expensive. "
2024-07-03 08:33:08 +12:00
if Application . compile_env ( :igniter , :testing? , false ) do
def include_all_elixir_files ( igniter ) do
2024-07-03 05:50:06 +12:00
igniter
2024-07-03 08:33:08 +12:00
end
else
def include_all_elixir_files ( igniter ) do
if igniter . assigns [ :private ] [ :included_all_elixir_files? ] do
igniter
else
igniter
2024-07-19 08:39:03 +12:00
|> include_glob ( " {lib,test,config}/**/*.{ex,exs} " )
2024-07-03 08:33:08 +12:00
|> assign_private ( :included_all_elixir_files? , true )
end
2024-07-03 05:50:06 +12:00
end
end
2024-07-02 07:29:51 +12:00
@doc """
Returns whether the current Igniter has pending changes .
"""
def has_changes? ( igniter , paths \\ nil ) do
paths =
if paths do
Enum . map ( paths , & Path . relative_to_cwd / 1 )
end
igniter . rewrite
|> Rewrite . sources ( )
|> then ( fn sources ->
if paths do
sources
|> Enum . filter ( & ( &1 . path in paths ) )
else
sources
end
end )
|> Enum . any? ( fn source ->
Rewrite.Source . from? ( source , :string ) || Rewrite.Source . updated? ( source )
end )
end
2024-06-13 10:22:08 +12:00
@doc """
Executes or dry - runs a given Igniter .
"""
2024-07-03 15:26:39 +12:00
def do_or_dry_run ( igniter , opts \\ [ ] ) do
2024-07-15 11:22:37 +12:00
igniter = prepare_for_write ( igniter )
2024-07-03 15:26:39 +12:00
2024-06-01 14:59:36 +12:00
title = opts [ :title ] || " Igniter "
2024-07-03 15:26:39 +12:00
halt_if_fails_check! ( igniter , title , opts )
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
case igniter do
%{ issues : [ ] } ->
result_of_dry_run =
if has_changes? ( igniter ) do
if opts [ :dry_run ] || ! opts [ :yes ] do
Mix . shell ( ) . info ( " \n #{ IO.ANSI . green ( ) } #{ title } #{ IO.ANSI . reset ( ) } : " )
2024-07-10 08:38:57 +12:00
display_diff ( Rewrite . sources ( igniter . rewrite ) , opts )
2024-07-03 15:26:39 +12:00
end
:dry_run_with_changes
2024-06-01 14:59:36 +12:00
else
2024-07-03 15:26:39 +12:00
unless opts [ :quiet_on_no_changes? ] || opts [ :yes ] do
Mix . shell ( ) . info ( " \n #{ title } : \n \n No proposed content changes! \n " )
end
:dry_run_with_no_changes
2024-06-01 14:59:36 +12:00
end
2024-07-03 15:26:39 +12:00
display_warnings ( igniter , title )
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
display_moves ( igniter )
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
display_tasks ( igniter , result_of_dry_run , opts )
2024-06-14 02:57:10 +12:00
2024-07-03 15:26:39 +12:00
if opts [ :dry_run ] ||
( result_of_dry_run == :dry_run_with_no_changes && Enum . empty? ( igniter . tasks ) &&
Enum . empty? ( igniter . moves ) ) do
result_of_dry_run
else
if opts [ :yes ] ||
Mix . shell ( ) . yes? ( opts [ :confirmation_message ] || " Proceed with changes? " ) do
igniter . rewrite
|> Enum . any? ( fn source ->
Rewrite.Source . from? ( source , :string ) || Rewrite.Source . updated? ( source )
end )
|> Kernel . || ( ! Enum . empty? ( igniter . tasks ) )
|> Kernel . || ( ! Enum . empty? ( igniter . moves ) )
|> if do
igniter . rewrite
|> Rewrite . write_all ( )
|> case do
{ :ok , _result } ->
unless Enum . empty? ( igniter . tasks ) do
Mix . shell ( ) . cmd ( " mix deps.get " )
end
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
igniter . moves
|> Enum . each ( fn { from , to } ->
File . mkdir_p! ( Path . dirname ( to ) )
File . rename! ( from , to )
2024-07-02 07:29:51 +12:00
end )
2024-07-03 15:26:39 +12:00
igniter . tasks
|> Enum . each ( fn { task , args } ->
Mix . shell ( ) . cmd ( " mix #{ task } #{ Enum . join ( args , " " ) } " )
end )
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
:changes_made
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
{ :error , error , rewrite } ->
igniter
|> Map . put ( :rewrite , rewrite )
|> Igniter . add_issue ( error )
|> igniter_issues ( )
2024-06-15 12:09:20 +12:00
2024-07-03 15:26:39 +12:00
:issues
end
else
:no_changes
2024-06-15 12:09:20 +12:00
end
2024-07-03 15:26:39 +12:00
else
:changes_aborted
end
end
igniter ->
igniter_issues ( igniter )
:issues
end
end
2024-06-15 12:09:20 +12:00
2024-07-03 15:26:39 +12:00
defp halt_if_fails_check! ( igniter , title , opts ) do
cond do
! opts [ :check ] ->
:ok
2024-06-28 15:15:51 +12:00
2024-07-03 15:26:39 +12:00
! Enum . empty? ( igniter . warnings ) ->
Mix . shell ( ) . error ( " Warnings would have been emitted and the --check flag was specified. " )
display_warnings ( igniter , title )
2024-06-28 15:15:51 +12:00
2024-07-03 15:26:39 +12:00
System . halt ( 2 )
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
! Enum . empty? ( igniter . issues ) ->
Mix . shell ( ) . error ( " Errors would have been emitted and the --check flag was specified. " )
igniter_issues ( igniter )
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
System . halt ( 3 )
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
! Enum . empty? ( igniter . tasks ) ->
Mix . shell ( ) . error ( " Tasks would have been run and the --check flag was specified. " )
display_tasks ( igniter , :dry_run_with_no_changes , [ ] )
2024-06-01 14:59:36 +12:00
2024-07-03 15:26:39 +12:00
System . halt ( 3 )
! Enum . empty? ( igniter . moves ) ->
Mix . shell ( ) . error ( " Files would have been moved and the --check flag was specified. " )
display_moves ( igniter )
System . halt ( 3 )
Igniter . has_changes? ( igniter ) ->
Mix . shell ( ) . error (
" Changes have been made to the project and the --check flag was specified. "
)
2024-07-10 08:38:57 +12:00
display_diff ( igniter . rewrite . sources , opts )
2024-07-03 15:26:39 +12:00
System . halt ( 1 )
2024-06-01 14:59:36 +12:00
end
end
2024-07-10 08:38:57 +12:00
defp display_diff ( sources , opts ) do
unless opts [ :yes ] do
Enum . each ( sources , fn source ->
if Rewrite.Source . from? ( source , :string ) do
content_lines =
source
|> Rewrite.Source . get ( :content )
|> String . split ( " \n " )
|> Enum . with_index ( )
space_padding =
content_lines
|> Enum . map ( & elem ( &1 , 1 ) )
|> Enum . max ( )
|> to_string ( )
|> String . length ( )
diffish_looking_text =
Enum . map_join ( content_lines , " \n " , fn { line , line_number_minus_one } ->
line_number = line_number_minus_one + 1
" #{ String . pad_trailing ( to_string ( line_number ) , space_padding ) } #{ IO.ANSI . yellow ( ) } | #{ IO.ANSI . green ( ) } #{ line } #{ IO.ANSI . reset ( ) } "
end )
2024-07-03 15:26:39 +12:00
2024-07-10 08:38:57 +12:00
if String . trim ( diffish_looking_text ) != " " do
Mix . shell ( ) . info ( """
Create : #{Rewrite.Source.get(source, :path)}
2024-07-03 15:26:39 +12:00
2024-07-10 08:38:57 +12:00
#{diffish_looking_text}
""" )
end
else
diff = Rewrite.Source . diff ( source ) |> IO . iodata_to_binary ( )
if String . trim ( diff ) != " " do
Mix . shell ( ) . info ( """
Update : #{Rewrite.Source.get(source, :path)}
#{diff}
""" )
end
2024-07-03 15:26:39 +12:00
end
2024-07-10 08:38:57 +12:00
end )
end
2024-07-03 15:26:39 +12:00
end
2024-06-01 14:59:36 +12:00
defp igniter_issues ( igniter ) do
Mix . shell ( ) . info ( " Issues during code generation " )
igniter . issues
|> Enum . map_join ( " \n " , fn error ->
if is_binary ( error ) do
" * #{ error } "
else
" * #{ Exception . format ( :error , error ) } "
end
end )
|> Mix . shell ( ) . info ( )
end
2024-06-28 15:15:51 +12:00
defp format ( igniter , adding_paths \\ nil ) do
igniter =
igniter
|> include_existing_elixir_file ( " config/config.exs " , require? : false )
|> include_existing_elixir_file ( " config/ #{ Mix . env ( ) } .exs " , require? : false )
2024-06-11 01:58:20 +12:00
2024-06-28 15:15:51 +12:00
if adding_paths &&
Enum . any? ( List . wrap ( adding_paths ) , & ( Path . basename ( &1 ) == " .formatter.exs " ) ) do
2024-06-01 14:09:38 +12:00
format ( igniter )
else
igniter =
" **/.formatter.exs "
|> Path . wildcard ( )
|> Enum . reduce ( igniter , fn path , igniter ->
Igniter . include_existing_elixir_file ( igniter , path )
end )
2024-06-04 15:14:36 +12:00
igniter =
if File . exists? ( " .formatter.exs " ) do
Igniter . include_existing_elixir_file ( igniter , " .formatter.exs " )
else
igniter
end
2024-06-01 14:09:38 +12:00
rewrite = igniter . rewrite
formatter_exs_files =
rewrite
|> Enum . filter ( fn source ->
source
|> Rewrite.Source . get ( :path )
|> Path . basename ( )
|> Kernel . == ( " .formatter.exs " )
end )
|> Map . new ( fn source ->
dir =
source
|> Rewrite.Source . get ( :path )
|> Path . dirname ( )
{ dir , source }
end )
rewrite =
Rewrite . map! ( rewrite , fn source ->
path = source |> Rewrite.Source . get ( :path )
2024-06-28 15:15:51 +12:00
if is_nil ( adding_paths ) || path in List . wrap ( adding_paths ) do
2024-06-01 14:09:38 +12:00
dir = Path . dirname ( path )
2024-06-14 00:17:50 +12:00
opts =
2024-06-14 11:10:20 +12:00
case find_formatter_exs_file_options ( dir , formatter_exs_files , Path . extname ( path ) ) do
2024-06-14 00:17:50 +12:00
:error ->
[ ]
{ :ok , opts } ->
opts
end
formatted =
with_evaled_configs ( rewrite , fn ->
Rewrite.Source.Ex . format ( source , opts )
end )
source
|> Rewrite.Source.Ex . put_formatter_opts ( opts )
|> Rewrite.Source . update ( :content , formatted )
2024-06-01 14:09:38 +12:00
else
source
end
end )
%{ igniter | rewrite : rewrite }
end
end
2024-06-04 15:14:36 +12:00
# for now we only eval `config.exs`
defp with_evaled_configs ( rewrite , fun ) do
2024-06-11 01:58:20 +12:00
[
Rewrite . source ( rewrite , " config/config.exs " ) ,
Rewrite . source ( rewrite , " config/ #{ Mix . env ( ) } .exs " )
]
|> Enum . flat_map ( fn
2024-06-04 15:14:36 +12:00
{ :ok , source } ->
2024-06-11 01:58:20 +12:00
[ Rewrite.Source . get ( source , :content ) ]
2024-06-04 15:14:36 +12:00
_ ->
2024-06-11 01:58:20 +12:00
[ ]
end )
|> case do
[ ] ->
2024-06-04 15:14:36 +12:00
fun . ( )
2024-06-11 01:58:20 +12:00
contents ->
to_set =
contents
|> Enum . join ( " \n " )
|> String . split ( " import_config " , parts : 2 )
|> List . first ( )
|> then ( & Config.Reader . eval! ( " config/config.exs " , &1 , env : Mix . env ( ) ) )
restore =
to_set
|> Keyword . keys ( )
|> Enum . map ( fn key ->
{ key , Application . get_all_env ( key ) }
end )
try do
Application . put_all_env ( to_set )
fun . ( )
after
Application . put_all_env ( restore )
end
2024-06-04 15:14:36 +12:00
end
end
2024-06-14 11:10:20 +12:00
defp find_formatter_exs_file_options ( path , formatter_exs_files , ext ) do
2024-06-01 14:09:38 +12:00
case Map . fetch ( formatter_exs_files , path ) do
{ :ok , source } ->
{ opts , _ } = Rewrite.Source . get ( source , :quoted ) |> Code . eval_quoted ( )
2024-06-14 11:10:20 +12:00
{ :ok , opts |> eval_deps ( ) |> filter_plugins ( ext ) }
2024-06-01 14:09:38 +12:00
:error ->
if path in [ " / " , " . " ] do
:error
else
new_path =
Path . join ( path , " .. " )
|> Path . expand ( )
|> Path . relative_to_cwd ( )
2024-06-14 11:10:20 +12:00
find_formatter_exs_file_options ( new_path , formatter_exs_files , ext )
2024-06-01 14:09:38 +12:00
end
end
end
# This can be removed if/when this PR is merged: https://github.com/hrzndhrn/rewrite/pull/34
defp eval_deps ( formatter_opts ) do
deps = Keyword . get ( formatter_opts , :import_deps , [ ] )
locals_without_parens = eval_deps_opts ( deps )
formatter_opts =
Keyword . update (
formatter_opts ,
:locals_without_parens ,
locals_without_parens ,
& ( locals_without_parens ++ &1 )
)
formatter_opts
end
defp eval_deps_opts ( [ ] ) do
[ ]
end
defp eval_deps_opts ( deps ) do
deps_paths = Mix.Project . deps_paths ( )
for dep <- deps ,
dep_path = fetch_valid_dep_path ( dep , deps_paths ) ,
! is_nil ( dep_path ) ,
dep_dot_formatter = Path . join ( dep_path , " .formatter.exs " ) ,
File . regular? ( dep_dot_formatter ) ,
dep_opts = eval_file_with_keyword_list ( dep_dot_formatter ) ,
parenless_call <- dep_opts [ :export ] [ :locals_without_parens ] || [ ] ,
uniq : true ,
do : parenless_call
end
defp fetch_valid_dep_path ( dep , deps_paths ) when is_atom ( dep ) do
with %{ ^ dep = > path } <- deps_paths ,
true <- File . dir? ( path ) do
path
else
_ ->
nil
end
end
defp fetch_valid_dep_path ( _dep , _deps_paths ) do
nil
end
defp eval_file_with_keyword_list ( path ) do
{ opts , _ } = Code . eval_file ( path )
unless Keyword . keyword? ( opts ) do
raise " Expected #{ inspect ( path ) } to return a keyword list, got: #{ inspect ( opts ) } "
end
opts
2024-05-28 15:30:41 +12:00
end
2024-06-04 05:13:49 +12:00
2024-06-15 12:09:20 +12:00
defp apply_func_with_zipper ( igniter , source , func ) do
2024-06-04 05:13:49 +12:00
quoted = Rewrite.Source . get ( source , :quoted )
zipper = Sourceror.Zipper . zip ( quoted )
case func . ( zipper ) do
2024-06-06 02:12:07 +12:00
{ :ok , % Sourceror.Zipper { } = zipper } ->
2024-06-15 12:09:20 +12:00
Rewrite . update! (
igniter . rewrite ,
Rewrite.Source . update (
source ,
:configure ,
:quoted ,
Sourceror.Zipper . root ( zipper )
)
2024-06-06 02:12:07 +12:00
)
2024-06-15 12:09:20 +12:00
|> then ( & Map . put ( igniter , :rewrite , &1 ) )
2024-06-06 02:12:07 +12:00
2024-06-04 05:13:49 +12:00
% Sourceror.Zipper { } = zipper ->
2024-06-15 12:09:20 +12:00
Rewrite . update! (
igniter . rewrite ,
Rewrite.Source . update (
source ,
:configure ,
:quoted ,
Sourceror.Zipper . root ( zipper )
)
2024-06-04 05:13:49 +12:00
)
2024-06-15 12:09:20 +12:00
|> then ( & Map . put ( igniter , :rewrite , &1 ) )
2024-06-04 05:13:49 +12:00
{ :error , error } ->
2024-06-15 12:09:20 +12:00
Rewrite . update! (
igniter . rewrite ,
Rewrite.Source . add_issues ( source , List . wrap ( error ) )
)
|> then ( & Map . put ( igniter , :rewrite , &1 ) )
{ :warning , warning } ->
Igniter . add_warning ( igniter , warning )
2024-06-04 05:13:49 +12:00
end
end
2024-06-14 11:10:20 +12:00
defp filter_plugins ( opts , ext ) do
Keyword . put ( opts , :plugins , plugins_for_ext ( opts , ext ) )
end
defp plugins_for_ext ( formatter_opts , ext ) do
formatter_opts
|> Keyword . get ( :plugins , [ ] )
|> Enum . filter ( fn plugin ->
Code . ensure_loaded? ( plugin ) and function_exported? ( plugin , :features , 1 ) and
ext in List . wrap ( plugin . features ( formatter_opts ) [ :extensions ] )
end )
end
2024-06-22 08:07:26 +12:00
defp read_ex_source! ( path ) do
source = Rewrite.Source.Ex . read! ( path )
2024-06-28 15:15:51 +12:00
content =
2024-06-22 08:07:26 +12:00
source
|> Rewrite.Source . get ( :content )
2024-06-28 15:15:51 +12:00
Rewrite.Source . update ( source , :content , content )
2024-06-22 08:07:26 +12:00
end
@doc false
2024-07-15 11:22:37 +12:00
def prepare_for_write ( igniter ) do
2024-07-03 15:26:39 +12:00
source_issues =
Enum . flat_map ( igniter . rewrite , fn source ->
changed_issues =
if Rewrite.Source . file_changed? ( source ) do
[ " File has been changed since it was originally read. " ]
else
[ ]
end
issues = Enum . uniq ( changed_issues ++ Rewrite.Source . issues ( source ) )
case issues do
[ ] ->
[ ]
issues ->
Enum . map ( issues , fn issue ->
" #{ source . path } : #{ issue } "
end )
end
end )
2024-07-10 12:23:15 +12:00
needs_test_support? =
Enum . any? ( igniter . rewrite , fn source ->
Path . extname ( source . path ) == " .ex " &&
source . path
|> Path . split ( )
|> List . starts_with? ( [ " test " , " support " ] )
end )
2024-06-28 15:15:51 +12:00
%{
2024-06-22 08:07:26 +12:00
igniter
2024-07-03 15:26:39 +12:00
| issues : Enum . uniq ( igniter . issues ++ source_issues ) ,
2024-06-22 08:07:26 +12:00
warnings : Enum . uniq ( igniter . warnings ) ,
tasks : Enum . uniq ( igniter . tasks )
}
2024-07-02 12:45:14 +12:00
|> Igniter.Code.Module . move_files ( )
2024-06-28 15:15:51 +12:00
|> remove_unchanged_files ( )
2024-07-10 12:23:15 +12:00
|> then ( fn igniter ->
if needs_test_support? do
Igniter.Project.Test . ensure_test_support ( igniter )
else
igniter
end
end )
2024-06-28 15:15:51 +12:00
end
2024-06-22 08:07:26 +12:00
2024-06-28 15:15:51 +12:00
defp remove_unchanged_files ( igniter ) do
igniter . rewrite
|> Enum . flat_map ( fn source ->
if Rewrite.Source . from? ( source , :string ) || changed? ( source ) do
[ ]
else
[ source . path ]
end
end )
|> then ( fn paths ->
%{ igniter | rewrite : Rewrite . drop ( igniter . rewrite , paths ) }
end )
end
2024-06-22 08:07:26 +12:00
2024-06-28 15:15:51 +12:00
defp parse_igniter_config ( igniter ) do
case Rewrite . source ( igniter . rewrite , " .igniter.exs " ) do
{ :error , _ } ->
assign ( igniter , :igniter_exs , [ ] )
2024-06-22 08:07:26 +12:00
2024-06-28 15:15:51 +12:00
{ :ok , source } ->
{ igniter_exs , _ } = Rewrite.Source . get ( source , :quoted ) |> Code . eval_quoted ( )
assign ( igniter , :igniter_exs , igniter_exs )
end
end
2024-06-22 08:07:26 +12:00
2024-06-28 15:15:51 +12:00
defp changed? ( source ) do
diff = Rewrite.Source . diff ( source ) |> IO . iodata_to_binary ( )
2024-06-22 08:07:26 +12:00
2024-06-28 15:15:51 +12:00
String . trim ( diff ) != " "
2024-06-22 08:07:26 +12:00
end
2024-07-03 15:26:39 +12:00
defp display_warnings ( %{ warnings : [ ] } , _title ) , do : :ok
defp display_warnings ( %{ warnings : warnings } , title ) do
Mix . shell ( ) . info ( " \n #{ title } - #{ IO.ANSI . yellow ( ) } Notices: #{ IO.ANSI . reset ( ) } \n " )
warnings
|> Enum . map_join ( " \n --- \n " , fn error ->
if is_binary ( error ) do
" * #{ IO.ANSI . yellow ( ) } #{ error } #{ IO.ANSI . reset ( ) } "
else
" * #{ IO.ANSI . yellow ( ) } #{ Exception . format ( :error , error ) } #{ IO.ANSI . reset ( ) } "
end
end )
|> Mix . shell ( ) . info ( )
end
defp display_moves ( %{ moves : moves } ) when moves == %{ } , do : :ok
defp display_moves ( %{ moves : moves } ) do
Mix . shell ( ) . info ( " The following files will be moved: " )
Enum . each ( moves , fn { from , to } ->
Mix . shell ( ) . info (
" #{ IO.ANSI . red ( ) } #{ from } #{ IO.ANSI . reset ( ) } : #{ IO.ANSI . green ( ) } #{ to } #{ IO.ANSI . reset ( ) } "
)
end )
end
defp display_tasks ( igniter , result_of_dry_run , opts ) do
if igniter . tasks != [ ] && ! opts [ :yes ] do
message =
if result_of_dry_run == :dry_run_with_no_changes do
" The following tasks will be run "
else
" The following tasks will be run after the above changes: "
end
Mix . shell ( ) . info ( """
#{message}
#{Enum.map_join(igniter.tasks, "\n", fn {task, args} -> "* #{IO.ANSI.red()}#{task}#{IO.ANSI.yellow()} #{Enum.join(args, " ")}#{IO.ANSI.reset()}" end)}
""" )
end
end
2024-05-28 15:30:41 +12:00
end