From 2dafd5a12007365f86e5ad0a1c484d8bde9024f9 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 24 May 2024 16:18:25 -0700 Subject: [PATCH] (WIP) update documentation for 1.0 (#166) - begins documenting every rewrite styler does via a cheatsheet - adds TODOs to readme --- README.md | 142 +++++------------- docs/credo.cheatmd | 46 ++++++ docs/styles.cheatmd | 273 +++++++++++++++++++++++++++++++++++ docs/troubleshooting.cheatmd | 34 +++++ lib/style/deprecations.ex | 1 - lib/style/pipes.ex | 6 +- mix.exs | 3 +- test/style/pipes_test.exs | 3 + 8 files changed, 397 insertions(+), 111 deletions(-) create mode 100644 docs/credo.cheatmd create mode 100644 docs/styles.cheatmd create mode 100644 docs/troubleshooting.cheatmd diff --git a/README.md b/README.md index 6eea95cf..d0f8cd2e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ you what's wrong, it just rewrites the code for you to fit its style rules. You can learn more about the history, purpose and implementation of Styler from our talk: [Styler: Elixir Style-Guide Enforcer @ GigCity Elixir 2023](https://www.youtube.com/watch?v=6pF8Hl5EuD4) +----------------------- + +Styler's documentation is under work as part of releasing 1.0. + +You can find the [0.11.9 documentation and readme here.](https://hexdocs.pm/styler/readme.html) + ## Installation Add `:styler` as a dependency to your project's `mix.exs`: @@ -12,93 +18,45 @@ Add `:styler` as a dependency to your project's `mix.exs`: ```elixir def deps do [ - {:styler, "~> 0.11", only: [:dev, :test], runtime: false}, + {:styler, "~> 1.0.0-rc.0", only: [:dev, :test], runtime: false}, ] end ``` +Please excuse the mess below as I find spare to to update our documentation =) + + +@TODO put this somewhere more reasonable +**Note** Styler's only public API is its usage as a formatter plugin. While you're welcome to play with its internals, +they can and will change without that change being reflected in Styler's semantic version. + Then add `Styler` as a plugin to your `.formatter.exs` file ```elixir [ plugins: [Styler] + # optionally: include styler configuration + # , styler: [alias_lifting_excludes: []] ] ``` -And that's it! Now when you run `mix format` you'll also get the benefits of Styler's *definitely-always-right* style fixes. +And that's it! Now when you run `mix format` you'll also get the benefits of Styler's Stylish Stylings. ### Configuration -There isn't any! This is intentional. +@TODO document: config for lifting, and why we won't add options other configs Styler is @adobe's internal Style Guide Enforcer - allowing exceptions to the styles goes against that ethos. Happily, it's open source and thus yours to do with as you will =) ## Features (or as we call them, "Styles") -At this point, Styler does a lot. We've catalogued a list of Credo rules that it automatically fixes, but it does some things - -like shrinking function heads down to a single line when possible - that Credo doesn't care about. - -Ultimately, the best way to see what Styler does is to just try it out! What could go wrong? (You're using version control, right?) +@TODO link examples -### Credo Rules Styler Replaces +https://hexdocs.pm/styler/1.0.0-rc.0/styles.html -If you're using Credo and Styler, **we recommend disabling these rules in `.credo.exs`** to save on unnecessary checks in CI. +## Styler & Credo -Disabling the rules means updating your `.credo.exs` depending on your configuration: - -- if you're using `checks: %{enabled: [...]}`, ensure none of the checks are listed in your enabled checks -- if you're using `checks: %{disabled: [...]}`, copy/paste the snippet below into the list -- if you're using `checks: [...]`, copy/paste the snippet below into the list and ensure none of the checks appear earlier in the list - -```elixir -# Styler Rewrites -# -# The following rules are automatically rewritten by Styler and so disabled here to save time -# Some of the rules have `priority: :high`, meaning Credo runs them unless we explicitly disable them -# (removing them from this file wouldn't be enough, the `false` is required) -# -# Some rules have a comment before them explaining ways Styler deviates from the Credo rule. -# -# always expands `A.{B, C}` -{Credo.Check.Consistency.MultiAliasImportRequireUse, false}, -# including `case`, `fn` and `with` statements -{Credo.Check.Consistency.ParameterPatternMatching, false}, -# Styler implements this rule with a depth of 3 and minimum repetition of 2 -{Credo.Check.Design.AliasUsage, false}, -{Credo.Check.Readability.AliasOrder, false}, -{Credo.Check.Readability.BlockPipe, false}, -# goes further than formatter - fixes bad underscores, eg: `100_00` -> `10_000` -{Credo.Check.Readability.LargeNumbers, false}, -# adds `@moduledoc false` -{Credo.Check.Readability.ModuleDoc, false}, -{Credo.Check.Readability.MultiAlias, false}, -{Credo.Check.Readability.OneArityFunctionInPipe, false}, -# removes parens -{Credo.Check.Readability.ParenthesesOnZeroArityDefs, false}, -{Credo.Check.Readability.PipeIntoAnonymousFunctions, false}, -{Credo.Check.Readability.PreferImplicitTry, false}, -{Credo.Check.Readability.SinglePipe, false}, -# **potentially breaks compilation** - see **Troubleshooting** section below -{Credo.Check.Readability.StrictModuleLayout, false}, -{Credo.Check.Readability.StringSigils, false}, -{Credo.Check.Readability.UnnecessaryAliasExpansion, false}, -{Credo.Check.Readability.WithSingleClause, false}, -{Credo.Check.Refactor.CaseTrivialMatches, false}, -{Credo.Check.Refactor.CondStatements, false}, -# in pipes only -{Credo.Check.Refactor.FilterCount, false}, -# in pipes only -{Credo.Check.Refactor.MapInto, false}, -# in pipes only -{Credo.Check.Refactor.MapJoin, false}, -{Credo.Check.Refactor.NegatedConditionsInUnless, false}, -{Credo.Check.Refactor.NegatedConditionsWithElse, false}, -# allows ecto's `from -{Credo.Check.Refactor.PipeChainStart, false}, -{Credo.Check.Refactor.RedundantWithClauseResult, false}, -{Credo.Check.Refactor.UnlessWithElse, false}, -{Credo.Check.Refactor.WithClauses, false}, - ``` +@TODO link credo doc ## Your first Styling @@ -106,57 +64,29 @@ Disabling the rules means updating your `.credo.exs` depending on your configura Once styled the first time, future styling formats shouldn't take noticeably more time. -### Troubleshooting: Compilation broke due to Module Directive rearrangement - -Styler naively moves module attributes, which can break compilation. For now, the only fix is some elbow grease. - -#### Module Attribute dependency - -Another common compilation break on the first run is a `@moduledoc` that depended on another module attribute which -was moved below it. - -For example, given the following broken code after an initial `mix format`: +## Styler can break your code -```elixir -defmodule MyGreatLibrary do - @moduledoc make_pretty_docs(@library_options) - use OptionsMagic, my_opts: @library_options - - @library_options [ ... ] -end -``` - -You can fix the code by moving the static value outside of the module into a naked variable and then reference it in the module. (Note that `use` macros need an `unquote` wrapping the variable!) - -Yes, this is a thing you can do in a `.ex` file =) - -```elixir -library_options = [ ... ] - -defmodule MyGreatLibrary do - @moduledoc make_pretty_docs(library_options) - use OptionsMagic, my_opts: unquote(library_options) - - @library_options library_options -end -``` +@TODO link troubleshooting +mention our rewrite of case true false to if and how we're OK with this being _Styler_, not _SemanticallyEquivalentRewriter_. ## Thanks & Inspiration ### [Sourceror](https://github.com/doorgan/sourceror/) -This work was inspired by earlier large-scale rewrites of an internal codebase that used the fantastic tool `Sourceror`. +Styler's first incarnation was as one-off scripts to rewrite an internal codebase to allow Credo rules to be turned on. -The initial implementation of Styler used Sourceror, but Sourceror's AST-embedding comment algorithm slows Styler down to -the point that it's no longer an appropriate drop-in for `mix format`. +These rewrites were entirely powered by the terrific `Sourceror` library. -Still, we're grateful for the inspiration Sourceror provided and the changes to the Elixir AST APIs that it drove. +While `Styler` no longer relies on `Sourceror`, we're grateful for its author's help with those scripts, the inspiration +Sourceror provided in showing us what was possible, and the changes to the Elixir AST APIs that it drove. -The AST-Zipper implementation in this project was forked from Sourceror's implementation. +Styler's [AST-Zipper](`m:Styler.Zipper`) implementation in this project was forked from Sourceror. Zipper has been a crucial +part of our ability to ergonomically zip around (heh) Elixir AST. ### [Credo](https://github.com/rrrene/credo/) -Similarly, this project originated from one-off scripts doing large scale rewrites of an enormous codebase as part of an -effort to enable particular Credo rules for that codebase. Credo's tests and implementations were referenced for implementing -Styles that took the work the rest of the way. Thanks to Credo & the Elixir community at large for coalescing around -many of these Elixir style credos. +We never would've bothered trying to rewrite our codebase if we didn't have Credo rules we wanted to apply. + +Credo's tests and implementations were referenced for implementing Styles that took the work the rest of the way. + +Thanks to Credo & the Elixir community at large for coalescing around many of these Elixir style credos. diff --git a/docs/credo.cheatmd b/docs/credo.cheatmd new file mode 100644 index 00000000..0b1597e8 --- /dev/null +++ b/docs/credo.cheatmd @@ -0,0 +1,46 @@ +### Credo Rules Styler Replaces + +If you're using Credo and Styler, **we recommend disabling these rules in `.credo.exs`** to save on unnecessary checks in CI. + +Disabling the rules means updating your `.credo.exs` depending on your configuration: + +- if you're using `checks: %{enabled: [...]}`, ensure none of the checks are listed in your enabled checks +- if you're using `checks: %{disabled: [...]}`, copy/paste the snippet below into the list +- if you're using `checks: [...]`, copy/paste the snippet below into the list and ensure none of the checks appear earlier in the list + +```elixir +# Styler Rewrites +# +# The following rules are automatically rewritten by Styler and so disabled here to save time +# Some of the rules have `priority: :high`, meaning Credo runs them unless we explicitly disable them +# (removing them from this file wouldn't be enough, the `false` is required) +# +{Credo.Check.Consistency.MultiAliasImportRequireUse, false}, +{Credo.Check.Consistency.ParameterPatternMatching, false}, +{Credo.Check.Design.AliasUsage, false}, +{Credo.Check.Readability.AliasOrder, false}, +{Credo.Check.Readability.BlockPipe, false}, +{Credo.Check.Readability.LargeNumbers, false}, +{Credo.Check.Readability.ModuleDoc, false}, +{Credo.Check.Readability.MultiAlias, false}, +{Credo.Check.Readability.OneArityFunctionInPipe, false}, +{Credo.Check.Readability.ParenthesesOnZeroArityDefs, false}, +{Credo.Check.Readability.PipeIntoAnonymousFunctions, false}, +{Credo.Check.Readability.PreferImplicitTry, false}, +{Credo.Check.Readability.SinglePipe, false}, +{Credo.Check.Readability.StrictModuleLayout, false}, +{Credo.Check.Readability.StringSigils, false}, +{Credo.Check.Readability.UnnecessaryAliasExpansion, false}, +{Credo.Check.Readability.WithSingleClause, false}, +{Credo.Check.Refactor.CaseTrivialMatches, false}, +{Credo.Check.Refactor.CondStatements, false}, +{Credo.Check.Refactor.FilterCount, false}, +{Credo.Check.Refactor.MapInto, false}, +{Credo.Check.Refactor.MapJoin, false}, +{Credo.Check.Refactor.NegatedConditionsInUnless, false}, +{Credo.Check.Refactor.NegatedConditionsWithElse, false}, +{Credo.Check.Refactor.PipeChainStart, false}, +{Credo.Check.Refactor.RedundantWithClauseResult, false}, +{Credo.Check.Refactor.UnlessWithElse, false}, +{Credo.Check.Refactor.WithClauses, false}, + ``` diff --git a/docs/styles.cheatmd b/docs/styles.cheatmd new file mode 100644 index 00000000..4017997a --- /dev/null +++ b/docs/styles.cheatmd @@ -0,0 +1,273 @@ +# Styles + +## Simple (Single Node) Styles + + +Function Performance & Readability Optimizations + +Optimizing for either performance or readability, probably both! +These apply to the piped versions as well + + +### Strings to Sigils + +Rewrites strings with 4 or more escaped quotes to string sigils with an alternative delimiter. +The delimiter will be one of `" ( { | [ ' < /`, chosen by which would require the fewest escapes, and otherwise preferred in the order listed. + +#### Before + +```elixir +conn +|> put_resp_content_type("application/json") +|> send_resp(403, "{\"errors\":[\"Not Authorized\"]}") +|> halt() +``` + +#### After + +```elixir +conn +|> put_resp_content_type("application/json") +|> send_resp(403, ~s({"errors":["Not Authorized"]}))) +|> halt() +``` + +### Large Base 10 Numbers + +Style base 10 numbers with 5 or more digits to have a `_` every three digits. +Formatter already does this except it doesn't rewrite "typos" like `100_000_0`. + +If you're concerned that this breaks your team's formatting for things like "cents" (like "$100" being written as `100_00`), +consider using a library made for denoting currencies rather than raw elixir integers. + +#### Before + +```elixir +10000 +1_0_0_0_0 # Elixir's formatter is fine with this +-543213 +123456789 +55333.22 +-123456728.0001 +``` + +#### After + +```elixir +10_000 +10_000 +-543_213 +123_456_789 +55_333.22 +-123_456_728.0001 +``` + +### `Enum.into(%{}/Map/Keyword/MapSet.new)` -> `X.new` + +While these examples use `%{}`, the same behaviour occurs for `Keyword.new()`, `MapSet.new()` and the empty map `%{}`. + +This is an improvement for the reader, who gets a more natural language expression: "make a new map from a" vs "take a and enumerate it into a new map" + +#### Before + +```elixir +Enum.into(a, %{}) +Enum.into(a, %{}, mapping_function) +``` + +#### After + +```elixir +Map.new(a) +Map.new(a, mapping_function) +``` + +- Enum.into(%{}/Map/Keyword/MapSet.new) -> X.new + +### Map/Keyword.merge w/ single key literal -> X.put + +`Keyword.merge` and `Map.merge` called with a literal map or keyword argument with a single key are rewritten to the +equivalent `put`, a cognitively simpler function. + +#### Before +```elixir +foo |> Keyword.merge(%{just_one_key: the_value}) |> bar() +``` + +#### After +```elixir +foo |> Keyword.put(:just_one_key, the_value) |> bar() +``` + +### Map/Keyword.drop w/ single key -> X.delete + +In the same vein as the `merge` style above, `[Map|Keyword].drop/2` with a single key to drop are rewritten to use `delete/2` + +#### Before +```elixir +Map.drop(foo, [key]) +``` +#### After +```elixir +Map.delete(foo, key) +``` + +### `Enum.reverse(foo) ++ bar -> Enum.reverse(foo, bar)` + +`Enum.reverse/2` optimizes a two-step reverse and concatenation into a single step. + +#### Before +```elixir +Enum.reverse(foo) ++ bar + +baz +|> Enum.reverse() +|> Enum.concat(bop) + +``` +#### After +```elixir +Enum.reverse(foo, bar) + +Enum.reverse(baz, bop) +``` + +### Timex.now/0 -> DateTime.utc_now/0 + +Timex certainly has its uses, but knowing what stdlib date/time struct is returned by `now/0` is a bit difficult! +We prefer calling the actual function rather than its rename in Timex, helping the reader by being more explicit. + +#### Before +```elixir +Timex.now() +``` +#### After +```elixir +DateTime.utc_now() +``` + + +### DateModule.compare(x, y) == :lt/:gt -> DateModule.before?/after? + +Again, the goal is readability and maintainability. `before?/2` and `after?/2` were implemented long after `compare/2`, +so it's not unusual that a codebase needs a lot of refactoring to be brought up to date with these new functions. +That's where Styler comes in! + +#### Before +```elixir +if DateTime.compare(start, end) == :gt, + do: :error, + else: :ok +``` +#### After +```elixir +if DateTime.after?(start, end), + do: :error, + else: :ok +``` + +### Code Readability + +- put matches on right +- `Credo.Check.Readability.PreferImplicitTry` + +### Consistency +- `def foo()` -> `def foo` + +### Elixir Deprecation Rewrites + +1.15+ + +- Logger.warn -> Logger.warning +- Path.safe_relative_to/2 => Path.safe_relative/2 +- Enum/String.slice/2 w/ ranges -> explicit steps +- ~R/my_regex/ -> ~r/my_regex/ +- Date.range/2 -> Date.range/3 when decreasing range +- IO.read/bin_read -> use `:eof` instead of `:all` + +1.16+ + +- File.stream!(file, options, line_or_bytes) => File.stream!(file, line_or_bytes, options) + +### Function Definitions + +- Shrink multi-line function defs +- Put assignments on the right + +## Module Directives (`use`, `import`, `alias`, `require`, ...) + +## Mix Configs + +Mix Config files have their config stanzas sorted. Similar to the sorting of aliases, this delivers consistency to an otherwise arbitrary world, and can even help catch bugs like configuring the same key multiple times. + +A file is considered a config file if + +1. its path matches `config/.*\.exs` or `rel/overlays/.*\.exs` +2. the file imports Mix.Config (`import Mix.Config`) + +Once a file is detected as a mix config, its `config/2,3` stanzas are grouped and ordered like so: + +- group config stanzas separated by assignments (`x = y`) together +- sort each group according to erlang term sorting +- move all existing assignments between the config stanzas to above the stanzas (without changing their ordering) + +## Control Flow Structures (aka "Blocks": `case`, `if`, `unless`, `cond`, `with`) + +### `case` + +- rewrite to `if` for `true/false`, `true/_`, `false/true` + + +### `with` + +`with` great power comes a great responsibility. don't use `with` when another (simpler!) "Control Flow Structure" + +- single statement `with` with `else` clauses is rewritten to `case` (which can be further rewritten to an `if`!) +- move non `<-` out of the head and into preroll or body +- fully replace with statement with normal code as +- drop redundant identity else clause `else: (error -> error)` (also more complex matches, ala `{:error, error} -> {:error, error}`) +- Credo.Check.Refactor.RedundantWithClauseResult + +### `cond` +- Credo.Check.Refactor.CondStatements + +### `if`/`unless` + +if/unless often looks to see if the root of the statement is a "negator", defined as one of the following operators: `:!, :not, :!=, :!==`. We always try to rewrite if/unless statements to not be negated, using the inverse construct when appropriate (but we'll never write an unless with an `else`) + +- repeated negators (`!!`) are removed +- negated if/unless without an `else` are inverted to unless/if (this is done recursively until 0 or 1 negations remain) +- `unless` with `else` are inverted to negated `if` statements +- negated `if` with `else` have their clauses inverted to remove the negation +- if/unless with `else: nil` is dropped as redundant + +## Pipe Chains + +### Pipe Start + +- raw value +- blocks are extracted to variables +- ecto's `from` is allowed + +### Piped function rewrites + +- add parens to function calls `|> fun |>` => `|> fun() |>` +- remove unnecessary `then/2`: `|> then(&f(&1, ...))` -> `|> f(...)` +- add `then` when defining anon funs in pipe `|> (& &1).() |>` => `|> |> then(& &1) |>` + +### Piped function optimizations + +Two function calls into one! Tries to fit everything on one line when shrinking. + +- `lhs |> Enum.reverse() |> Enum.concat(enum)` => `lhs |> Enum.reverse(enum)` (also Kernel.++) +- `lhs |> Enum.filter(filterer) |> Enum.count()` => `lhs |> Enum.count(count)` +- `lhs |> Enum.map(mapper) |> Enum.join(joiner)` => `lhs |> Enum.map_join(joiner, mapper)` +- `lhs |> Enum.map(mapper) |> Enum.into(empty_map)` => `lhs |> Map.new(mapper)` +- `lhs |> Enum.map(mapper) |> Enum.into(collectable)` => `lhs |> Enum.into(collectable, mapper)` +- `lhs |> Enum.map(mapper) |> Map.new()` => `lhs |> Map.new(mapper)` mapset & keyword also + +### Unpiping Single Pipes + +- notably, optimizations might turn a 2 pipe into a single pipe +- doesn't unpipe when we're starting w/ quote +- pretty straight forward i daresay diff --git a/docs/troubleshooting.cheatmd b/docs/troubleshooting.cheatmd new file mode 100644 index 00000000..7daa9d37 --- /dev/null +++ b/docs/troubleshooting.cheatmd @@ -0,0 +1,34 @@ +### Troubleshooting: Compilation broke due to Module Directive rearrangement + +Styler naively moves module attributes, which can break compilation. For now, the only fix is some elbow grease. + +#### Module Attribute dependency + +Another common compilation break on the first run is a `@moduledoc` that depended on another module attribute which +was moved below it. + +For example, given the following broken code after an initial `mix format`: + +```elixir +defmodule MyGreatLibrary do + @moduledoc make_pretty_docs(@library_options) + use OptionsMagic, my_opts: @library_options + + @library_options [ ... ] +end +``` + +You can fix the code by moving the static value outside of the module into a naked variable and then reference it in the module. (Note that `use` macros need an `unquote` wrapping the variable!) + +Yes, this is a thing you can do in a `.ex` file =) + +```elixir +library_options = [ ... ] + +defmodule MyGreatLibrary do + @moduledoc make_pretty_docs(library_options) + use OptionsMagic, my_opts: unquote(library_options) + + @library_options library_options +end +``` diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex index 22b922c3..68050083 100644 --- a/lib/style/deprecations.ex +++ b/lib/style/deprecations.ex @@ -23,7 +23,6 @@ defmodule Styler.Style.Deprecations do do: {{:., dm, [{:__aliases__, am, [:Logger]}, :warning]}, funm, args} # Path.safe_relative_to/2 => Path.safe_relative/2 - # Path.safe_relative/2 is available since v1.14 # TODO: Remove after Elixir v1.19 defp style({{:., dm, [{_, _, [:Path]} = mod, :safe_relative_to]}, funm, args}), do: {{:., dm, [mod, :safe_relative]}, funm, args} diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index 003e341c..52457950 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -209,7 +209,7 @@ defmodule Styler.Style.Pipes do {:|>, [line: meta[:line]], [lhs, {reverse, [line: meta[:line]], [enum]}]} end - # `lhs |> Enum.reverse() |> Enum.concat(enum)` => `lhs |> Enum.reverse(enum)` + # `lhs |> Enum.reverse() |> Kernel.++(enum)` => `lhs |> Enum.reverse(enum)` defp fix_pipe( pipe_chain( lhs, @@ -245,7 +245,7 @@ defmodule Styler.Style.Pipes do {:|>, [line: dm[:line]], [lhs, rhs]} end - # `lhs |> Enum.map(mapper) |> Enum.into(empty_map)` => `lhs |> Map.new(mapper) + # `lhs |> Enum.map(mapper) |> Enum.into(empty_map)` => `lhs |> Map.new(mapper)` # or # `lhs |> Enum.map(mapper) |> Enum.into(collectable)` => `lhs |> Enum.into(collectable, mapper) defp fix_pipe( @@ -288,7 +288,7 @@ defmodule Styler.Style.Pipes do if replacement, do: {:|>, meta, [lhs, {replacement, dm, rest}]}, else: node end - # `lhs |> Enum.map(mapper) |> Map.new()` => `lhs |> Map.new(mapper) + # `lhs |> Enum.map(mapper) |> Map.new()` => `lhs |> Map.new(mapper)` defp fix_pipe( pipe_chain(lhs, {{:., _, [{_, _, [enum]}, :map]}, _, [mapper]}, {{:., _, [{_, _, [mod]}, :new]} = new, nm, []}) ) diff --git a/mix.exs b/mix.exs index 8fa1837e..a00cd5cd 100644 --- a/mix.exs +++ b/mix.exs @@ -60,7 +60,8 @@ defmodule Styler.MixProject do source_url: @url, extras: [ "CHANGELOG.md": [title: "Changelog"], - "README.md": [title: "Styler"] + "README.md": [title: "Styler"], + "docs/styles.cheatmd": [title: "Examples"] ] ] end diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 10170a2c..ecc071b7 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -499,6 +499,9 @@ defmodule Styler.Style.PipesTest do assert_style "a |> then(&fun/1)", "fun(a)" assert_style "a |> then(&fun(&1)) |> c", "a |> fun() |> c()" assert_style "a |> then(&fun(&1, d)) |> c", "a |> fun(d) |> c()" + assert_style "a |> then(&M.f(&1)) |> c", "a |> M.f() |> c()" + + # Doens't rewrite multiple refs / non-starting argument assert_style "a |> then(&fun(d, &1)) |> c()" assert_style "a |> then(&fun(&1, d, %{foo: &1})) |> c()"