Last active
June 14, 2021 10:25
-
-
Save amalbuquerque/d44750bf021ba454e97b4a9086ec0ead to your computer and use it in GitHub Desktop.
Elixir Type Specs Cleaner
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Elixir Type specs cleaner v1.0.1 | |
# | |
# I wanted to understand the time cost of having type specs in practically all | |
# our modules of a large Elixir codebase since, by using remote structs on type | |
# specs instead of struct references that would become a compile time | |
# dependency, we already saw some time improvements. | |
# | |
# Good (using `Bar.t`): `@spec foo(String.t) :: Bar.t` | |
# Bad (using `%Bar{}`): `@spec foo(String.t) :: %Bar{}` | |
# | |
# As such, I created this 0-dependencies Elixir script that removes the type specs | |
# so we can then easily compare compilation times. | |
# | |
# **BEWARE**: This script is destructive, it changes files in place! | |
# | |
# By default it runs in `dry-run` mode (it doesn't change your files). | |
# To run it in dry-run mode, use it like this: | |
# `$ elixir type_specs_cleaner.exs foo.ex` | |
# | |
# If you're sure you want to remove your type specs and change your code, | |
# run the script like this: | |
# `$ elixir type_specs_cleaner.exs --real-run foo.ex bar.ex baz.ex` | |
# | |
# If you want to pass it a list of files from `xargs`, use the `--quiet` option. | |
# This will make the script run in quiet mode, not showing those warning prompts: | |
# `find . | grep foo$ | xargs elixir type_specs_cleaner.exs --quiet` | |
# | |
# If you're really sure what you're doing, pass both `--quiet` and `--real-run` | |
# flags. Please create some backups first, it's your responsibility if you lose | |
# any code with this tool. | |
# | |
# v1.0 - 2021/06/14: Initial version | |
# v1.0.1 - 2021/06/14: Ignore `deps/` and `_build/` folders | |
defmodule SpecCleaner do | |
@nodes_to_remove [:spec, :type] | |
def confirm_proceed_or_exit(ask_message, confirm_message) do | |
IO.puts(ask_message <> "\n This is an irreversible operation! Do you want to continue? Y/[n]") | |
case IO.read(:line) do | |
"Y" <> _rest -> | |
IO.puts(confirm_message) | |
_ -> | |
IO.puts("Aborting...") | |
exit(:normal) | |
end | |
end | |
def clean_spec_from_file(file_path, real_run) do | |
case File.read(file_path) do | |
{:error, _} -> | |
IO.puts("Problem reading from '#{file_path}'. Skipping this file...") | |
{:ok, file_content} -> | |
content_without_specs = file_content | |
|> clean_type_specs() # returns AST | |
|> Macro.to_string() | |
|> Code.format_string!() | |
if real_run do | |
File.write(file_path, content_without_specs) | |
IO.puts("Type specs removed from #{file_path}...") | |
else | |
IO.puts("File #{file_path} without type specs looks like:\n#{content_without_specs}") | |
end | |
end | |
end | |
def clean_type_specs_from_ast(ast) do | |
Macro.prewalk(ast, fn | |
{:@, _, [first | _rest]} = node -> | |
case first do | |
{marker, _, _} when marker in @nodes_to_remove -> | |
:removed_by_spec_cleaner | |
_ -> | |
node | |
end | |
node -> node | |
end) | |
end | |
def clean_type_specs(file_content) do | |
ast = Code.string_to_quoted!(file_content) | |
clean_type_specs_from_ast(ast) | |
end | |
def files_from_args do | |
case System.argv() do | |
[] -> | |
raise """ | |
You need to pass one or more `.ex` files to be cleaned | |
from specs. | |
""" | |
files -> | |
files | |
|> List.delete("--real-run") | |
|> List.delete("--quiet") | |
|> Enum.reject(&Regex.match?(~r[deps/], &1)) | |
|> Enum.reject(&Regex.match?(~r[_build/], &1)) | |
|> Enum.filter(&Regex.match?(~r/\.ex$/, &1)) | |
end | |
end | |
end | |
real_run? = "--real-run" in System.argv() | |
quiet? = "--quiet" in System.argv() | |
files = SpecCleaner.files_from_args() | |
IO.puts("Processing the following files: #{inspect(files)}") | |
case real_run? do | |
true -> | |
if not quiet? do | |
SpecCleaner.confirm_proceed_or_exit( | |
"*BEWARE!* The #{length(files)} files will be destructively updated, plz make sure you have a backup of the files.", | |
"Will process #{length(files)} files.\n") | |
SpecCleaner.confirm_proceed_or_exit( | |
"This is your last chance to abort the operation! It will destructively change the files (by removing the @spec and @type)", | |
"Processing #{length(files)} files...\n") | |
end | |
_ -> | |
IO.puts("(Dry-run mode)") | |
end | |
start = :erlang.monotonic_time(:microsecond) | |
files | |
|> Task.async_stream(fn file -> SpecCleaner.clean_spec_from_file(file, real_run?) end) | |
|> Stream.run() | |
finish = :erlang.monotonic_time(:microsecond) | |
time_it_took = finish - start | |
IO.puts("Processed #{length(files)} files in #{time_it_took/1000} millisecs") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment