-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add options for enabling/disabling Gradient type-checking for specific apps/files #124
Changes from all commits
99c5697
39dde9e
bd04a48
da07e85
740ec3d
2f13126
84d50d8
867a8ce
ac4c260
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 |
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", "ba5481cf1c208a0c29d15e5ab241b5af58dde28d", [ref: "ba5481c"]}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
defmodule Mix.Tasks.CleanExamples do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had some test failures while running some of the tests that were fixed by removing the compiled test files so they could be re-compiled from scratch. I'm adding this mix task, along with an alias to call it before |
||
@moduledoc """ | ||
Mix task for removing compiled files under test/examples/_build and | ||
test/examples/erlang/_build. Useful for getting a clean build for tests. | ||
""" | ||
|
||
use Mix.Task | ||
|
||
@impl Mix.Task | ||
def run(_args) do | ||
File.rm_rf!("test/examples/_build") | ||
File.rm_rf!("test/examples/erlang/_build") | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -20,6 +20,7 @@ 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 | ||
|
@@ -40,6 +41,8 @@ defmodule Mix.Tasks.Gradient do | |
|
||
use Mix.Task | ||
|
||
alias Gradient.ElixirFileUtils | ||
|
||
@options [ | ||
# skip phases options | ||
no_compile: :boolean, | ||
|
@@ -53,6 +56,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, | ||
|
@@ -73,14 +77,18 @@ defmodule Mix.Tasks.Gradient do | |
|
||
options = Enum.reduce(options, [], &prepare_option/2) | ||
|
||
# Compile the project before the analysis | ||
maybe_compile_project(options) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved this earlier in the pipeline to avoid a specific issue - when you run When I dug in, the reason is that in Gradualizer, It turns out that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, thanks! |
||
# Load dependencies | ||
maybe_load_deps(options) | ||
# Start Gradualizer application | ||
Application.ensure_all_started(:gradualizer) | ||
# 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...") | ||
|
||
|
@@ -100,6 +108,36 @@ 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 | ||
crash_on_error? = Keyword.get(opts, :crash_on_error, false) | ||
|
||
|
@@ -272,4 +310,99 @@ 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in #138 this line is not necessary because before
maybe_specify_forms/2
is called, we already callput_source_path
.