Skip to content

Commit

Permalink
Add ability to configure individual apps in an umbrella app for enabl…
Browse files Browse the repository at this point in the history
…ing/disabling Gradient checks

Fix analyzing beam files instead of ex files

Working on unit tests for changes

Fix unit tests
  • Loading branch information
japhib committed Oct 4, 2022
1 parent 88b352a commit d83d549
Show file tree
Hide file tree
Showing 5 changed files with 443 additions and 6 deletions.
9 changes: 9 additions & 0 deletions examples/simple_umbrella_app/apps/app_a/lib/app_a_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule AppAHelper do
@moduledoc """
Documentation for `AppAHelper`.
"""

def help do
:here_you_go
end
end
9 changes: 9 additions & 0 deletions examples/simple_umbrella_app/apps/app_b/lib/app_b_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule AppBHelper do
@moduledoc """
Documentation for `AppBHelper`.
"""

def help do
:here_you_go
end
end
2 changes: 1 addition & 1 deletion examples/simple_umbrella_app/mix.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
%{
"gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "9e629ade733780113973fe672a51d9650ed0cd86", [ref: "9e629ad"]},
"gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "8b4bf33ffc5596142e690c5aa4f24f2458b9b0fd", [ref: "8b4bf33"]},
}
134 changes: 131 additions & 3 deletions lib/mix/tasks/gradient.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule Mix.Tasks.Gradient do
## Command-line options
* `--no-compile` - do not compile even if needed
* `--no-ex-check` - do not perform checks specyfic for Elixir
* `--no-ex-check` - do not perform checks specific for Elixir
(from ElixirChecker module)
* `--no-gradualizer-check` - do not perform the Gradualizer checks
* `--no-specify` - do not specify missing lines in AST what can
Expand All @@ -19,9 +19,10 @@ defmodule Mix.Tasks.Gradient do
* `--infer` - infer type information from literals and other language
constructs,
* `--verbose` - show what Gradualizer is doing
* `--print-filenames` - print the name of every file being analyzed (mainly useful for tests)
* `--no-fancy` - do not use fancy error messages
* `--fmt-location none` - do not display location for easier comparison
* `--fmt-location brief` - display location for machine processing
* `--fmt-location brief` - display location for machine processing
* `--fmt-location verbose` - display location for human readers (default)
* `--no-colors` - do not use colors in printed messages
Expand All @@ -36,6 +37,8 @@ defmodule Mix.Tasks.Gradient do

use Mix.Task

alias Gradient.ElixirFileUtils

@options [
# skip phases options
no_compile: :boolean,
Expand All @@ -48,6 +51,7 @@ defmodule Mix.Tasks.Gradient do
stop_on_first_error: :boolean,
infer: :boolean,
verbose: :boolean,
print_filenames: :boolean,
# formatter options
no_fancy: :boolean,
fmt_location: :string,
Expand All @@ -70,7 +74,11 @@ defmodule Mix.Tasks.Gradient do
# Compile the project before the analysis
maybe_compile_project(options)
# Get paths to files
files = get_paths(user_paths)
files = get_paths(user_paths) |> filter_enabled_paths()

if options[:print_filenames] do
print_filenames(files)
end

IO.puts("Typechecking files...")

Expand All @@ -84,6 +92,32 @@ defmodule Mix.Tasks.Gradient do
:ok
end

defp print_filenames(files) do
IO.puts("Files to check:")

cwd = File.cwd!() <> "/"
# Range for slicing off cwd from the beginning of a string
cwd_range = String.length(cwd)..-1

Enum.each(files, fn {app, filenames} ->
# Print app name
case app do
"apps/" <> appname -> IO.puts("Files in app #{appname}:")
# May be nil for non-umbrella apps
_ -> :noop
end

# Print actual filenames
for filename <- filenames do
# Convert charlist to string
filename = to_string(filename)
# If the filename starts with the cwd, don't print the cwd
filename = if String.starts_with?(filename, cwd), do: String.slice(filename, cwd_range), else: filename
IO.puts(filename)
end
end)
end

defp execute(stream, opts) do
res = if opts[:crash_on_error], do: stream, else: Enum.to_list(stream)

Expand Down Expand Up @@ -213,4 +247,98 @@ defmodule Mix.Tasks.Gradient do
defp compile_path(app_name) do
Mix.Project.build_path() <> "/lib/" <> to_string(app_name) <> "/ebin"
end

@spec filter_enabled_paths(map()) :: map()
def filter_enabled_paths(apps_paths) do
apps_paths
|> Enum.map(fn {app_name, app_files} ->
{app_name, filter_paths_for_app(app_name, app_files)}
end)
|> Map.new()
end

defp filter_paths_for_app(app_name, app_files) do
config = gradient_config_for_app(app_name)

enabled? = Keyword.get(config, :enabled, true)
file_overrides_enabled? = Keyword.get(config, :file_overrides, enabled?)

if file_overrides_enabled? do
magic_comment =
if enabled?, do: "# gradient:disable-for-file", else: "# gradient:enable-for-file"

filter_files_with_magic_comment(app_name, app_files, magic_comment, not enabled?)
else
if enabled?, do: app_files, else: []
end
end

defp gradient_config_for_app(app) do
app_config = mix_config_for_app(app)[:gradient] || []

if Mix.Project.umbrella?() do
# Merge in config from umbrella app
umbrella_config = Mix.Project.config()[:gradient] || []
Keyword.merge(umbrella_config, app_config)
else
app_config
end
end

defp path_for_app(nil), do: ""
defp path_for_app(app_name), do: app_name <> "/"

defp mix_config_for_app(nil), do: Mix.Project.config()

defp mix_config_for_app(app) do
# Read the file looking for a "defmodule" to get the module name of the app's mixfile
mixfile_module_str =
File.stream!(path_for_app(app) <> "mix.exs")
|> Enum.find(&String.starts_with?(&1, "defmodule"))
|> (fn a -> Regex.run(~r/defmodule ([\w.]+)/, a) end).()
|> Enum.at(1)

mixfile_module = String.to_atom("Elixir." <> mixfile_module_str)

# return the module's config
mixfile_module.project()
end

defp filter_files_with_magic_comment(app_name, beam_files, comment, should_include?) do
app_path = path_for_app(app_name) |> Path.expand()
deps_path = Path.expand(mix_config_for_app(app_name)[:deps_path] || "deps")

Enum.filter(beam_files, fn beam_path ->
# Given the path to a .beam file, find out the .ex source file path
ex_path =
ex_filename_from_beam(beam_path)
# Turn the relative path into absolute
|> Path.expand(app_path)

# Filter out any paths representing code generated by deps
is_dep = String.starts_with?(ex_path, deps_path)

# Filter out the files with the magic comment
has_magic_comment? =
File.stream!(ex_path)
|> Enum.any?(fn line -> String.trim(line) == comment end)

# Negate has_magic_comment? if should_include? is false
has_magic_comment? = if should_include?, do: has_magic_comment?, else: not has_magic_comment?

has_magic_comment? and not is_dep
end)
end

defp ex_filename_from_beam(beam_path) do
case ElixirFileUtils.get_forms(beam_path) do
{:ok, [{:attribute, _, :file, {filename, _}} | _]} ->
# Convert the *.beam compiled filename to its corresponding *.ex source file
# (it's a charlist initially so we pipe it through to_string)
filename |> to_string()

error ->
raise "Error resolving .ex filename from compiled .beam filename #{inspect(beam_path)}: #{inspect(error)}"
end
end
end
Loading

0 comments on commit d83d549

Please sign in to comment.