Skip to content

Instantly share code, notes, and snippets.

@amalbuquerque
Last active June 14, 2021 10:25
Show Gist options
  • Save amalbuquerque/d44750bf021ba454e97b4a9086ec0ead to your computer and use it in GitHub Desktop.
Save amalbuquerque/d44750bf021ba454e97b4a9086ec0ead to your computer and use it in GitHub Desktop.
Elixir Type Specs Cleaner
# 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