Skip to content

Conversation

whatyouhide
Copy link
Member

@whatyouhide whatyouhide commented Dec 12, 2017

I am not sure yet on how to go about testing this, any suggestions are appreciated :). Testing is done.

The names of the options have been just decided by me for now so feedback is welcome.

Closes #6644.

Copy link
Member

@ericmj ericmj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need a manifest for this? Is Code.eval_file that much slower than File.read + binary_to_term?

# This function reads exported configuration from the imported dependencies and deals with
# caching the result of reading such configuration in a manifest file.
defp fetch_deps_opts(formatter_opts) do
case formatter_opts[:import_deps] do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this should be a cond.

@ericmj
Copy link
Member

ericmj commented Dec 12, 2017

I am not sure yet on how to go about testing this, any suggestions are appreciated

Could you do the same as the other format tests but also write to deps/foo/.formatter.exs?

@whatyouhide
Copy link
Member Author

@ericmj that doesn't work because we read dependencies, but I was able to add a test using a fixture project.

test "can read exported configuration from dependencies" do
Mix.Project.push(__MODULE__.FormatWithDepsApp)

in_fixture "format_with_deps", fn ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the fixture is empty you can use in_tmp instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I didn't realize we were recreating the dependency directory structure in the test as well. Updated to use in_tmp :)

@whatyouhide whatyouhide changed the title [WIP] Allow "mix format" to read exported configuration from dependencies Allow "mix format" to read exported configuration from dependencies Dec 13, 2017
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
* `:input` (a list of paths and patterns) - specifies the default inputs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be :inputs.

See the "Importing dependencies configuration" section below for more
information.

* `:export_locals_without_parens` (a list of function names and arities) -
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking an :export keys with a :locals_without_parens inside. But this way also works. What do you prefer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like them both. Maybe :export with subkeys is more flexible for the future. Shall we go with that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍.

# Regular formatter configuration for my_dep
# ...

export_locals_without_parens: [some_dsl_call: :*]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to give examples without :* so developers don't overuse it.

# This function reads exported configuration from the imported dependencies and deals with
# caching the result of reading such configuration in a manifest file.
defp fetch_deps_opts(formatter_opts) do
deps = formatter_opts[:import_deps]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword.get(formatter_opts, :import_deps, []) so we can remove the nil check?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this initially but removed it because I thought it made sense. Not sure anymore, so I'll just reintroduce this 😄

end

defp deps_manifest() do
Path.join(Mix.Project.build_path(), ".cached_deps_formatter.exs")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mix.Project.manifest_path()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also best to remove the .exs extension to avoid issues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.exs was a residue of initially dumping the Elixir term, good catch. Pushed a fix that uses manifest_path/0 instead of build_path/0 as well.

for dep <- deps,
dep_dot_formatter = Path.join(Map.fetch!(deps_paths, dep), ".formatter.exs"),
File.regular?(dep_dot_formatter),
{dep_opts, _} = Code.eval_file(dep_dot_formatter),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if dep_opts is not a keyword list?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a fix that extracts reading a kw list from a file (using this when reading .formatter.exs as well).

dep_parenless_calls = eval_deps_opts(deps)

if dep_parenless_calls != [] do
write_deps_manifest(dep_parenless_calls)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't write the file, the manifest is always out of date and we will execute this every time. It is probably best to write the file even if it will be an empty list.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, but do we want to write it even if folks don't provide :export_locals_without_parens? That might be why I wasn't using Keyword.get(..., []).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the case where the user specifies some deps but they don't export anything.

deps_paths = Mix.Project.deps_paths()

for dep <- deps,
dep_dot_formatter = Path.join(Map.fetch!(deps_paths, dep), ".formatter.exs"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if an invalid dep is given? Should we have a better error message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a fix that validates dependencies with a nice error message if something goes wrong.

deps_paths = Mix.Project.deps_paths()

for dep <- deps,
ensure_valid_dependency!(dep, deps_paths),
Copy link
Member

@josevalim josevalim Dec 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path = fetch_dependency_path!(dep, deps_paths)?

File.touch!(manifest_path, {{1970, 1, 1}, {0, 0, 0}})
%File.Stat{mtime: mtime} = File.stat!(manifest_path)

File.touch!(".formatter.exs")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary?


# Let's check that the manifest gets updated if .formatter.exs changes.
File.touch!(manifest_path, {{1970, 1, 1}, {0, 0, 0}})
%File.Stat{mtime: mtime} = File.stat!(manifest_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know the mtime, there is no reason to read it back!

Mix.Tasks.Format.run(["a.ex"])
%File.Stat{mtime: new_mtime} = File.stat!(manifest_path)

assert new_mtime > mtime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other places in the suite we would do:

assert File.stat!(manifest_path).mtime > {{1970, 1, 1}, {0, 0, 0}}

@josevalim josevalim merged commit 91ededc into master Dec 13, 2017
@josevalim
Copy link
Member

❤️ 💚 💙 💛 💜

@josevalim josevalim deleted the al/formatter-deps branch December 13, 2017 13:42
@whatyouhide
Copy link
Member Author

🎉


This task supports importing formatter configuration from dependencies.

A dependency that wants to export formatter configuration needs to have a `.formatter.exs` file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strictly speaking, for Hex dependencies the .formatter.exs file needs to be in the package and by default it won't be [1]. I think we could add it by default, or make a note here, or hope maintainers will remember to set it. WDYT?

[1] https://github.com/hexpm/hex/blob/v0.17.1/lib/mix/tasks/hex.build.ex#L4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wojtekmach let's update hex?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants