From 3769ffb81aaaad80ad4cf17e20dc154a63fdc531 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 7 Aug 2024 08:31:38 -0600 Subject: [PATCH 01/86] ignore no-paren, no-body function heads. closes #185 --- CHANGELOG.md | 1 + lib/style/defs.ex | 19 +++++++++++++------ test/style/defs_test.exs | 2 ++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d31e73cf..fbb76561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ they can and will change without that change being reflected in Styler's semanti ### Fixes +* don't blow up on `def function_head_with_no_body_nor_parens` (#185, h/t @ypconstante) * fix `with` arrow replacement + redundant body removal creating invalid statements (#184, h/t @JesseHerrick) * allow Kernel unary `!` and `not` as valid pipe starts (#183, h/t @nherzing) diff --git a/lib/style/defs.ex b/lib/style/defs.ex index 1e76e54c..fb344e78 100644 --- a/lib/style/defs.ex +++ b/lib/style/defs.ex @@ -52,13 +52,20 @@ defmodule Styler.Style.Defs do first_line = meta[:line] last_line = head_meta[:closing][:line] - if first_line == last_line do + cond do + # weird `def fun`, nothing else + is_nil(last_line) -> + {:skip, zipper, ctx} + # Already collapsed - {:skip, zipper, ctx} - else - comments = Style.displace_comments(ctx.comments, first_line..last_line) - node = {def, meta, [Style.set_line(head, meta[:line])]} - {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} + first_line == last_line -> + {:skip, zipper, ctx} + + # I just felt like this clause deserved a comment too. It's my favorite one + true -> + comments = Style.displace_comments(ctx.comments, first_line..last_line) + node = {def, meta, [Style.set_line(head, first_line)]} + {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} end end diff --git a/test/style/defs_test.exs b/test/style/defs_test.exs index b549ac1f..d80f5871 100644 --- a/test/style/defs_test.exs +++ b/test/style/defs_test.exs @@ -98,6 +98,8 @@ defmodule Styler.Style.DefsTest do end test "no body" do + assert_style "def no_body_nor_parens_yikes!" + assert_style( """ # Top comment From 1661f4181d3289f6915b520a8d7f85d24538551f Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 7 Aug 2024 08:35:24 -0600 Subject: [PATCH 02/86] minor optimization --- lib/style/defs.ex | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/lib/style/defs.ex b/lib/style/defs.ex index fb344e78..f1670fd5 100644 --- a/lib/style/defs.ex +++ b/lib/style/defs.ex @@ -52,20 +52,13 @@ defmodule Styler.Style.Defs do first_line = meta[:line] last_line = head_meta[:closing][:line] - cond do - # weird `def fun`, nothing else - is_nil(last_line) -> - {:skip, zipper, ctx} - - # Already collapsed - first_line == last_line -> - {:skip, zipper, ctx} - - # I just felt like this clause deserved a comment too. It's my favorite one - true -> - comments = Style.displace_comments(ctx.comments, first_line..last_line) - node = {def, meta, [Style.set_line(head, first_line)]} - {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} + # Already collapsed or it's a bodyless/paramless `def fun` + if first_line == last_line || is_nil(last_line) do + {:skip, zipper, ctx} + else + comments = Style.displace_comments(ctx.comments, first_line..last_line) + node = {def, meta, [Style.set_line(head, first_line)]} + {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} end end From b3ccc02f0fbaa25cadc970dda5af1434fd3cfcb3 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 8 Aug 2024 10:42:39 -0600 Subject: [PATCH 03/86] Wrap up 1.0.0 docs overhaul --- CHANGELOG.md | 564 ++---------------------------------- README.md | 12 +- docs/control_flow_macros.md | 17 +- docs/mix_configs.md | 77 ++++- docs/pipes.md | 122 ++++++-- mix.lock | 2 +- 6 files changed, 216 insertions(+), 578 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb76561..6e82a4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,44 +3,7 @@ **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. -## main - -### Improvements - -#### `with` - -* remove `with` structure with no left arrows in its head to be normal code (#174) -* `with true <- x(), do: y` => `if x(), do: y` (#173) - -### Fixes - -* don't blow up on `def function_head_with_no_body_nor_parens` (#185, h/t @ypconstante) -* fix `with` arrow replacement + redundant body removal creating invalid statements (#184, h/t @JesseHerrick) -* allow Kernel unary `!` and `not` as valid pipe starts (#183, h/t @nherzing) - -## 1.0.0-rc.2 - -### Fixes - -* fix `Map.drop(x, [a | b])` registering as a chance to refactor to `Map.delete` - -## 1.0.0-rc.1 - -### Improvements - -* Lots of documentation added. Nearly done and ready for 1.0.0. -* `Enum.into(x, [])` => `Enum.to_list(x)` -* `Enum.into(x, [], mapper)` => `Enum.map(x, mapper)` -* `a |> Enum.map(m) |> Enum.join()` to `map_join(a, m)`. we already did this for `join/2`, but missed the case for `join/1` - -## 1.0.0-rc.0 - -At this point, 1.0.0 feels feature complete. Two things remains for a full release: - -1. feedback! -2. documentation overhaul! [monitor progress here](https://github.com/adobe/elixir-styler/pull/166) - -### Improvements +## 1.0.0 Styler's two biggest outstanding bugs have been fixed, both related to compilation breaking during module directive organization. One was references to aliases being moved above where the aliases were declared, and the other was similarly module directives being moved after their uses in module directives. @@ -48,6 +11,10 @@ In both cases, Styler is now smart enough to auto-apply the fixes we recommended Other than that, a slew of powerful new features have been added, the neatest one (in the author's opinion anyways) being Alias Lifting. +Thanks to everyone who reported bugs that contributed to all the fixes released in 1.0.0 as well. + +### Improvements + #### Alias Lifting Along the lines of `Credo.Check.Design.AliasUsage`, Styler now "lifts" deeply nested aliases (depth >= 3, ala `A.B.C....`) that are used more than once. @@ -116,6 +83,18 @@ Styler now organizes `Mix.Config.config/2,3` stanzas according to erlang term so See the moduledoc for `Styler.Style.Configs` for more. +#### Pipe Optimizations + +* `Enum.into(x, [])` => `Enum.to_list(x)` +* `Enum.into(x, [], mapper)` => `Enum.map(x, mapper)` +* `a |> Enum.map(m) |> Enum.join()` to `map_join(a, m)`. we already did this for `join/2`, but missed the case for `join/1` +* `lhs |> Enum.reverse() |> Kernel.++(enum)` => `lhs |> Enum.reverse(enum)` + +#### `with` styles + +* remove `with` structure with no left arrows in its head to be normal code (#174) +* `with true <- x(), do: y` => `if x(), do: y` (#173) + #### Everything Else * `if`/`unless`: invert if and unless with `!=` or `!==`, like we do for `!` and `not` #132 @@ -124,10 +103,13 @@ See the moduledoc for `Styler.Style.Configs` for more. (`"\"\"\"\""` -> `~s("""")`) (`Credo.Check.Readability.StringSigils`) #146 * `Map.drop(foo, [single_key])` => `Map.delete(foo, single_key)` #161 (also in pipes) * `Keyword.drop(foo, [single_key])` => `Keyword.delete(foo, single_key)` #161 (also in pipes) -* `lhs |> Enum.reverse() |> Kernel.++(enum)` => `lhs |> Enum.reverse(enum)` ### Fixes +* don't blow up on `def function_head_with_no_body_nor_parens` (#185, h/t @ypconstante) +* fix `with` arrow replacement + redundant body removal creating invalid statements (#184, h/t @JesseHerrick) +* allow Kernel unary `!` and `not` as valid pipe starts (#183, h/t @nherzing) +* fix `Map.drop(x, [a | b])` registering as a chance to refactor to `Map.delete` * `alias`: expands aliases when moving an alias after another directive that relied on it (#137) * module directives: various fixes for unreported obscure crashes * pipes: fix a comment-shifting scenario when unpiping @@ -139,506 +121,4 @@ See the moduledoc for `Styler.Style.Configs` for more. * drop support for elixir `1.14` * ModuleDirectives: group callback attributes (`before_compile after_compile after_verify`) with nondirectives (previously, were grouped with `use`, their relative order maintained). to keep the desired behaviour, you can make new `use` macros that wrap these callbacks. Apologies if this makes using Styler untenable for your codebase, but it's probably not a good tool for macro-heavy libraries. -* sorting configs for the first time can change your configuration; see `Styler.Style.Configs` moduledoc for more - -## v0.11.9 - -### Improvements - -* pipes: check for `Stream.foo` equivalents to `Enum.foo` in a few more cases - -### Fixes - -* pipes: `|> then(&(&1 op y))` rewrites with `|> Kernel.op(y)` as long as the operator is defined in `Kernel`; skips the rewrite otherwise (h/t @kerryb for the report & @saveman71 for the fix) - -## v0.11.8 - -Two releases in one day!? @koudelka made too good a point about `Map.new` not being special... - -### Improvements - -* pipes: treat `MapSet.new` and `Keyword.new` the same way we do `Map.new` (h/t @koudelka) -* pipes: treat `Stream.map` the same as `Enum.map` when piped `|> Enum.into` - -## v0.11.7 - -### Improvements - -* deprecations: `~R` -> `~r`, `Date.range/2` -> `Date.range/3` with decreasing dates (h/t @milmazz) -* if: rewrite `if not x, do: y` => `unless x, do: y` -* pipes: `|> Enum.map(foo) |> Map.new()` => `|> Map.new(foo)` -* pipes: remove unnecessary `then/2` on named function captures: `|> then(&foo/1)` => `|> foo()`, `|> then(&foo(&1, ...))` => `|> foo(...)` (thanks to @tfiedlerdejanze for the idea + impl!) - -## v0.11.6 - -### Fixes - -* directives: maintain order of module compilation callbacks (`@before_compile` etc) relative to `use` statements (Closes #120, h/t @frankdugan3) - -## v0.11.5 - -### Fixes - -* fix parsing ranges with non-trivial integer bounds like `x..y` (Closes #119, h/t @maennchen) - -## v0.11.4 - -### Improvements - -Shoutout @milmazz for all the deprecation work below =) - -* Deprecations: Rewrite 1.16 Deprecations (h/t @milmazz for all the work here) - * add `//1` step to `Enum.slice/2|String.slice/2` with decreasing ranges - * `File.stream!(file, options, line_or_bytes)` => `File.stream!(file, line_or_bytes, options)` -* Deprecations `Path.safe_relative_to/2` => `Path.safe_relative/2` - -## v0.11.3 - -### Fixes - -* directives: fix infinite loop when encountering `@spec import(...) :: ...` (Closes #115, h/t @kerryb) -* `with`: fix deletion of arrow-less `with` statements within function invocations - -## v0.11.2 - -### Fixes - -* `pipes`: fix unpiping do-blocks into variables when the parent expression is a function invocation - like `a(if x do y end |> z(), b)` (Closes #114, h/t @wkirschbaum) - -## v0.11.1 - -### Fixes - -* `with`: fix `with` replacement when it's the only child of a `do` or `->` block (Closes #107, h/t @kerryb -- turns out those edge cases _did_ exist in the wild!) - -## v0.11.0 - -### Improvements - -#### Comments - -Styler will no longer make comments jump around in any situation, and will move comments with the appropriate node in all cases but module directive rearrangement (where they'll just be left behind - sorry! we're still working on it). - -* Keep comments in logical places when rewriting if/unless/cond/with (#79, #97, #101, #103) - -#### With Statements - -This release has a slew of improvements for `with` statements. It's not surprising that there's lots of style rules for `with` given that just about any `case`, `if`, or even `cond do` could also be expressed as a `with`. They're very powerful! And with great power... - -* style trivial pattern matches ala `lhs <- rhs` to `lhs = rhs` (#86) -* style `_ <- rhs` to `rhs` -* style keyword `, do: ` to `do end` rather than wrapping multiple statements in parens -* style statements all the way to `if` statements when appropriate (#100) - -#### Other - -* Rewrite `{Map|Keyword}.merge(single_key: value)` to use `put/3` instead (#96) - -### Fixes - -* `with`: various edge cases we can only hope no one's encountered and thus never reported - -## v0.10.5 - -After being bitten by two of them in a row, Styler's test suite now makes sure that there are no -idempotency bugs as part of its tests. - -In short, we now have `assert style(x) == style(style(x))` as part of every test. Sorry for not thinking to include this before :) - -### Fixes - -* alias: fix single-module alias deletion newlines bug -* comments: ensure all generated nodes always include line meta (#101) - -## v0.10.4 - -### Improvements - -* alias: delete noop single-module aliases (`alias Foo`, #87, h/t @mgieger) - -### Fixes - -* pipes: unnest all pipe starts in one pass (`f(g(h(x))) |> j()` => `x |> h() |> g() |> f() |> j()`, #94, h/t @tomjschuster) - -## v0.10.3 - -### Improvements - -* charlists: leave charlist rewriting to elixir's formatter on elixir >= 1.15 - -### Fixes - -* charlists: rewrite empty charlist to use sigil (`''` => `~c""`) -* pipes: don't blow up extracting fully-qualified macros (`Foo.bar do end |> foo()`, #91, h/t @NikitaNaumenko) - -## v0.10.2 - -### Improvements - -* `with`: remove identity singleton else clause (eg `else {:error, e} -> {:error, e} end`, `else error -> error end`) - -## v0.10.1 - -### Fixes - -* Fix function head shrink-failures causing comments to jump into blocks (Closes #67, h/t @APB9785) - -## v0.10.0 - -### Improvements - -* hoist all block-starts to pipes to their own variables (makes styler play better with piped macros) - -### Fixes - -* fix pipes starting with a macro do-block creating invalid ast (#83, h/t @mhanberg) - -## v0.9.7 - -### Fixes - -* rewrite pipes starting with `quote` blocks like we do with `case|if|cond|with` blocks (#82, h/t @SteffenDE) - -## v0.9.6 - -### Breaking Change - -* removed `mix style` task - -## v0.9.5 - -### Fixes - -* fix mistaking `Timex.now/1` in a pipe for `Timex.now/0` (#66, h/t @sabiwara) - -### Removed style - -* stop rewriting `Timex.today/0` given that we allow `Timex.today/1` -- too inconsistent. - -## v0.9.4 - -### Improvements - -* `if` statements: drop `else` clauses whose body is simply `nil` - -## v0.9.3 - -### Fixes - -* fix `unless a do b else c end` rewrites to `if` not flopping do/else bodies! (#77, h/t @jcowgar) -* fix pipes styling ranges with steps (`a..b//c`) incorrectly (#76, h/t @cschmatzler) - -## v0.9.2 - -### Fixes - -* fix exception styling module attributes named `@def` (we confused them with real `def`s, whoops!) (#75, h/t @randycoulman) - -## v0.9.1 - -the boolean blocks edition! - -### Improvements - -* auto-fix `Credo.Check.Refactor.CondStatements` (detects any truthy atom, not just `true`) -* if/unless rewrites: - - `Credo.Check.Refactor.NegatedConditionsWithElse` - - `Credo.Check.Refactor.NegatedConditionsInUnless` - - `Credo.Check.Refactor.UnlessWithElse` - -## v0.9.0 - -the with statement edition! - -### Improvements - -* Added right-hand-pattern-matching rewrites to `for` and `with` left arrow expressions `<-` - (ex: `with map = %{} <- foo()` => `with %{} = map <- foo`) -* `with` statement rewrites, solving the following credo rules - * `Credo.Check.Readability.WithSingleClause` - * `Credo.Check.Refactor.RedundantWithClauseResult` - * `Credo.Check.Refactor.WithClauses` - -## v0.8.5 - -### Fixes - -* Fixed exception when encountering non-arrowed case statements ala `case foo, do: unquote(quoted)` (#69, h/t @brettinternet, nice) - -## v0.8.4 - -### Fixes - -* Timex related fixes (#66): - * Rewrite `Timex.now/1` to `DateTime.now!/1` instead of `DateTime.utc_now/1` - * Only rewrite `Timex.today/0`, don't change `Timex.today/1` - -## v0.8.3 - -### Improvements - -* DateTime rewrites (#62, ht @milmazz) - * `DateTime.compare` => `DateTime.{before/after}` (elixir >= 1.15) - * `Timex.now` => `DateTime.utc_now` - * `Timex.today` => `Date.utc_today` - -### Fixes - -* Pipes: add `!=`, `!==`, `===`, `and`, and `or` to list of valid infix operators (#64) - -## v0.8.2 - -### Fixes - -* Pipes always de-sugars keyword lists when unpiping them (#60) - -## v0.8.1 - -### Fixes - -* ModuleDirectives doesn't mistake variables for directives (#57, h/t @leandrocp) - -## v0.8.0 - -### Improvements (Bug Fix!?) - -* ModuleDirectives no longer throws comments around a file when hoisting directives up (#53) - -## v0.7.14 - -### Improvements - -* rewrite `Logger.warn/1,2` to `Logger.warning/1,2` due to Elixir 1.15 deprecation - -## v0.7.13 - -### Fixes - -* don't unpipe single-piped `unquote` expressions (h/t @elliottneilclark) - -## v0.7.12 - -### Fixes - -* fix 0-arity paren removal on metaprogramming creating uncompilable code (h/t @simonprev) - -## v0.7.11 - -### Fixes - -* fix crash from `mix style` running plugins as part of formatting (no longer runs formatter plugins) - -### Improvements - -* single-quote charlists are rewritten to use the `~c` sigil (`'foo'` -> `~c'foo'`) (h/t @fhunleth) -* `mix style` warns the user that Styler is primarily meant to be used as a plugin - -## v0.7.10 - -### Fixes - -* fix crash when encountering single-quote charlists (h/t @fhunleth) - -### Improvements - -* single-quote charlists are rewritten to use the `~c` sigil (`'foo'` -> `~c'foo'`) -* when encountering `_ = bar ->`, replace it with `bar ->` - -## v0.7.9 - -### Fixes - -* Fix a toggle state resulting from (ahem, nonsense) code like `_ = bar ->` encountering ParameterPatternMatching style - -## v0.7.8 - -### Fixes - -* Fix crash trying to remove 0-arity parens from metaprogramming ala `def unquote(foo)()` - -## v0.7.7 - -### Improvements - -* Rewrite `Enum.into/2,3` into `Map.new/1,2` when the collectable is `%{}` or `Map.new/0` - -## v0.7.6 - -### Fixes - -* Fix crash when single pipe had inner defs (h/t [@michallepicki](https://github.com/adobe/elixir-styler/issues/39)) - -## v0.7.5 - -### Fixes - -* Fix bug from `ParameterPatternMatching` implementation that re-ordered pattern matching in `cond do` `->` clauses - -## v0.7.4 - -### Features - -* Implement `Credo.Check.Readability.PreferImplicitTry` -* Implement `Credo.Check.Consistency.ParameterPatternMatching` for `def|defp|fn|case` - -## v0.7.3 - -### Features - -* Remove parens from 0-arity function definitions (`Credo.Check.Readability.ParenthesesOnZeroArityDefs`) - -## v0.7.2 - -### Features - -* Rewrite `case ... true -> ...; _ -> ...` to `if` statements as well - -## v0.7.1 - -### Features - -* Rewrite `case ... true / else ->` to be `if` statements - -## v0.7.0 - -### Features - -* `Styler.Style.Simple`: - * Optimize `Enum.reverse(foo) ++ bar` to `Enum.reverse(foo, bar)` -* `Styler.Style.Pipes`: - * Rewrite `|> (& ...).()` to `|> then(& ...)` (`Credo.Check.Readability.PipeIntoAnonymousFunctions`) - * Add parens to 1-arity pipe functions (`Credo.Check.Readability.OneArityFunctionInPipe`) - * Optimize `a |> Enum.reverse() |> Enum.concat(enum)` to `Enum.reverse(a, enum)` - -## v0.6.1 - -### Improvements - -* Better error handling: `mix format` will still format files if a style fails - -### Fixes - -* `mix style`: only run on `.ex` and `.exs` files -* `ModuleDirectives`: now expands `alias __MODULE__.{A, B}` (h/t [@adriankumpf](https://github.com/adriankumpf)) - -## v0.6.0 - -### Features - -* `mix style`: brought back to life for folks who want to incrementally introduce Styler - -### Fixes - -* `Styler.Style.Pipes`: - * include `x in y` and `^foo` (for ecto) as a valid pipe starts - * work even harder to keep rewrites on one line - -## v0.5.2 - -### Fixes - -* `ModuleDirectives`: handle dynamic module names -* `Pipes`: include `Ecto.Query.from` and `Query.from` as valid pipe starts - -## v0.5.1 - -### Improvements - -* Sped up styling just a little bit - -## v0.5.0 - -### Improvements - -* `Styler` now implements `Mix.Task.Format`, meaning it is now an Elixir formatter plugin. -See the README for new installation & usage instructions - -### Breaking Change! Wooo! - -* the `mix style` task has been removed - -## v0.4.1 - -### Improvements - -* `Pipes` rewrites `|> Enum.into(%{}[, mapper])` and `Enum.into(Map.new()[, mapper])` to `Map.new/1,2` calls - -## v0.4.0 - -### Improvements - -* `Pipes` rewrites some two-step processes into one, fixing these credo issues in pipe chains: - * `Credo.Check.Refactor.FilterCount` - * `Credo.Check.Refactor.MapJoin` - * `Credo.Check.Refactor.MapInto` - -### Fixes - -* `ModuleDirectives` handles even weirder places to hide your aliases (anonymous functions, in this case) -* `Pipes` tries even harder to keep single-pipe rewrites of invocations on one line - -## v0.3.1 - -### Fixes - -* `Pipes` - * fixed omission of `==` as a valid pipe start operator (h/t @peake100 for the issue) - * fixed rewrite of `a |> b`, where `b` was invoked without parenthesis - -## v0.3.0 - -### Improvements - -* Enabled `Defs` style and overhauled it to properly handles comments -* Optimized and tweaked `ModuleDirectives` style - * Now culls newlines between "groups" of the same directive - * sorts `@behaviour` directives - * orders directives within non defmodule contexts (eg, a `def do`) if there's at least one `alias|require|use|import` - -### Fixes - -* `Pipes` will try to keep single-pipe rewrites on one line - -## v0.2.0 - -### Improvements - -* Added `ModuleDirectives` style - * Note that this is potentially destructive in some rare cases. See moduledoc for more. - * This supersedes the `Aliases` style, which has been removed. -* `mix style -` reads and writes to stdin/stdout - -### Fixes - -* `Pipes` style is now aware of `unless` blocks - -## v0.1.1 - -### Improvements - -* Lots of README tweaking =) -* Optimized some Zipper operations -* Added `Simple` style, replacing the following Credo rule: - * `Credo.Check.Readability.LargeNumbers` - -### Fixes - -* Exceptions while parsing code now appropriately render filename rather than `nofile:xx` -* Fixed opaque `Zipper.path()` typespec implementation mismatches (thanks @sega-yarkin) -* Made `ex_doc` dev only, removing it as a dependency for users of Styler - -## v0.1.0 - -### Improvements - -* Initial release of Styler -* Added `Aliases` style, replacing the following Credo rules: - * `Credo.Check.Readability.AliasOrder` - * `Credo.Check.Readability.MultiAlias` - * `Credo.Check.Readability.UnnecessaryAliasExpansion` -* Added `Pipes` style, replacing the following Credo rules: - * `Credo.Check.Readability.BlockPipe` - * `Credo.Check.Readability.SinglePipe` - * `Credo.Check.Refactor.PipeChainStart` -* Added `Defs` style (currently disabled by default) +* sorting configs for the first time can change your configuration; see [Mix Configs docs](docs/mix_configs.md) for more diff --git a/README.md b/README.md index de88728c..a4a1b085 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,11 @@ You can learn more about the history, purpose and implementation of Styler from - auto-fixes [many credo rules](docs/credo.md), meaning you can turn them off to speed credo up - [keeps a strict module layout](docs/module_directives.md#directive-organization) -- alphabetizes module directives + - alphabetizes module directives - [extracts repeated aliases](docs/module_directives.md#alias-lifting) -- pipes and unpipes function calls based on the number of calls -- optimizes standard library calls (`a |> Enum.map(m) |> Enum.into(Map.new)` => `Map.new(a, m)`) +- [makes your pipe chains pretty as can be](docs/pipes.md) + - pipes and unpipes function calls based on the number of calls + - optimizes standard library calls (`a |> Enum.map(m) |> Enum.into(Map.new)` => `Map.new(a, m)`) - replaces strings with sigils when the string has many escaped quotes - ... and so much more @@ -24,15 +25,14 @@ You can learn more about the history, purpose and implementation of Styler from ## Who is Styler for? -Styler was designed for a large team (40+ engineers) working in a single codebase. It helps remove fiddly code review comments and removes failed linter CI slowdowns, helping teams get things done faster. Teams in similar situations might appreciate Styler. +Styler was designed for a **large team (40+ engineers) working in a single codebase. It helps remove fiddly code review comments and removes failed linter CI slowdowns, helping teams get things done faster. Teams in similar situations might appreciate Styler. Its automations are also extremely valuable for taming legacy elixir codebases or just refactoring in general. Some of its rewrites have inspired code actions in elixir language servers. Conversely, Styler probably _isn't_ a good match for: -- libraries - experimental, macro-heavy codebases -- small teams that don't want to think about code standards +- teams that don't care about code standards ## Installation diff --git a/docs/control_flow_macros.md b/docs/control_flow_macros.md index d4123a9b..39243034 100644 --- a/docs/control_flow_macros.md +++ b/docs/control_flow_macros.md @@ -97,26 +97,29 @@ if x, do: y ### "Erlang heritage" `case` true/false -> `if` -Trivial true/false `case` statements are rewritten to `if` statements. While this results in a [semantically different program](https://github.com/rrrene/credo/issues/564#issue-338349517), we argue that it results in a better program for maintainability. If the developer wants a their case statement to raise when receiving a non-boolean value as a feature of the program, they would better serve their callers by raising something more descriptive. +Trivial true/false `case` statements are rewritten to `if` statements. While this results in a [semantically different program](https://github.com/rrrene/credo/issues/564#issue-338349517), we argue that it results in a better program for maintainability. If the developer wants their case statement to raise when receiving a non-boolean value as a feature of the program, they would better serve their callers by raising something more descriptive. In other words, Styler leaves the code with better style, trumping obscure exception design :) ```elixir -# instead of this +# Styler will rewrite this even if the clause order is flipped, +# and if the `false` is replaced with a wildcard (`_`) case foo do true -> :ok false -> :error end -# do this +# styled: if foo do :ok else :error end +``` + +Per the argument above, if the `if` statement is an incorrect rewrite for your program, we recommend this manual fix rewrite: -# OR this. readers now know that the exception is an intentional design, -# rather than an accidental "feature" +```elixir case foo do true -> :ok false -> :error @@ -265,8 +268,6 @@ If the pattern of the final clause of the head is also the `with` statements `do with {:ok, a} <- foo(), {:ok, b} <- bar(a) do {:ok, b} -else - error -> error end # Styled: with {:ok, a} <- foo() do @@ -276,7 +277,7 @@ end ### Replace with `case` -A `with` statement with a single clause in the head and `else` is a really just a `case` clause putting on airs. +A `with` statement with a single clause in the head and an `else` body is really just a `case` statement putting on airs. ```elixir # Given: diff --git a/docs/mix_configs.md b/docs/mix_configs.md index c3b45e8e..70bf88fd 100644 --- a/docs/mix_configs.md +++ b/docs/mix_configs.md @@ -4,7 +4,7 @@ Mix Config files have their config stanzas sorted. Similar to the sorting of ali A file is considered a config file if -1. its path matches `config/.*\.exs` or `rel/overlays/.*\.exs` +1. its path matches `~r|config/.*\.exs|` `~r|rel/overlays/.*\.exs|` 2. the file has `import Config` Once a file is detected as a mix config, its `config/2,3` stanzas are grouped and ordered like so: @@ -13,6 +13,79 @@ Once a file is detected as a mix config, its `config/2,3` stanzas are grouped an - sort each group according to erlang term sorting - move all existing assignments between the config stanzas to above the stanzas (without changing their ordering) +## THIS CAN BREAK YOUR PROGRAM + +It's important to double check your configuration after running Styler on it for the first time. + +**First Use Advice**: To limit the size of changes Styler submits to a codebase, we recommend formatting only a few (or a single) files at a time and making pull requests for each. Only commit Styler as a new formatter plugin once each of these more dangerous changes has been safely committed to the codebase. + +Imagine your application configures the same value twice, once with an invalid or application breaking value, and then again with a correct value, like so: + +```elixir +string = "i am a string" +atom = :i_am_an_atom + +config :my_app, value_must_be_an_atom: string +... +... +config :my_app, value_must_be_an_atom: atom +``` + +When styler sorts the configuration file, this dormant mistake can become a bug if the sorting changes the order such that the invalid value takes precedence (aka comes last) + +```elixir +string = "i am a string" +atom = :i_am_an_atom + +# The value that must be an atom is now a string! +config :my_app, value_must_be_an_atom: atom +config :my_app, value_must_be_an_atom: string +``` + ## Examples -TODOs +Sorts configs by erlang term ordering: + +```elixir +# Given +import Config + +config :z, :x, :c +config :a, :b, :c +config :y, :x, :z +config :a, :c, :d + +# Styled: +import Config + +config :a, :b, :c +config :a, :c, :d + +config :y, :x, :z + +config :z, :x, :c +``` + +Non-config statements break the file up into chunks, where each chunk is sorted separately relative to itself. + +```elixir +# Given +import Config + +config :z, :x, :c +config :a, :b, :c +var = "value" +config :y, :x, var +config :a, :c, var + +# Styled: +import Config + +config :a, :b, :c +config :z, :x, :c + +var = "value" + +config :a, :c, var +config :y, :x, var +``` diff --git a/docs/pipes.md b/docs/pipes.md index 9958bcc6..5b8f065c 100644 --- a/docs/pipes.md +++ b/docs/pipes.md @@ -2,30 +2,114 @@ ## Pipe Start -- raw value -- blocks are extracted to variables -- ecto's `from` is allowed +Styler will ensure that the start of a pipechain is a 0-arity function, a raw value, or a variable. -## Piped function rewrites +```elixir +Enum.at(enum, 5) +|> IO.inspect() + +# Styled: +enum +|> Enum.at(5) +|> IO.inspect() +``` + +If the start of a pipe is a block expression, styler will create a new variable to store the result of that expression and make that variable the start of the pipe. + +```elixir +if a do + b +else + c +end +|> Enum.at(4) +|> IO.inspect() + +# Styled: +if_result = + if a do + b + else + c + end + +if_result +|> Enum.at(4) +|> IO.inspect() +``` + +### Add parenthesis to function calls in pipes + +```elixir +a |> b |> c |> d +# Styled: +a |> b() |> c() |> d() +``` + +### Remove Unnecessary `then/2` + +When the piped argument is being passed as the first argument to the inner function, there's no need for `then/2`. + +```elixir +a |> then(&f(&1, ...)) |> b() +# Styled: +a |> f(...)) |> b() +``` - add parens to function calls `|> fun |>` => `|> fun() |>` -- remove unnecessary `then/2`: `|> then(&f(&1, ...))` -> `|> f(...)` -- add `then/2` when defining anon funs in pipe `|> (& &1).() |>` => `|> then(& &1) |>` -## Piped function optimizations +### Add `then/2` when defining and calling anonymous functions in pipes + +```elixir +a |> (fn x -> x end).() |> c() +# Styled: +a |> then(fn x -> x end) |> c() +``` + +### Piped function optimizations + +Two function calls into one! Fewer steps is always nice. + +```elixir +# reverse |> concat => reverse/2 +a |> Enum.reverse() |> Enum.concat(enum) |> ... +# Styled: +a |> Enum.reverse(enum) |> ... + +# filter |> count => count(filter) +a |> Enum.filter(filterer) |> Enum.count() |> ... +# Styled: +a |> Enum.count(filterer) |> ... + +# map |> join => map_join +a |> Enum.map(mapper) |> Enum.join(joiner) |> ... +# Styled: +a |> Enum.map_join(joiner, mapper) |> ... + +# Enum.map |> X.new() => X.new(mapper) +# where X is one of: Map, MapSet, Keyword +a |> Enum.map(mapper) |> Map.new() |> ... +# Styled: +a |> Map.new(mapper) |> ... + +# Enum.map |> Enum.into(empty_collectable) => X.new(mapper) +# Where empty_collectable is one of `%{}`, `Map.new()`, `Keyword.new()`, `MapSet.new()` +# Given: +a |> Enum.map(mapper) |> Enum.into(%{}) |> ... +# Styled: +a |> Map.new(mapper) |> ... +``` -Two function calls into one! Tries to fit everything on one line when shrinking. +### Unpiping Single Pipes -| Before | After | -|--------|-------| -| `lhs \|> Enum.reverse() \|> Enum.concat(enum)` | `lhs \|> Enum.reverse(enum)` (also Kernel.++) | -| `lhs \|> Enum.filter(filterer) \|> Enum.count()` | `lhs \|> Enum.count(filterer)` | -| `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) \|> Map.new()` | `lhs \|> Map.new(mapper)` mapset & keyword also | +Styler rewrites pipechains with a single pipe to be function calls. Notably, this rule combined with the optimizations rewrites above means some chains with more than one pipe will also become function calls. -## Unpiping Single Pipes +```elixir +foo = bar |> baz() +# Styled: +foo = baz(bar) -- 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 +map = a |> Enum.map(mapper) |> Map.new() +# Styled: +map = Map.new(a, mapper) +``` diff --git a/mix.lock b/mix.lock index ad250b75..3339de94 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, - "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, From ea8c72308b317a3201170c976b4cc2f8a09a1c09 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 8 Aug 2024 10:43:23 -0600 Subject: [PATCH 04/86] 1.0.0 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index f128ae75..02eba042 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.0.0-rc.2" + @version "1.0.0" @url "https://github.com/adobe/elixir-styler" def project do From a9bc6890f54819ce234bdb0e1bdacd8fc9548ecc Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 27 Aug 2024 09:55:13 -0600 Subject: [PATCH 05/86] Add missing documentation. Closes #188 --- docs/pipes.md | 2 +- docs/styles.md | 194 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 161 insertions(+), 35 deletions(-) diff --git a/docs/pipes.md b/docs/pipes.md index 5b8f065c..bcf98ef7 100644 --- a/docs/pipes.md +++ b/docs/pipes.md @@ -53,7 +53,7 @@ When the piped argument is being passed as the first argument to the inner funct ```elixir a |> then(&f(&1, ...)) |> b() # Styled: -a |> f(...)) |> b() +a |> f(...) |> b() ``` - add parens to function calls `|> fun |>` => `|> fun() |>` diff --git a/docs/styles.md b/docs/styles.md index 3c333737..d3d26497 100644 --- a/docs/styles.md +++ b/docs/styles.md @@ -10,7 +10,12 @@ These apply to the piped versions as well 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. -* `"{\"errors\":[\"Not Authorized\"]}"` => `~s({"errors":["Not Authorized"]})` +```elixir +# Before +"{\"errors\":[\"Not Authorized\"]}" +# Styled +~s({"errors":["Not Authorized"]}) +``` ## Large Base 10 Numbers @@ -51,29 +56,58 @@ Note that all of the examples below also apply to pipes (`enum |> Enum.into(...) `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 | After | -|--------|-------| -| `Keyword.merge(kw, [key: :value])` | `Keyword.put(kw, :key, :value)` | -| `Map.merge(map, %{key: :value})` | `Map.put(map, :key, :value)` | -| `Map.merge(map, %{key => value})` | `Map.put(map, key, value)` | -| `map \|> Map.merge(%{key: value}) \|> foo()` | `map \|> Map.put(:key, value) \|> foo()` | +```elixir +# Before +Keyword.merge(kw, [key: :value]) +# Styled +Keyword.put(kw, :key, :value) + +# Before +Map.merge(map, %{key: :value}) +# Styled +Map.put(map, :key, :value) + +# Before +Map.merge(map, %{key => value}) +# Styled +Map.put(map, key, value) + +# Before +map |> Map.merge(%{key: value}) |> foo() +# Styled +map |> Map.put(:key, value) |> foo() +``` ## 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 | After | -|--------|-------| -| `Map.drop(map, [key])` | `Map.delete(map, key)`| -| `Keyword.drop(kw, [key])` | `Keyword.delete(kw, key)`| +```elixir +# Before +Map.drop(map, [key]) +# Styled +Map.delete(map, key) + +# Before +Keyword.drop(kw, [key]) +# Styled +Keyword.delete(kw, key) +``` ## `Enum.reverse/1` and concatenation -> `Enum.reverse/2` `Enum.reverse/2` optimizes a two-step reverse and concatenation into a single step. -| Before | After | -|--------|-------| -| `Enum.reverse(foo) ++ bar` | `Enum.reverse(foo, bar)`| -| `baz \|> Enum.reverse() \|> Enum.concat(bop)` | `Enum.reverse(baz, bop)`| +```elixir +# Before +Enum.reverse(foo) ++ bar +# Styled +Enum.reverse(foo, bar) + +# Before +baz |> Enum.reverse() |> Enum.concat(bop) +# Styled +Enum.reverse(baz, bop) +``` ## `Timex.now/0` ->` DateTime.utc_now/0` @@ -91,10 +125,17 @@ That's where Styler comes in! The examples below use `DateTime.compare/2`, but the same is also done for `NaiveDateTime|Time|Date.compare/2` -| Before | After | -|--------|-------| -| `DateTime.compare(start, end_date) == :gt` | `DateTime.after?(start, end_date)` | -| `DateTime.compare(start, end_date) == :lt` | `DateTime.before?(start, end_date)` | +```elixir +# Before +DateTime.compare(start, end_date) == :gt +# Styled +DateTime.after?(start, end_date) + +# Before +DateTime.compare(start, end_date) == :lt +# Styled +DateTime.before?(start, end_date) +``` ## Implicit Try @@ -140,12 +181,19 @@ end The author of the library disagrees with this style convention :) BUT, the wonderful thing about Styler is it lets you write code how _you_ want to, while normalizing it for reading for your entire team. The most important thing is not having to think about the style, and instead focus on what you're trying to achieve. -| Before | After | -|--------|-------| -| `def foo()` | `def foo`| -| `defp foo()` | `defp foo`| -| `defmacro foo()` | `defmacro foo`| -| `defmacrop foo()` | `defmacrop foo`| +```elixir +# Before +def foo() +defp foo() +defmacro foo() +defmacrop foo() + +# Styled +def foo +defp foo +defmacro foo +defmacrop foo +``` ## Elixir Deprecation Rewrites @@ -163,16 +211,94 @@ The author of the library disagrees with this style convention :) BUT, the wonde \* For both of the "decreasing range" changes, the rewrite can only be applied if the range is being passed as an argument to the function. ### 1.16+ -| Before | After | -|--------|-------| -|`File.stream!(file, options, line_or_bytes)` | `File.stream!(file, line_or_bytes, options)`| +File.stream! `:line` and `:bytes` deprecation -## Code Readability +```elixir +# Before +File.stream!(path, [encoding: :utf8, trim_bom: true], :line) +# Styled +File.stream!(path, :line, encoding: :utf8, trim_bom: true) +``` -- put matches on right -- `Credo.Check.Readability.PreferImplicitTry` +## Putting variable matching on the right -## Function Definitions +```elixir +# Before +case foo do + bar = %{baz: baz? = true} -> :baz? + opts = [[a = %{}] | _] -> a +end +# Styled: +case foo do + %{baz: true = baz?} = bar -> :baz? + [[%{} = a] | _] = opts -> a +end + +# Before +with {:ok, result = %{}} <- foo, do: result +# Styled +with {:ok, %{} = result} <- foo, do: result + +# Before +def foo(bar = %{baz: baz? = true}, opts = [[a = %{}] | _]), do: :ok +# Styled +def foo(%{baz: true = baz?} = bar, [[%{} = a] | _] = opts), do: :ok +``` + +## Drops superfluous `= _` in pattern matching + +```elixir +# Before +def foo(_ = bar), do: bar +# Styled +def foo(bar), do: bar + +# Before +case foo do + _ = bar -> :ok +end +# Styled +case foo do + _ = bar -> :ok +end +``` + +## Use Implicit Try -- Shrink multi-line function defs -- Put assignments on the right +```elixir +# before +def foo d + try do + throw_ball() + catch + :ball -> :caught + end +end + +# Styled: +def foo d + throw_ball() +catch + :ball -> :caught +end +``` + +## Shrink Function Definitions to One Line When Possible + +```elixir +# Before + +def save( + # Socket comment + %Socket{assigns: %{user: user, live_action: :new}} = initial_socket, + # Params comment + params + ), + do: :ok + +# Styled + +# Socket comment +# Params comment +def save(%Socket{assigns: %{user: user, live_action: :new}} = initial_socket, params), do: :ok +``` From ee0a6fa0bcc13e0b8ac360f7c39ea026b548ea81 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 27 Aug 2024 09:56:25 -0600 Subject: [PATCH 06/86] update readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4a1b085..28d45589 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ You can learn more about the history, purpose and implementation of Styler from - replaces strings with sigils when the string has many escaped quotes - ... and so much more -[See our Rewrites documentation on hexdocs for all the nitty-gritty on what all Styler does](https://hexdocs.pm/styler/) +[See our Rewrites documentation on hexdocs](https://hexdocs.pm/styler/styles.html) ## Who is Styler for? From c29d10dc3952af9ba11fb5d6499cc32dc8c0ead5 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 27 Aug 2024 10:00:08 -0600 Subject: [PATCH 07/86] add more verbiage re styler can add bugs. Closes #186 --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28d45589..39c25929 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,13 @@ Styler [will not add configuration](https://github.com/adobe/elixir-styler/pull/ Ultimately Styler is @adobe's internal tool that we're happy to share with the world. We're delighted if you like it as is, and just as excited if it's a starting point for you to make something even better for yourself. -## !Styler can change the behaviour of your program! +## WARNING: Styler can change the behaviour of your program! + +In some cases, this can introduce bugs. It goes without saying, but look over your changes before committing to main :) +(Here's an [example issue](https://github.com/adobe/elixir-styler/issues/186) where Styler unexpectedly changed the behaviour of a user's program.) + +A simple example of a way Styler changes the behaviour of code is the following rewrite: -The best example of the way in which Styler changes the meaning of your code is the following rewrite: ```elixir # Before: this case statement... case foo do From 3a66e42cd529a3a085f84b91cf97a8797988bf60 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 27 Aug 2024 10:03:01 -0600 Subject: [PATCH 08/86] reword warning to list examples of breakages --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 39c25929..c2350650 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,6 @@ Ultimately Styler is @adobe's internal tool that we're happy to share with the w ## WARNING: Styler can change the behaviour of your program! In some cases, this can introduce bugs. It goes without saying, but look over your changes before committing to main :) -(Here's an [example issue](https://github.com/adobe/elixir-styler/issues/186) where Styler unexpectedly changed the behaviour of a user's program.) A simple example of a way Styler changes the behaviour of code is the following rewrite: @@ -115,6 +114,12 @@ end Also good style! But Styler assumes that most of the time people just meant the `if` equivalent of the code, and so makes that change. If issues like this bother you, Styler probably isn't the tool you're looking for. +Other ways Styler can change your program: + +- [`with` statement rewrites](https://github.com/adobe/elixir-styler/issues/186) +- [config file sorting](https://hexdocs.pm/styler/mix_configs.html#this-can-break-your-program) +- and likely other ways. stay safe out there! + ## Thanks & Inspiration ### [Sourceror](https://github.com/doorgan/sourceror/) From f53cf7c952b9bda912b85fa9de20d011607e7795 Mon Sep 17 00:00:00 2001 From: Kem Tekinay Date: Fri, 30 Aug 2024 13:49:21 -0400 Subject: [PATCH 09/86] Update styles.md Fixed example for "drop superfluous _..." --- docs/styles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/styles.md b/docs/styles.md index d3d26497..69e04cea 100644 --- a/docs/styles.md +++ b/docs/styles.md @@ -259,7 +259,7 @@ case foo do end # Styled case foo do - _ = bar -> :ok + bar -> :ok end ``` From f2b269ece58ab4f550cb7f81bf6142d2efe11487 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 11 Sep 2024 09:05:34 -0600 Subject: [PATCH 10/86] Add Stream.run optimizations, fix optimizations shrinking pipes to one line (Closes #180) --- CHANGELOG.md | 10 ++++++++++ docs/pipes.md | 7 +++++++ lib/style/pipes.ex | 42 ++++++++++++++++++++++++++++++--------- test/style/pipes_test.exs | 39 +++++++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e82a4c3..ce56518d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ **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. +## main + +### Improvements + +* `pipes`: optimize `|> Stream.{each|map}(fun) |> Stream.run()` to `|> Enum.each(fun)` + +### Fixes + +* `pipes`: optimizations reducing 2 pipes to 1 no longer squeeze all pipes onto one line (#180) + ## 1.0.0 Styler's two biggest outstanding bugs have been fixed, both related to compilation breaking during module directive organization. One was references to aliases being moved above where the aliases were declared, and the other was similarly module directives being moved after their uses in module directives. diff --git a/docs/pipes.md b/docs/pipes.md index bcf98ef7..08673c94 100644 --- a/docs/pipes.md +++ b/docs/pipes.md @@ -98,6 +98,13 @@ a |> Map.new(mapper) |> ... a |> Enum.map(mapper) |> Enum.into(%{}) |> ... # Styled: a |> Map.new(mapper) |> ... + +# Given: +a |> b() |> Stream.each(fun) |> Stream.run() +a |> b() |> Stream.map(fun) |> Stream.run() +# Styled: +a |> b() |> Enum.each(fun) +a |> b() |> Enum.each(fun) ``` ### Unpiping Single Pipes diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index 034ffb2f..d2974650 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -159,8 +159,8 @@ defmodule Styler.Style.Pipes do # `pipe_chain(a, b, c)` generates the ast for `a |> b |> c` # the intention is to make it a little easier to see what the fix_pipe functions are matching on =) - defmacrop pipe_chain(a, b, c) do - quote do: {:|>, _, [{:|>, _, [unquote(a), unquote(b)]}, unquote(c)]} + defmacrop pipe_chain(pm, a, b, c) do + quote do: {:|>, unquote(pm), [{:|>, _, [unquote(a), unquote(b)]}, unquote(c)]} end # a |> fun => a |> fun() @@ -201,40 +201,58 @@ defmodule Styler.Style.Pipes do # `lhs |> Enum.reverse() |> Enum.concat(enum)` => `lhs |> Enum.reverse(enum)` defp fix_pipe( pipe_chain( + pm, lhs, {{:., _, [{_, _, [:Enum]}, :reverse]} = reverse, meta, []}, {{:., _, [{_, _, [:Enum]}, :concat]}, _, [enum]} ) ) do - {:|>, [line: meta[:line]], [lhs, {reverse, [line: meta[:line]], [enum]}]} + {:|>, pm, [lhs, {reverse, [line: meta[:line]], [enum]}]} end # `lhs |> Enum.reverse() |> Kernel.++(enum)` => `lhs |> Enum.reverse(enum)` defp fix_pipe( pipe_chain( + pm, lhs, {{:., _, [{_, _, [:Enum]}, :reverse]} = reverse, meta, []}, {{:., _, [{_, _, [:Kernel]}, :++]}, _, [enum]} ) ) do - {:|>, [line: meta[:line]], [lhs, {reverse, [line: meta[:line]], [enum]}]} + {:|>, pm, [lhs, {reverse, [line: meta[:line]], [enum]}]} end # `lhs |> Enum.filter(filterer) |> Enum.count()` => `lhs |> Enum.count(count)` defp fix_pipe( pipe_chain( + pm, lhs, {{:., _, [{_, _, [mod]}, :filter]}, meta, [filterer]}, {{:., _, [{_, _, [:Enum]}, :count]} = count, _, []} ) ) when mod in @enum do - {:|>, [line: meta[:line]], [lhs, {count, [line: meta[:line]], [filterer]}]} + {:|>, pm, [lhs, {count, [line: meta[:line]], [filterer]}]} + end + + # `lhs |> Stream.map(fun) |> Stream.run()` => `lhs |> Enum.each(fun)` + # `lhs |> Stream.each(fun) |> Stream.run()` => `lhs |> Enum.each(fun)` + defp fix_pipe( + pipe_chain( + pm, + lhs, + {{:., dm, [{a, am, [:Stream]}, map_or_each]}, fm, fa}, + {{:., _, [{_, _, [:Stream]}, :run]}, _, []} + ) + ) + when map_or_each in [:map, :each] do + {:|>, pm, [lhs, {{:., dm, [{a, am, [:Enum]}, :each]}, fm, fa}]} end # `lhs |> Enum.map(mapper) |> Enum.join(joiner)` => `lhs |> Enum.map_join(joiner, mapper)` defp fix_pipe( pipe_chain( + pm, lhs, {{:., dm, [{_, _, [mod]}, :map]}, em, map_args}, {{:., _, [{_, _, [:Enum]} = enum, :join]}, _, join_args} @@ -242,7 +260,7 @@ defmodule Styler.Style.Pipes do ) when mod in @enum do rhs = Style.set_line({{:., dm, [enum, :map_join]}, em, join_args ++ map_args}, dm[:line]) - {:|>, [line: dm[:line]], [lhs, rhs]} + {:|>, pm, [lhs, rhs]} end # `lhs |> Enum.map(mapper) |> Enum.into(empty_map)` => `lhs |> Map.new(mapper)` @@ -250,6 +268,7 @@ defmodule Styler.Style.Pipes do # `lhs |> Enum.map(mapper) |> Enum.into(collectable)` => `lhs |> Enum.into(collectable, mapper) defp fix_pipe( pipe_chain( + pm, lhs, {{:., dm, [{_, _, [mod]}, :map]}, _, [mapper]}, {{:., _, [{_, _, [:Enum]}, :into]} = into, _, [collectable]} @@ -268,15 +287,20 @@ defmodule Styler.Style.Pipes do {into, dm, [collectable, mapper]} end - Style.set_line({:|>, [], [lhs, rhs]}, dm[:line]) + Style.set_line({:|>, pm, [lhs, rhs]}, dm[:line]) end # `lhs |> Enum.map(mapper) |> Map.new()` => `lhs |> Map.new(mapper)` defp fix_pipe( - pipe_chain(lhs, {{:., _, [{_, _, [enum]}, :map]}, _, [mapper]}, {{:., _, [{_, _, [mod]}, :new]} = new, nm, []}) + pipe_chain( + pm, + lhs, + {{:., _, [{_, _, [enum]}, :map]}, _, [mapper]}, + {{:., _, [{_, _, [mod]}, :new]} = new, nm, []} + ) ) when mod in @collectable and enum in @enum do - Style.set_line({:|>, [], [lhs, {new, nm, [mapper]}]}, nm[:line]) + Style.set_line({:|>, pm, [lhs, {new, nm, [mapper]}]}, nm[:line]) end defp fix_pipe(node), do: node diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 7b5ddd20..5bd24c93 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -589,11 +589,14 @@ defmodule Styler.Style.PipesTest do assert_style( """ a + |> b() |> #{enum}.filter(fun) |> Enum.count() """, """ - Enum.count(a, fun) + a + |> b() + |> Enum.count(fun) """ ) @@ -621,9 +624,43 @@ defmodule Styler.Style.PipesTest do end end + test "Stream.{each/map}/Stream.run" do + assert_style("a |> Stream.each(fun) |> Stream.run()", "Enum.each(a, fun)") + assert_style("a |> Stream.map(fun) |> Stream.run()", "Enum.each(a, fun)") + + assert_style( + """ + a + |> foo + |> Stream.map(fun) + |> Stream.run() + """, + """ + a + |> foo() + |> Enum.each(fun) + """ + ) + end + test "map/join" do for enum <- ~w(Enum Stream) do assert_style("a |> #{enum}.map(mapper) |> Enum.join()", "Enum.map_join(a, mapper)") + + assert_style( + """ + a + |> b() + |> #{enum}.map(mapper) + |> Enum.join() + """, + """ + a + |> b() + |> Enum.map_join(mapper) + """ + ) + assert_style("a |> #{enum}.map(mapper) |> Enum.join(joiner)", "Enum.map_join(a, joiner, mapper)") end end From d6c90cdd12118b1831622a01986a4cfa20a9f4b5 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 23 Sep 2024 10:35:17 -0600 Subject: [PATCH 11/86] fix infinite loop rewriting negated if with empty do body. closes #196 --- CHANGELOG.md | 1 + lib/style/blocks.ex | 4 ++++ test/style/blocks_test.exs | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce56518d..ce20b8d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ they can and will change without that change being reflected in Styler's semanti ### Fixes * `pipes`: optimizations reducing 2 pipes to 1 no longer squeeze all pipes onto one line (#180) +* `if`: fix infinite loop rewriting negated if with empty do body `if x != y, do: (), else: :ok` (#196, h/t @itamm15) ## 1.0.0 diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index dbfe5ab8..ffb0ba0e 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -201,6 +201,10 @@ defmodule Styler.Style.Blocks do [negator, [do_block]] when is_negator(negator) -> zipper |> Zipper.replace({:unless, m, [invert(negator), [do_block]]}) |> run(ctx) + # drop `else end` + [head, [do_block, {_, {:__block__, _, []}}]] -> + {:cont, Zipper.replace(zipper, {:if, m, [head, [do_block]]}), ctx} + # drop `else: nil` [head, [do_block, {_, {:__block__, _, [nil]}}]] -> {:cont, Zipper.replace(zipper, {:if, m, [head, [do_block]]}), ctx} diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index 8444666f..400fd991 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -839,6 +839,24 @@ defmodule Styler.Style.BlocksTest do assert_style("if !!x, do: y", "if x, do: y") end + test "regression: negation with empty do body" do + assert_style( + """ + if a != b do + # comment + else + :ok + end + """, + """ + if a == b do + # comment + :ok + end + """ + ) + end + test "Credo.Check.Refactor.UnlessWithElse" do for negator <- ["!", "not "] do assert_style( From cde90d3b1b8624a065347b60e6048f3b9e22c8f9 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 23 Sep 2024 10:58:57 -0600 Subject: [PATCH 12/86] Remove unless from codebases (#194) --- CHANGELOG.md | 3 + docs/control_flow_macros.md | 7 +- lib/style/blocks.ex | 42 +++++----- lib/style/module_directives.ex | 2 +- lib/style/pipes.ex | 2 + test/style/blocks_test.exs | 139 +++++++++++++++++---------------- test/style/pipes_test.exs | 6 +- test/support/style_case.ex | 4 +- 8 files changed, 110 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce20b8d5..616ca034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ they can and will change without that change being reflected in Styler's semanti ### Improvements +The big change here is the rewrite/removal of `unless` due to [unless "eventually" being deprecated](https://github.com/elixir-lang/elixir/pull/13769#issuecomment-2334878315). Thanks to @janpieper and @ypconstante for bringing this up in #190. + +* `unless`: rewrite all `unless` to `if` (#190) * `pipes`: optimize `|> Stream.{each|map}(fun) |> Stream.run()` to `|> Enum.each(fun)` ### Fixes diff --git a/docs/control_flow_macros.md b/docs/control_flow_macros.md index 39243034..70a85460 100644 --- a/docs/control_flow_macros.md +++ b/docs/control_flow_macros.md @@ -11,13 +11,15 @@ The number of "blocks" in Elixir means there are many ways to write semantically We believe readability is enhanced by using the simplest api possible, whether we're talking about internal module function calls or standard-library macros. -### use `case`, `if`, or `unless` when... +### use `case`, `if`, or `cond` when... We advocate for `case` and `if` as the first tools to be considered for any control flow as they are the two simplest blocks. If a branch _can_ be expressed with an `if` statement, it _should_ be. Otherwise, `case` is the next best choice. In situations where developers might reach for an `if/elseif/else` block in other languages, `cond do` should be used. (`cond do` seems to see a paucity of use in the language, but many complex nested expressions or with statements can be improved by replacing them with a `cond do`). -`unless` is a special case of `if` meant to make code read as natural-language (citation needed). While it sometimes succeeds in this goal, its absence in most programming languages often makes it feel cumbersome to programmers with non-Ruby backgrounds. Thankfully, with Styler's help developers don't need to ever reach for `unless` - expressions that are "simpler" with its use are automatically rewritten to use it. +### use `unless` when... + +Never! `unless` [is being deprecated](https://github.com/elixir-lang/elixir/pull/13769#issuecomment-2334878315) and so should not be used. ### use `with` when... @@ -25,7 +27,6 @@ We advocate for `case` and `if` as the first tools to be considered for any cont > > - Uncle Ben - As the most powerful of the Kernel control-flow expressions, `with` requires the most cognitive overhead to understand. Its power means that we can use it as a replacement for anything we might express using a `case`, `if`, or `cond` (especially with the liberal application of small private helper functions). Unfortunately, this has lead to a proliferation of `with` in codebases where simpler expressions would have sufficed, meaning a lot of Elixir code ends up being harder for readers to understand than it needs to be. diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index ffb0ba0e..af2d6a89 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -176,31 +176,24 @@ defmodule Styler.Style.Blocks do end end - def run({{:unless, m, children}, _} = zipper, ctx) do - case children do - # Credo.Check.Refactor.UnlessWithElse - [{_, hm, _} = head, [_, _] = do_else] -> - zipper |> Zipper.replace({:if, m, [{:!, hm, [head]}, do_else]}) |> run(ctx) - - # Credo.Check.Refactor.NegatedConditionsInUnless - [negator, [{do_, do_body}]] when is_negator(negator) -> - zipper |> Zipper.replace({:if, m, [invert(negator), [{do_, do_body}]]}) |> run(ctx) - - _ -> - {:cont, zipper, ctx} - end + def run({{:unless, m, [head, do_else]}, _} = zipper, ctx) do + zipper + |> Zipper.replace({:if, m, [invert(head), do_else]}) + |> run(ctx) end def run({{:if, m, children}, _} = zipper, ctx) do case children do + # double negator + # if !!x, do: y[, else: ...] => if x, do: y[, else: ...] + [{_, _, [nb]} = na, do_else] when is_negator(na) and is_negator(nb) -> + zipper |> Zipper.replace({:if, m, [invert(nb), do_else]}) |> run(ctx) + # Credo.Check.Refactor.NegatedConditionsWithElse + # if !x, do: y, else: z => if x, do: z, else: y [negator, [{do_, do_body}, {else_, else_body}]] when is_negator(negator) -> zipper |> Zipper.replace({:if, m, [invert(negator), [{do_, else_body}, {else_, do_body}]]}) |> run(ctx) - # if not x, do: y => unless x, do: y - [negator, [do_block]] when is_negator(negator) -> - zipper |> Zipper.replace({:unless, m, [invert(negator), [do_block]]}) |> run(ctx) - # drop `else end` [head, [do_block, {_, {:__block__, _, []}}]] -> {:cont, Zipper.replace(zipper, {:if, m, [head, [do_block]]}), ctx} @@ -268,7 +261,7 @@ defmodule Styler.Style.Blocks do # c # d # ) - # @TODO would be nice to changeto + # @TODO would be nice to change to # a # b # c @@ -331,5 +324,16 @@ defmodule Styler.Style.Blocks do defp invert({:!=, m, [a, b]}), do: {:==, m, [a, b]} defp invert({:!==, m, [a, b]}), do: {:===, m, [a, b]} - defp invert({_, _, [expr]}), do: expr + defp invert({:==, m, [a, b]}), do: {:!=, m, [a, b]} + defp invert({:===, m, [a, b]}), do: {:!==, m, [a, b]} + defp invert({:!, _, [condition]}), do: condition + defp invert({:not, _, [condition]}), do: condition + + defp invert({fun, m, _} = ast) do + meta = [line: m[:line]] + + if fun == :|>, + do: {:|>, meta, [ast, {{:., meta, [Kernel, :!]}, meta, []}]}, + else: {:!, meta, [ast]} + end end diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index e385eb9d..623ce5f0 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -135,7 +135,7 @@ defmodule Styler.Style.ModuleDirectives do defp moduledoc({:__aliases__, m, aliases}) do name = aliases |> List.last() |> to_string() # module names ending with these suffixes will not have a default moduledoc appended - unless String.ends_with?(name, ~w(Test Mixfile MixProject Controller Endpoint Repo Router Socket View HTML JSON)) do + if !String.ends_with?(name, ~w(Test Mixfile MixProject Controller Endpoint Repo Router Socket View HTML JSON)) do Style.set_line(@moduledoc_false, m[:line] + 1) end end diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index d2974650..20761b3a 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -128,6 +128,8 @@ defmodule Styler.Style.Pipes do # |> ... var_name = case fun do + # unless will be rewritten to `if` statements in the Blocks Style + :unless -> :if fun when is_atom(fun) -> fun {:., _, [{:__aliases__, _, _}, fun]} when is_atom(fun) -> fun _ -> "block" diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index 400fd991..14fd9e85 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -822,42 +822,25 @@ defmodule Styler.Style.BlocksTest do end end - describe "if/unless" do - test "drops if else nil" do - assert_style("if a, do: b, else: nil", "if a, do: b") - - assert_style("if a do b else nil end", """ - if a do - b - end - """) - end - - test "if not => unless" do - assert_style("if not x, do: y", "unless x, do: y") - assert_style("if !x, do: y", "unless x, do: y") - assert_style("if !!x, do: y", "if x, do: y") - end - - test "regression: negation with empty do body" do + describe "unless to if" do + test "inverts all the things" do assert_style( """ - if a != b do - # comment + unless !! not true do + a else - :ok + b end """, """ - if a == b do - # comment - :ok + if true do + a + else + b end """ ) - end - test "Credo.Check.Refactor.UnlessWithElse" do for negator <- ["!", "not "] do assert_style( """ @@ -912,9 +895,7 @@ defmodule Styler.Style.BlocksTest do end """ ) - end - test "Credo.Check.Refactor.NegatedConditionsInUnless" do for negator <- ["!", "not "] do assert_style("unless #{negator} foo, do: :bar", "if foo, do: :bar") @@ -950,7 +931,69 @@ defmodule Styler.Style.BlocksTest do end end - test "Credo.Check.Refactor.NegatedConditionsWithElse" do + test "unless with pipes" do + assert_style "unless a |> b() |> c(), do: x", "if a |> b() |> c() |> Kernel.!(), do: x" + end + end + + describe "if" do + test "drops else nil" do + assert_style("if a, do: b, else: nil", "if a, do: b") + + assert_style("if a do b else nil end", """ + if a do + b + end + """) + + assert_style( + """ + if a != b do + # comment + else + :ok + end + """, + """ + if a == b do + # comment + :ok + end + """ + ) + end + + test "double negator rewrites" do + for a <- ~w(not !), block <- ["do: z", "do: z, else: zz"] do + assert_style "if #{a} (x != y), #{block}", "if x == y, #{block}" + assert_style "if #{a} (x !== y), #{block}", "if x === y, #{block}" + assert_style "if #{a} ! x, #{block}", "if x, #{block}" + assert_style "if #{a} not x, #{block}", "if x, #{block}" + end + + assert_style("if not x, do: y", "if not x, do: y") + assert_style("if !x, do: y", "if !x, do: y") + + assert_style( + """ + if !!val do + a + else + b + end + """, + """ + if val do + a + else + b + end + """ + ) + end + + test "single negator do/else swaps" do + # covers Credo.Check.Refactor.NegatedConditionsWithElse for negator <- ["!", "not "] do assert_style("if #{negator}foo, do: :bar, else: :baz", "if foo, do: :baz, else: :bar") @@ -994,44 +1037,6 @@ defmodule Styler.Style.BlocksTest do end end - test "recurses" do - assert_style( - """ - if !!val do - a - else - b - end - """, - """ - if val do - a - else - b - end - """ - ) - - assert_style( - """ - unless !! not true do - a - else - b - end - """, - """ - if true do - a - else - b - end - """ - ) - - assert_style("if not (a != b), do: c", "if a == b, do: c") - end - test "comments and flips" do assert_style( """ diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 5bd24c93..49763ea5 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -228,12 +228,12 @@ defmodule Styler.Style.PipesTest do |> wee() """, """ - unless_result = - unless foo do + if_result = + if !foo do bar end - wee(unless_result) + wee(if_result) """ ) end diff --git a/test/support/style_case.ex b/test/support/style_case.ex index 61a11f2a..f87c7bd3 100644 --- a/test/support/style_case.ex +++ b/test/support/style_case.ex @@ -70,7 +70,7 @@ defmodule Styler.StyleCase do # body blocks - for example, the block node for an anonymous function - don't have line meta # yes, i just did `&& case`. sometimes it's funny to write ugly things in my project that's all about style. # i believe they calls that one "irony" - is_body_block? = + body_block? = node == :__block__ && case up && Zipper.node(up) do # top of a snippet @@ -99,7 +99,7 @@ defmodule Styler.StyleCase do end end - unless line || is_body_block? do + if is_nil(line) and not body_block? do IO.puts("missing `:line` meta in node:") dbg(ast) From 926ad515207e4ceb58305bd580856a768c0daa21 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 23 Sep 2024 11:16:32 -0600 Subject: [PATCH 13/86] use `not` over `!` when rewriting `unless a (> >= < <= in) b` --- lib/style/blocks.ex | 11 +++-------- test/style/blocks_test.exs | 8 ++++++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index af2d6a89..659e2419 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -328,12 +328,7 @@ defmodule Styler.Style.Blocks do defp invert({:===, m, [a, b]}), do: {:!==, m, [a, b]} defp invert({:!, _, [condition]}), do: condition defp invert({:not, _, [condition]}), do: condition - - defp invert({fun, m, _} = ast) do - meta = [line: m[:line]] - - if fun == :|>, - do: {:|>, meta, [ast, {{:., meta, [Kernel, :!]}, meta, []}]}, - else: {:!, meta, [ast]} - end + defp invert({:|>, m, _} = ast), do: {:|>, m, [ast, {{:., m, [Kernel, :!]}, m, []}]} + defp invert({bool, m, [_, _]} = ast) when bool in ~w(> >= < <= in)a, do: {:not, m, [ast]} + defp invert({_, m, _} = ast), do: {:!, [line: m[:line]], [ast]} end diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index 14fd9e85..f93a1800 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -934,6 +934,14 @@ defmodule Styler.Style.BlocksTest do test "unless with pipes" do assert_style "unless a |> b() |> c(), do: x", "if a |> b() |> c() |> Kernel.!(), do: x" end + + test "kernel boolean operators" do + assert_style "unless a in b, do: x", "if a not in b, do: x" + + for bool <- ~w(> >= < <=)a do + assert_style "unless a #{bool} b, do: x", "if not (a #{bool} b), do: x" + end + end end describe "if" do From 912515793ca41716206a2b2ac41e96b79c288d8e Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 23 Sep 2024 11:19:05 -0600 Subject: [PATCH 14/86] actually, lets only do `not` for `in` --- lib/style/blocks.ex | 2 +- test/style/blocks_test.exs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index 659e2419..9763d4d3 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -329,6 +329,6 @@ defmodule Styler.Style.Blocks do defp invert({:!, _, [condition]}), do: condition defp invert({:not, _, [condition]}), do: condition defp invert({:|>, m, _} = ast), do: {:|>, m, [ast, {{:., m, [Kernel, :!]}, m, []}]} - defp invert({bool, m, [_, _]} = ast) when bool in ~w(> >= < <= in)a, do: {:not, m, [ast]} + defp invert({:in, m, [_, _]} = ast), do: {:not, m, [ast]} defp invert({_, m, _} = ast), do: {:!, [line: m[:line]], [ast]} end diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index f93a1800..7c140f05 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -935,12 +935,8 @@ defmodule Styler.Style.BlocksTest do assert_style "unless a |> b() |> c(), do: x", "if a |> b() |> c() |> Kernel.!(), do: x" end - test "kernel boolean operators" do + test "in" do assert_style "unless a in b, do: x", "if a not in b, do: x" - - for bool <- ~w(> >= < <=)a do - assert_style "unless a #{bool} b, do: x", "if not (a #{bool} b), do: x" - end end end From 42b5d4031650391e6dcb3d963ecb082787a289ff Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 23 Sep 2024 11:20:32 -0600 Subject: [PATCH 15/86] v1.1.0 --- CHANGELOG.md | 3 ++- mix.exs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 616ca034..35791058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ **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. - ## main +## 1.1.0 + ### Improvements The big change here is the rewrite/removal of `unless` due to [unless "eventually" being deprecated](https://github.com/elixir-lang/elixir/pull/13769#issuecomment-2334878315). Thanks to @janpieper and @ypconstante for bringing this up in #190. diff --git a/mix.exs b/mix.exs index 02eba042..a74b30cf 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.0.0" + @version "1.1.0" @url "https://github.com/adobe/elixir-styler" def project do From 79dce095e831411a9b7a0fd60a7181914ee99333 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 23 Sep 2024 12:38:34 -0600 Subject: [PATCH 16/86] update version in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2350650..f6978a52 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Add `:styler` as a dependency to your project's `mix.exs`: ```elixir def deps do [ - {:styler, "~> 1.0.0-rc.1", only: [:dev, :test], runtime: false}, + {:styler, "~> 1.1", only: [:dev, :test], runtime: false}, ] end ``` From 4aa18ba4fb7a2232bbe337fe43afff4e1387d67b Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 23 Sep 2024 13:42:41 -0600 Subject: [PATCH 17/86] Don't pipe into `Kernel.!` when rewriting unless with pipes --- CHANGELOG.md | 6 ++++++ lib/style/blocks.ex | 1 - mix.exs | 2 +- test/style/blocks_test.exs | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35791058..528daf90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +## 1.1.1 + +### Improvements + +* `unless`: rewrite `unless a |> b |> c` as `unless !(a |> b() |> c())` rather than `unless a |> b() |> c() |> Kernel.!()` (h/t @gregmefford) + ## 1.1.0 ### Improvements diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index 9763d4d3..ce3a849c 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -328,7 +328,6 @@ defmodule Styler.Style.Blocks do defp invert({:===, m, [a, b]}), do: {:!==, m, [a, b]} defp invert({:!, _, [condition]}), do: condition defp invert({:not, _, [condition]}), do: condition - defp invert({:|>, m, _} = ast), do: {:|>, m, [ast, {{:., m, [Kernel, :!]}, m, []}]} defp invert({:in, m, [_, _]} = ast), do: {:not, m, [ast]} defp invert({_, m, _} = ast), do: {:!, [line: m[:line]], [ast]} end diff --git a/mix.exs b/mix.exs index a74b30cf..edf369cb 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.1.0" + @version "1.1.1" @url "https://github.com/adobe/elixir-styler" def project do diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index 7c140f05..fae1144b 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -932,7 +932,7 @@ defmodule Styler.Style.BlocksTest do end test "unless with pipes" do - assert_style "unless a |> b() |> c(), do: x", "if a |> b() |> c() |> Kernel.!(), do: x" + assert_style "unless a |> b() |> c(), do: x", "if !(a |> b() |> c()), do: x" end test "in" do From 7fb05fa64e3e752027736186755c28a7cc911d76 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 18 Oct 2024 12:26:18 -0600 Subject: [PATCH 18/86] configs: improve comment handling when moving a small number of nodes. Closes #187 --- CHANGELOG.md | 4 ++++ lib/style/configs.ex | 34 +++++++++++++--------------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 528daf90..ce09af45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +### Improvements + +* Config Sorting: improve comment handling when only sorting a few nodes (Closes #187) + ## 1.1.1 ### Improvements diff --git a/lib/style/configs.ex b/lib/style/configs.ex index 9725bcf4..ab110a0b 100644 --- a/lib/style/configs.ex +++ b/lib/style/configs.ex @@ -85,21 +85,16 @@ defmodule Styler.Style.Configs do # so i'm trying to guess which change will be less damaging. # moving >=3 nodes hints that this is an initial run, where `set_lines` definitely outperforms. {nodes, comments} = - case change_count(nodes) do - 0 -> - {nodes, comments} - - n when n < 3 -> - {Style.fix_line_numbers(nodes, List.last(rest)), comments} - - _ -> - # after running, this block should take up the same # of lines that it did before - # the first node of `rest` is greater than the highest line in configs, assignments - # config line is the first line to be used as part of this block - # that will change when we consider preceding comments - {node_comments, _} = comments_for_node(config, comments) - first_line = min(List.last(node_comments)[:line] || cfm[:line], cfm[:line]) - set_lines(nodes, comments, first_line) + if changed?(nodes) do + # after running, this block should take up the same # of lines that it did before + # the first node of `rest` is greater than the highest line in configs, assignments + # config line is the first line to be used as part of this block + # that will change when we consider preceding comments + {node_comments, _} = comments_for_node(config, comments) + first_line = min(List.last(node_comments)[:line] || cfm[:line], cfm[:line]) + set_lines(nodes, comments, first_line) + else + {nodes, comments} end [config | left_siblings] = Enum.reverse(nodes, zm.l) @@ -120,14 +115,11 @@ defmodule Styler.Style.Configs do end end - defp change_count(nodes, n \\ 0) - - defp change_count([{_, am, _}, {_, bm, _} = b | tail], n) do - n = if am[:line] > bm[:line], do: n + 1, else: n - change_count([b | tail], n) + defp changed?([{_, am, _}, {_, bm, _} = b | tail]) do + if am[:line] > bm[:line], do: true, else: changed?([b | tail]) end - defp change_count(_, n), do: n + defp changed?(_), do: false defp set_lines(nodes, comments, first_line) do {nodes, comments, node_comments} = set_lines(nodes, comments, first_line, [], []) From b38b9906abb19587eb4b263df24b248fa2f4ba27 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 18 Oct 2024 12:26:39 -0600 Subject: [PATCH 19/86] v1.1.2 --- CHANGELOG.md | 2 ++ mix.exs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce09af45..ae6678cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +## 1.1.2 + ### Improvements * Config Sorting: improve comment handling when only sorting a few nodes (Closes #187) diff --git a/mix.exs b/mix.exs index edf369cb..2cabf780 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.1.1" + @version "1.1.2" @url "https://github.com/adobe/elixir-styler" def project do From 548fbf6cd8401b9ad29254c851d35c176535455e Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 18 Oct 2024 13:06:57 -0600 Subject: [PATCH 20/86] pipes: improve comment behaviour in optimizations (Closes #176) --- CHANGELOG.md | 4 ++ lib/style/pipes.ex | 18 +++--- test/style/pipes_test.exs | 116 ++++++++++++++++++++++++++++++-------- 3 files changed, 106 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae6678cd..15db7758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +### Fixes + +* `pipes`: optimizations are less likely to move comments (Closes #176) + ## 1.1.2 ### Improvements diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index 20761b3a..9aecaed3 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -261,7 +261,7 @@ defmodule Styler.Style.Pipes do ) ) when mod in @enum do - rhs = Style.set_line({{:., dm, [enum, :map_join]}, em, join_args ++ map_args}, dm[:line]) + rhs = {{:., dm, [enum, :map_join]}, em, Style.set_line(join_args, dm[:line]) ++ map_args} {:|>, pm, [lhs, rhs]} end @@ -272,7 +272,7 @@ defmodule Styler.Style.Pipes do pipe_chain( pm, lhs, - {{:., dm, [{_, _, [mod]}, :map]}, _, [mapper]}, + {{:., dm, [{_, _, [mod]}, :map]}, em, [mapper]}, {{:., _, [{_, _, [:Enum]}, :into]} = into, _, [collectable]} ) ) @@ -280,16 +280,16 @@ defmodule Styler.Style.Pipes do rhs = case collectable do {{:., _, [{_, _, [mod]}, :new]}, _, []} when mod in @collectable -> - {{:., dm, [{:__aliases__, dm, [mod]}, :new]}, dm, [mapper]} + {{:., dm, [{:__aliases__, dm, [mod]}, :new]}, em, [mapper]} {:%{}, _, []} -> - {{:., dm, [{:__aliases__, dm, [:Map]}, :new]}, dm, [mapper]} + {{:., dm, [{:__aliases__, dm, [:Map]}, :new]}, em, [mapper]} _ -> - {into, dm, [collectable, mapper]} + {into, em, [Style.set_line(collectable, dm[:line]), mapper]} end - Style.set_line({:|>, pm, [lhs, rhs]}, dm[:line]) + {:|>, pm, [lhs, rhs]} end # `lhs |> Enum.map(mapper) |> Map.new()` => `lhs |> Map.new(mapper)` @@ -297,12 +297,12 @@ defmodule Styler.Style.Pipes do pipe_chain( pm, lhs, - {{:., _, [{_, _, [enum]}, :map]}, _, [mapper]}, - {{:., _, [{_, _, [mod]}, :new]} = new, nm, []} + {{:., _, [{_, _, [enum]}, :map]}, em, [mapper]}, + {{:., _, [{_, _, [mod]}, :new]} = new, _, []} ) ) when mod in @collectable and enum in @enum do - Style.set_line({:|>, pm, [lhs, {new, nm, [mapper]}]}, nm[:line]) + {:|>, pm, [lhs, {Style.set_line(new, em[:line]), em, [mapper]}]} end defp fix_pipe(node), do: node diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 49763ea5..5c363551 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -749,29 +749,99 @@ defmodule Styler.Style.PipesTest do describe "comments" do test "unpiping doesn't move comment in anonymous function" do - assert_style """ - aliased = - aliases - |> MapSet.new(fn - {:alias, _, [{:__aliases__, _, aliases}]} -> List.last(aliases) - {:alias, _, [{:__aliases__, _, _}, [{_as, {:__aliases__, _, [as]}}]]} -> as - # alias __MODULE__ or other oddities - {:alias, _, _} -> nil - end) - - excluded_first = MapSet.union(aliased, @excluded_namespaces) - """, - """ - aliased = - MapSet.new(aliases, fn - {:alias, _, [{:__aliases__, _, aliases}]} -> List.last(aliases) - {:alias, _, [{:__aliases__, _, _}, [{_as, {:__aliases__, _, [as]}}]]} -> as - # alias __MODULE__ or other oddities - {:alias, _, _} -> nil - end) - - excluded_first = MapSet.union(aliased, @excluded_namespaces) - """ + assert_style( + """ + aliased = + aliases + |> MapSet.new(fn + {:alias, _, [{:__aliases__, _, aliases}]} -> List.last(aliases) + {:alias, _, [{:__aliases__, _, _}, [{_as, {:__aliases__, _, [as]}}]]} -> as + # alias __MODULE__ or other oddities + {:alias, _, _} -> nil + end) + + excluded_first = MapSet.union(aliased, @excluded_namespaces) + """, + """ + aliased = + MapSet.new(aliases, fn + {:alias, _, [{:__aliases__, _, aliases}]} -> List.last(aliases) + {:alias, _, [{:__aliases__, _, _}, [{_as, {:__aliases__, _, [as]}}]]} -> as + # alias __MODULE__ or other oddities + {:alias, _, _} -> nil + end) + + excluded_first = MapSet.union(aliased, @excluded_namespaces) + """ + ) end end + + test "optimizing with comments" do + assert_style( + """ + a + |> Enum.map(fn b -> + c + # a comment + d + end) + |> Enum.join(x) + |> Enum.each(...) + """, + """ + a + |> Enum.map_join(x, fn b -> + c + # a comment + d + end) + |> Enum.each(...) + """ + ) + + assert_style( + """ + a + |> Enum.map(fn b -> + c + # a comment + d + end) + |> Enum.into(x) + |> Enum.each(...) + """, + """ + a + |> Enum.into(x, fn b -> + c + # a comment + d + end) + |> Enum.each(...) + """ + ) + + assert_style( + """ + a + |> Enum.map(fn b -> + c + # a comment + d + end) + |> Keyword.new() + |> Enum.each(...) + """, + """ + a + |> Keyword.new(fn b -> + c + # a comment + d + end) + |> Enum.each(...) + """ + ) + end end From fe32a848e8b60ee2b663694158e59cc484e8bc93 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 7 Nov 2024 11:20:55 -0700 Subject: [PATCH 21/86] One-line unpiped assignments (#197) Closes #181 --- CHANGELOG.md | 4 ++ lib/style.ex | 30 +++++++++---- lib/style/pipes.ex | 91 ++++++++++++++++++++++++++++++-------- test/style/pipes_test.exs | 93 +++++++++++++++++++++++++++++++++++---- 4 files changed, 183 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15db7758..11460ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +### Improvements + +* `pipes`: unpiping assignments will make the assignment one-line when possible (Closes #181) + ### Fixes * `pipes`: optimizations are less likely to move comments (Closes #176) diff --git a/lib/style.ex b/lib/style.ex index 0f20ced8..6368293c 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -144,7 +144,7 @@ defmodule Styler.Style do comments |> Enum.map(fn comment -> if delta = Enum.find_value(shifts, fn {range, delta} -> comment.line in range && delta end) do - %{comment | line: comment.line + delta} + %{comment | line: max(comment.line + delta, 1)} else comment end @@ -230,14 +230,28 @@ defmodule Styler.Style do else: do_fix_lines(nodes, line, [node | acc]) end - # @TODO can i shortcut and just return end_of_expression[:line] when it's available? + def max_line([_ | _] = list), do: list |> List.last() |> max_line() + def max_line(ast) do - {_, max_line} = - Macro.prewalk(ast, 0, fn - {_, meta, _} = ast, max -> {ast, max(meta[:line] || max, max)} - ast, max -> {ast, max} - end) + meta = + case ast do + {_, meta, _} -> + meta + + _ -> + [] + end - max_line + if max_line = meta[:closing][:line] do + max_line + else + {_, max_line} = + Macro.prewalk(ast, 0, fn + {_, meta, _} = ast, max -> {ast, max(meta[:line] || max, max)} + ast, max -> {ast, max} + end) + + max_line + end end end diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index 9aecaed3..b6204ca7 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -50,25 +50,79 @@ defmodule Styler.Style.Pipes do {{:|>, _, [_, {:unquote, _, [_]}]}, _} = single_pipe_unquote_zipper -> {:cont, single_pipe_unquote_zipper, ctx} + # unpipe a single pipe zipper {{:|>, _, [lhs, rhs]}, _} = single_pipe_zipper -> - {_, meta, _} = lhs - # try to get everything on one line if we can - line = meta[:line] - {fun, meta, args} = rhs + {fun, rhs_meta, args} = rhs + {_, lhs_meta, _} = lhs + lhs_line = lhs_meta[:line] args = args || [] - - # no way multi-headed fn fits on one line; everything else (?) is just a matter of line length - args = - if Enum.any?(args, &match?({:fn, _, [{:->, _, _}, {:->, _, _} | _]}, &1)) do - Style.shift_line(args, -1) - else - Style.set_line(args, line) + # Every branch ends with the zipper being replaced with a function call + # `lhs |> rhs(...args)` => `rhs(lhs, ...args)` + # The differences are just figuring out what line number updates to make + # in order to get the following properties: + # + # 1. write the function call on one line if reasonable + # 2. keep comments well behaved (by doing meta line-number gymnastics) + + # if we see multiple `->`, there's no way we can online this + # future heuristics would include finding multiple lines + definitively_multiline? = + Enum.any?(args, fn + {:fn, _, [{:->, _, _}, {:->, _, _} | _]} -> true + {:fn, _, [{:->, _, [_, _]}]} -> true + _ -> false + end) + + if definitively_multiline? do + # shift rhs up to hang out with lhs + # 1 lhs + # 2 |> fun( + # 3 ...args... + # n ) + # => + # 1 fun(lhs + # 2 ... args... + # n-1 ) + + # because there could be comments between lhs and rhs, or the dev may have a bunch of empty lines, + # we need to calculate the distance between the two ("shift") + rhs_line = rhs_meta[:line] + shift = lhs_line - rhs_line + {fun, meta, args} = Style.shift_line(rhs, shift) + + # Not going to lie, no idea why the `shift + 1` is correct but it makes tests pass ¯\_(ツ)_/¯ + rhs_max_line = Style.max_line(rhs) + + comments = + ctx.comments + |> Style.displace_comments(lhs_line..(rhs_line - 1)) + |> Style.shift_comments(rhs_line..rhs_max_line, shift + 1) + + {:cont, Zipper.replace(single_pipe_zipper, {fun, meta, [lhs | args]}), %{ctx | comments: comments}} + else + # try to get everything on one line. + # formatter will kick it back to multiple if line-length doesn't accommodate + case Zipper.up(single_pipe_zipper) do + # if the parent is an assignment, put it on the same line as the `=` + {{:=, am, [{_, vm, _} = var, _single_pipe]}, _} = assignment_parent -> + # 1 var = + # 2 lhs + # 3 |> rhs(...args) + # => + # 1 var = rhs(lhs, ...args) + oneline_assignment = Style.set_line({:=, am, [var, {fun, rhs_meta, [lhs | args]}]}, vm[:line]) + # skip so we don't re-traverse + {:cont, Zipper.replace(assignment_parent, oneline_assignment), ctx} + + _ -> + # lhs + # |> rhs(...args) + # => + # rhs(lhs, ...) + oneline_function_call = Style.set_line({fun, rhs_meta, [lhs | args]}, lhs_line) + {:cont, Zipper.replace(single_pipe_zipper, oneline_function_call), ctx} end - - lhs = Style.set_line(lhs, line) - {_, meta, _} = Style.set_line({:ignore, meta, []}, line) - function_call_zipper = Zipper.replace(single_pipe_zipper, {fun, meta, [lhs | args]}) - {:cont, function_call_zipper, ctx} + end end non_pipe -> @@ -162,7 +216,7 @@ defmodule Styler.Style.Pipes do # `pipe_chain(a, b, c)` generates the ast for `a |> b |> c` # the intention is to make it a little easier to see what the fix_pipe functions are matching on =) defmacrop pipe_chain(pm, a, b, c) do - quote do: {:|>, unquote(pm), [{:|>, _, [unquote(a), unquote(b)]}, unquote(c)]} + quote do: {:|>, _, [{:|>, unquote(pm), [unquote(a), unquote(b)]}, unquote(c)]} end # a |> fun => a |> fun() @@ -286,7 +340,8 @@ defmodule Styler.Style.Pipes do {{:., dm, [{:__aliases__, dm, [:Map]}, :new]}, em, [mapper]} _ -> - {into, em, [Style.set_line(collectable, dm[:line]), mapper]} + {into, m, [collectable]} = Style.set_line({into, em, [collectable]}, dm[:line]) + {into, m, [collectable, mapper]} end {:|>, pm, [lhs, rhs]} diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 5c363551..93ba461f 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -432,6 +432,18 @@ defmodule Styler.Style.PipesTest do """ ) end + + test "onelines assignments" do + assert_style( + """ + x = + y + |> Enum.map(&f/1) + |> Enum.join() + """, + "x = Enum.map_join(y, &f/1)" + ) + end end describe "valid pipe starts & unpiping" do @@ -677,16 +689,24 @@ defmodule Styler.Style.PipesTest do assert_style( """ + # a + # b a_multiline_mapper |> #{enum}.map(fn %{gets: shrunk, down: to_a_more_reasonable} -> + # c IO.puts "woo!" + # d {shrunk, to_a_more_reasonable} end) |> Enum.into(size) """, """ + # a + # b Enum.into(a_multiline_mapper, size, fn %{gets: shrunk, down: to_a_more_reasonable} -> + # c IO.puts("woo!") + # d {shrunk, to_a_more_reasonable} end) """ @@ -751,16 +771,16 @@ defmodule Styler.Style.PipesTest do test "unpiping doesn't move comment in anonymous function" do assert_style( """ - aliased = - aliases - |> MapSet.new(fn - {:alias, _, [{:__aliases__, _, aliases}]} -> List.last(aliases) - {:alias, _, [{:__aliases__, _, _}, [{_as, {:__aliases__, _, [as]}}]]} -> as - # alias __MODULE__ or other oddities - {:alias, _, _} -> nil - end) + aliased = + aliases + |> MapSet.new(fn + {:alias, _, [{:__aliases__, _, aliases}]} -> List.last(aliases) + {:alias, _, [{:__aliases__, _, _}, [{_as, {:__aliases__, _, [as]}}]]} -> as + # alias __MODULE__ or other oddities + {:alias, _, _} -> nil + end) - excluded_first = MapSet.union(aliased, @excluded_namespaces) + excluded_first = MapSet.union(aliased, @excluded_namespaces) """, """ aliased = @@ -774,6 +794,61 @@ defmodule Styler.Style.PipesTest do excluded_first = MapSet.union(aliased, @excluded_namespaces) """ ) + + assert_style( + """ + foo = + # bar + bar + # baz + |> baz(fn -> + # a + a + # b + b + end) + """, + """ + # bar + # baz + foo = + baz(bar, fn -> + # a + a + # b + b + end) + """ + ) + + assert_style( + """ + foo = + # bar + bar + # baz + + + + |> baz(fn -> + # a + a + # b + b + end) + """, + """ + # bar + # baz + foo = + baz(bar, fn -> + # a + a + # b + b + end) + """ + ) end end From 60f79c7649c447c4d581e330d160f0a2c75b23bc Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 7 Nov 2024 12:39:11 -0700 Subject: [PATCH 22/86] Pipify: `d(a |> b |> c)` => `a |> b() |> c() |> d()`(#198) Closes #133 --- CHANGELOG.md | 1 + lib/style.ex | 3 + lib/style/blocks.ex | 3 +- lib/style/pipes.ex | 42 +++++++++ test/style/pipes_test.exs | 181 +++++++++++++++++++++++--------------- 5 files changed, 159 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11460ae8..1a1e535a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ they can and will change without that change being reflected in Styler's semanti ### Improvements +* `pipes`: pipe-ifies when first arg to a function is a pipe. reach out if this happens in unstylish places in your code (Closes #133) * `pipes`: unpiping assignments will make the assignment one-line when possible (Closes #181) ### Fixes diff --git a/lib/style.ex b/lib/style.ex index 6368293c..da93416b 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -83,6 +83,9 @@ defmodule Styler.Style do end end + def do_block?([{{:__block__, _, [:do]}, _body} | _]), do: true + def do_block?(_), do: false + @doc """ Returns a zipper focused on the nearest node where additional nodes can be inserted (a "block"). diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index ce3a849c..96e10ce9 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -79,9 +79,8 @@ defmodule Styler.Style.Blocks do def run({{:with, with_meta, children}, _} = zipper, ctx) when is_list(children) do # a std lib `with` block will have at least one left arrow and a `do` body. anything else we skip ¯\_(ツ)_/¯ arrow_or_match? = &(left_arrow?(&1) || match?({:=, _, _}, &1)) - do_block? = &match?([{{:__block__, _, [:do]}, _body} | _], &1) - if Enum.any?(children, arrow_or_match?) and Enum.any?(children, do_block?) do + if Enum.any?(children, arrow_or_match?) and Enum.any?(children, &Style.do_block?/1) do {preroll, children} = children |> Enum.map(fn diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index b6204ca7..76a97b44 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -130,6 +130,48 @@ defmodule Styler.Style.Pipes do end end + # a(b |> c[, ...args]) + # The first argument to a function-looking node is a pipe. + # Maybe pipe the whole thing? + def run({{f, m, [{:|>, _, _} = pipe | args]}, _} = zipper, ctx) do + parent = + case Zipper.up(zipper) do + {{parent, _, _}, _} -> parent + _ -> nil + end + + stringified = is_atom(f) && to_string(f) + + cond do + # this is likely a macro + # assert a |> b() |> c() + !m[:closing] -> + {:cont, zipper, ctx} + + # leave bools alone as they often read better coming first, like when prepended with `not` + # [not ]is_nil(a |> b() |> c()) + stringified && (String.starts_with?(stringified, "is_") or String.ends_with?(stringified, "?")) -> + {:cont, zipper, ctx} + + # string interpolation, module attribute assignment, or prettier bools with not + parent in [:"::", :@, :not] -> + {:cont, zipper, ctx} + + # double down on being good to exunit macros, and any other special ops + # ..., do: assert(a |> b |> c) + # not (a |> b() |> c()) + f in [:assert, :refute | @special_ops] -> + {:cont, zipper, ctx} + + # if a |> b() |> c(), do: ... + Enum.any?(args, &Style.do_block?/1) -> + {:cont, zipper, ctx} + + true -> + {:cont, Zipper.replace(zipper, {:|>, m, [pipe, {f, m, args}]}), ctx} + end + end + def run(zipper, ctx), do: {:cont, zipper, ctx} defp fix_pipe_start({pipe, zmeta} = zipper) do diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 93ba461f..0da3eb5d 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -90,7 +90,7 @@ defmodule Styler.Style.PipesTest do y end - a(foo(if_result), b) + if_result |> foo() |> a(b) """ ) end @@ -767,8 +767,8 @@ defmodule Styler.Style.PipesTest do end end - describe "comments" do - test "unpiping doesn't move comment in anonymous function" do + describe "comments and..." do + test "unpiping" do assert_style( """ aliased = @@ -850,73 +850,116 @@ defmodule Styler.Style.PipesTest do """ ) end + + test "optimizing" do + assert_style( + """ + a + |> Enum.map(fn b -> + c + # a comment + d + end) + |> Enum.join(x) + |> Enum.each(...) + """, + """ + a + |> Enum.map_join(x, fn b -> + c + # a comment + d + end) + |> Enum.each(...) + """ + ) + + assert_style( + """ + a + |> Enum.map(fn b -> + c + # a comment + d + end) + |> Enum.into(x) + |> Enum.each(...) + """, + """ + a + |> Enum.into(x, fn b -> + c + # a comment + d + end) + |> Enum.each(...) + """ + ) + + assert_style( + """ + a + |> Enum.map(fn b -> + c + # a comment + d + end) + |> Keyword.new() + |> Enum.each(...) + """, + """ + a + |> Keyword.new(fn b -> + c + # a comment + d + end) + |> Enum.each(...) + """ + ) + end end - test "optimizing with comments" do - assert_style( - """ - a - |> Enum.map(fn b -> - c - # a comment - d - end) - |> Enum.join(x) - |> Enum.each(...) - """, - """ - a - |> Enum.map_join(x, fn b -> - c - # a comment - d - end) - |> Enum.each(...) - """ - ) - - assert_style( - """ - a - |> Enum.map(fn b -> - c - # a comment - d - end) - |> Enum.into(x) - |> Enum.each(...) - """, - """ - a - |> Enum.into(x, fn b -> - c - # a comment - d - end) - |> Enum.each(...) - """ - ) - - assert_style( - """ - a - |> Enum.map(fn b -> - c - # a comment - d - end) - |> Keyword.new() - |> Enum.each(...) - """, - """ - a - |> Keyword.new(fn b -> - c - # a comment - d - end) - |> Enum.each(...) - """ - ) + describe "pipifying" do + test "no false positives" do + pipe = "a() |> b() |> c()" + assert_style pipe + assert_style String.replace(pipe, " |>", "\n|>") + assert_style "fn -> #{pipe} end" + assert_style "if #{pipe}, do: ..." + assert_style "x\n\n#{pipe}" + assert_style "@moduledoc #{pipe}" + assert_style "!(#{pipe})" + assert_style "not foo(#{pipe})" + assert_style ~s<"\#{#{pipe}}"> + end + + test "pipifying" do + assert_style "d(a |> b |> c)", "a |> b() |> c() |> d()" + + assert_style( + """ + # d + d( + # a + a + # b + |> b + # c + |> c + ) + """, + """ + # d + # a + a + # b + |> b() + # c + |> c() + |> d() + """ + ) + end end end From 5016027975dbac6bdbf517c1c363074a3b844765 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 18 Nov 2024 23:42:58 -0700 Subject: [PATCH 23/86] 1.18 hard deprecations (#203) * `List.zip` => `Enum.zip` * `first..last = range` => `first..last//_ = range` --- lib/style/deprecations.ex | 17 ++++++++++++++++ test/style/deprecations_test.exs | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex index 68050083..f810e5a3 100644 --- a/lib/style/deprecations.ex +++ b/lib/style/deprecations.ex @@ -17,6 +17,20 @@ defmodule Styler.Style.Deprecations do def run({node, meta}, ctx), do: {:cont, {style(node), meta}, ctx} + # Deprecated in 1.18 + # rewrite patterns of `first..last = ...` to `first..last//_ = ...` + defp style({:=, m, [{:.., _, [_first, _last]} = range, rhs]}), do: {:=, m, [rewrite_range_match(range), rhs]} + defp style({:->, m, [[{:.., _, [_first, _last]} = range], rhs]}), do: {:->, m, [[rewrite_range_match(range)], rhs]} + defp style({:<-, m, [{:.., _, [_first, _last]} = range, rhs]}), do: {:<-, m, [rewrite_range_match(range), rhs]} + + defp style({def, dm, [{x, xm, params} | rest]}) when def in ~w(def defp)a and is_list(params), + do: {def, dm, [{x, xm, Enum.map(params, &rewrite_range_match/1)} | rest]} + + # Deprecated in 1.18 + # List.zip => Enum.zip + defp style({{:., dm_, [{:__aliases__, am, [:List]}, :zip]}, fm, arg}), + do: {{:., dm_, [{:__aliases__, am, [:Enum]}, :zip]}, fm, arg} + # Logger.warn => Logger.warning # Started to emit warning after Elixir 1.15.0 defp style({{:., dm, [{:__aliases__, am, [:Logger]}, :warn]}, funm, args}), @@ -84,6 +98,9 @@ defmodule Styler.Style.Deprecations do defp style(node), do: node + defp rewrite_range_match({:.., dm, [first, {_, m, _} = last]}), do: {:"..//", dm, [first, last, {:_, m, nil}]} + defp rewrite_range_match(x), do: x + defp add_step_to_date_range?(first, last) do with {:ok, f} <- extract_date_value(first), {:ok, l} <- extract_date_value(last), diff --git a/test/style/deprecations_test.exs b/test/style/deprecations_test.exs index 3d5e8ed9..cb9396fd 100644 --- a/test/style/deprecations_test.exs +++ b/test/style/deprecations_test.exs @@ -33,6 +33,40 @@ defmodule Styler.Style.DeprecationsTest do ) end + test "matching ranges" do + assert_style "first..last = range", "first..last//_ = range" + assert_style "^first..^last = range", "^first..^last//_ = range" + assert_style "first..last = x = y", "first..last//_ = x = y" + assert_style "y = first..last = x", "y = first..last//_ = x" + + assert_style "def foo(x..y), do: :ok", "def foo(x..y//_), do: :ok" + assert_style "def foo(a, x..y = z), do: :ok", "def foo(a, x..y//_ = z), do: :ok" + assert_style "def foo(%{a: x..y = z}), do: :ok", "def foo(%{a: x..y//_ = z}), do: :ok" + + assert_style "with a..b = c <- :ok, d..e <- :better, do: :ok", "with a..b//_ = c <- :ok, d..e//_ <- :better, do: :ok" + + assert_style( + """ + case x do + a..b = c -> :ok + d..e -> :better + end + """, + """ + case x do + a..b//_ = c -> :ok + d..e//_ -> :better + end + """ + ) + end + + test "List.zip/1" do + assert_style "List.zip(foo)", "Enum.zip(foo)" + assert_style "foo |> List.zip |> bar", "foo |> Enum.zip() |> bar()" + assert_style "foo |> List.zip", "Enum.zip(foo)" + end + describe "1.16 deprecations" do @describetag skip: Version.match?(System.version(), "< 1.16.0-dev") From 929d217753e11757d2624c6c64a129dc2d798e26 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 19 Nov 2024 11:04:07 -0800 Subject: [PATCH 24/86] docs and changelog --- CHANGELOG.md | 3 ++ docs/deprecations.md | 48 ++++++++++++++++++++ docs/styles.md | 101 ++++++++----------------------------------- 3 files changed, 70 insertions(+), 82 deletions(-) create mode 100644 docs/deprecations.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a1e535a..cd7001bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ they can and will change without that change being reflected in Styler's semanti * `pipes`: pipe-ifies when first arg to a function is a pipe. reach out if this happens in unstylish places in your code (Closes #133) * `pipes`: unpiping assignments will make the assignment one-line when possible (Closes #181) +* `deprecations`: 1.18 deprecations + * `List.zip` => `Enum.zip` + * `first..last = range` => `first..last//_ = range` ### Fixes diff --git a/docs/deprecations.md b/docs/deprecations.md new file mode 100644 index 00000000..1ed13297 --- /dev/null +++ b/docs/deprecations.md @@ -0,0 +1,48 @@ +## Elixir Deprecation Rewrites + +| Before | After | +|--------|-------| +| `Logger.warn` | `Logger.warning`| +| `Path.safe_relative_to/2` | `Path.safe_relative/2`| +| `~R/my_regex/` | `~r/my_regex/`| +| `Enum/String.slice/2` with decreasing ranges | add explicit steps to the range * | +| `Date.range/2` with decreasing range | `Date.range/3` *| +| `IO.read/bin_read` with `:all` option | replace `:all` with `:eof`| + +\* For both of the "decreasing range" changes, the rewrite can only be applied if the range is being passed as an argument to the function. + +### 1.18 Deprecations + +#### `List.zip/1` + +``` +# Before +List.zip(list) +# Styled +Enum.zip(list) +``` + +#### Range Matching Without Step + +```elixir +# Before +first..last = range +# Styled +first..last//_ = range + +# Before +def foo(x..y), do: :ok +# Styled +def foo(x..y//_), do: :ok +``` + +### 1.16+ + +`File.stream!/3` `:line` and `:bytes` deprecation + +```elixir +# Before +File.stream!(path, [encoding: :utf8, trim_bom: true], :line) +# Styled +File.stream!(path, :line, encoding: :utf8, trim_bom: true) +``` diff --git a/docs/styles.md b/docs/styles.md index 69e04cea..bf404aa3 100644 --- a/docs/styles.md +++ b/docs/styles.md @@ -109,7 +109,7 @@ baz |> Enum.reverse() |> Enum.concat(bop) Enum.reverse(baz, bop) ``` -## `Timex.now/0` ->` DateTime.utc_now/0` +## `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! @@ -137,43 +137,25 @@ DateTime.compare(start, end_date) == :lt DateTime.before?(start, end_date) ``` -## Implicit Try +## Implicit `try` Styler will rewrite functions whose entire body is a try/do to instead use the implicit try syntax, per Credo's `Credo.Check.Readability.PreferImplicitTry` -The following example illustrates the most complex case, but Styler happily handles just basic try do/rescue bodies just as easily. - -### Before - ```elixir -def foo() do +# before +def foo do try do - uh_oh() - rescue - exception -> {:error, exception} + throw_ball() catch - :a_throw -> {:error, :threw!} - else - try_has_an_else_clause? -> {:did_you_know, try_has_an_else_clause?} - after - :done + :ball -> :caught end end -``` - -### After -```elixir -def foo() do - uh_oh() -rescue - exception -> {:error, exception} +# Styled: +def foo do + throw_ball() catch - :a_throw -> {:error, :threw!} -else - try_has_an_else_clause? -> {:did_you_know, try_has_an_else_clause?} -after - :done + :ball -> :caught end ``` @@ -183,44 +165,19 @@ The author of the library disagrees with this style convention :) BUT, the wonde ```elixir # Before -def foo() -defp foo() -defmacro foo() -defmacrop foo() - -# Styled -def foo -defp foo -defmacro foo -defmacrop foo -``` - -## Elixir Deprecation Rewrites - -### 1.15+ - -| Before | After | -|--------|-------| -| `Logger.warn` | `Logger.warning`| -| `Path.safe_relative_to/2` | `Path.safe_relative/2`| -| `~R/my_regex/` | `~r/my_regex/`| -| `Enum/String.slice/2` with decreasing ranges | add explicit steps to the range * | -| `Date.range/2` with decreasing range | `Date.range/3` *| -| `IO.read/bin_read` with `:all` option | replace `:all` with `:eof`| - -\* For both of the "decreasing range" changes, the rewrite can only be applied if the range is being passed as an argument to the function. - -### 1.16+ -File.stream! `:line` and `:bytes` deprecation +def foo() do +defp foo() do +defmacro foo() do +defmacrop foo() do -```elixir -# Before -File.stream!(path, [encoding: :utf8, trim_bom: true], :line) # Styled -File.stream!(path, :line, encoding: :utf8, trim_bom: true) +def foo do +defp foo do +defmacro foo do +defmacrop foo do ``` -## Putting variable matching on the right +## Variable matching on the right ```elixir # Before @@ -263,26 +220,6 @@ case foo do end ``` -## Use Implicit Try - -```elixir -# before -def foo d - try do - throw_ball() - catch - :ball -> :caught - end -end - -# Styled: -def foo d - throw_ball() -catch - :ball -> :caught -end -``` - ## Shrink Function Definitions to One Line When Possible ```elixir From da3f3c955735d0c941de87ff90595628f6d2843f Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 20 Nov 2024 11:17:32 -0800 Subject: [PATCH 25/86] docs prep for 1.2 --- CHANGELOG.md | 2 ++ docs/deprecations.md | 63 +++++++++++++++++++++++++++++++++++--------- docs/pipes.md | 14 ++++++++-- mix.exs | 3 ++- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd7001bd..ffeaa3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +## 1.2.0 + ### Improvements * `pipes`: pipe-ifies when first arg to a function is a pipe. reach out if this happens in unstylish places in your code (Closes #133) diff --git a/docs/deprecations.md b/docs/deprecations.md index 1ed13297..bbc7c190 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -1,27 +1,34 @@ ## Elixir Deprecation Rewrites -| Before | After | -|--------|-------| -| `Logger.warn` | `Logger.warning`| -| `Path.safe_relative_to/2` | `Path.safe_relative/2`| -| `~R/my_regex/` | `~r/my_regex/`| -| `Enum/String.slice/2` with decreasing ranges | add explicit steps to the range * | -| `Date.range/2` with decreasing range | `Date.range/3` *| -| `IO.read/bin_read` with `:all` option | replace `:all` with `:eof`| +Elixir's built-in formatter now does its own rewrites via the `--migrate` flag, but doesn't quite cover every possible automated rewrite on the hard deprecations list. Styler tries to cover the rest! + +Styler will rewrite deprecations so long as their alternative is available on the given elixir version. In other words, Styler doesn't care what version of Elixir you're using when it applies the ex-1.18 rewrites - all it cares about is that the alternative is valid in your version of elixir. -\* For both of the "decreasing range" changes, the rewrite can only be applied if the range is being passed as an argument to the function. +### elixir `main` -### 1.18 Deprecations +https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations + +These deprecations will be released with Elixir 1.18 #### `List.zip/1` -``` +```elixir # Before List.zip(list) # Styled Enum.zip(list) ``` +#### `unless` + +This is covered by the Elixir Formatter with the `--migrate` flag, but Styler brings the same transformation to codebases on earlier versions of Elixir. + +Rewrite `unless x` to `if !x` + +### 1.17 + +[1.17 Deprecations](https://hexdocs.pm/elixir/1.17.0/changelog.html#4-hard-deprecations) + #### Range Matching Without Step ```elixir @@ -36,9 +43,11 @@ def foo(x..y), do: :ok def foo(x..y//_), do: :ok ``` -### 1.16+ +### 1.16 + +[1.16 Deprecations](https://hexdocs.pm/elixir/1.16.0/changelog.html#4-hard-deprecations) -`File.stream!/3` `:line` and `:bytes` deprecation +#### `File.stream!/3` `:line` and `:bytes` deprecation ```elixir # Before @@ -46,3 +55,31 @@ File.stream!(path, [encoding: :utf8, trim_bom: true], :line) # Styled File.stream!(path, :line, encoding: :utf8, trim_bom: true) ``` + +### Explicit decreasing ranges + +In all these cases, the rewrite will only be applied when literals are being passed to the function. In other words, variables will not be traced back to their assignment, and so it is still possible to receive deprecation warnings on this issue. + +```elixir +# Before +Enum.slice(x, 1..-2) +# Styled +Enum.slice(x, 1..-2//1) + +# Before +Date.range(~D[2000-01-01], ~D[1999-01-01]) +# Styled +Date.range(~D[2000-01-01], ~D[1999-01-01], -1) +``` + +### 1.15 + +[1.15 Deprecations](https://hexdocs.pm/elixir/1.15.0/changelog.html#4-hard-deprecations) + +| Before | After | +|--------|-------| +| `Logger.warn` | `Logger.warning`| +| `Path.safe_relative_to/2` | `Path.safe_relative/2`| +| `~R/my_regex/` | `~r/my_regex/`| +| `Date.range/2` with decreasing range | `Date.range/3` *| +| `IO.read/bin_read` with `:all` option | replace `:all` with `:eof`| diff --git a/docs/pipes.md b/docs/pipes.md index 08673c94..8d800984 100644 --- a/docs/pipes.md +++ b/docs/pipes.md @@ -1,6 +1,6 @@ -# Pipe Chains +## Pipe Chains -## Pipe Start +### Pipe Start Styler will ensure that the start of a pipechain is a 0-arity function, a raw value, or a variable. @@ -120,3 +120,13 @@ map = a |> Enum.map(mapper) |> Map.new() # Styled: map = Map.new(a, mapper) ``` + +### Pipe-ify + +If the first argument to a function call is a pipe, Styler makes the function call the final pipe of the chain. + +```elixir +d(a |> b |> c) +# Styled +a |> b() |> c() |> d() +``` diff --git a/mix.exs b/mix.exs index 2cabf780..9e804879 100644 --- a/mix.exs +++ b/mix.exs @@ -65,9 +65,10 @@ defmodule Styler.MixProject do extras: [ "CHANGELOG.md": [title: "Changelog"], "docs/styles.md": [title: "Basic Styles"], + "docs/deprecations.md": [title: "Deprecated Elixirisms"], "docs/pipes.md": [title: "Pipe Chains"], "docs/control_flow_macros.md": [title: "Control Flow Macros (if, case, ...)"], - "docs/mix_configs.md": [title: "Mix Configs (config/config.exs, ...)"], + "docs/mix_configs.md": [title: "Mix Configs (config/*.exs)"], "docs/module_directives.md": [title: "Module Directives (use, alias, ...)"], "docs/credo.md": [title: "Styler & Credo"], "README.md": [title: "Styler"] From 65007bade5bc3c906fd9434d86c7ad4984cc7481 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 20 Nov 2024 11:21:06 -0800 Subject: [PATCH 26/86] v1.2.0 --- README.md | 2 +- mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6978a52..840b221e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Add `:styler` as a dependency to your project's `mix.exs`: ```elixir def deps do [ - {:styler, "~> 1.1", only: [:dev, :test], runtime: false}, + {:styler, "~> 1.2", only: [:dev, :test], runtime: false}, ] end ``` diff --git a/mix.exs b/mix.exs index 9e804879..1bea0327 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.1.2" + @version "1.2.0" @url "https://github.com/adobe/elixir-styler" def project do From d930cab52758b2c8827ca26f4dcd0a882f7ff322 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 20 Nov 2024 11:39:27 -0800 Subject: [PATCH 27/86] correct docs on Enum.into --- docs/styles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/styles.md b/docs/styles.md index bf404aa3..10769d3e 100644 --- a/docs/styles.md +++ b/docs/styles.md @@ -47,7 +47,7 @@ Note that all of the examples below also apply to pipes (`enum |> Enum.into(...) | `Enum.into(enum, %{})` | `Map.new(enum)`| | `Enum.into(enum, Map.new())` | `Map.new(enum)`| | `Enum.into(enum, Keyword.new())` | `Keyword.new(enum)`| -| `Enum.into(enum, MapSet.new())` | `Keyword.new(enum)`| +| `Enum.into(enum, MapSet.new())` | `MapSet.new(enum)`| | `Enum.into(enum, %{}, fn x -> {x, x} end)` | `Map.new(enum, fn x -> {x, x} end)`| | `Enum.into(enum, [])` | `Enum.to_list(enum)` | | `Enum.into(enum, [], mapper)` | `Enum.map(enum, mapper)`| From 872ad3479f15d9e5e2ce4e33013c905db8af6e2b Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 21 Nov 2024 09:37:04 -0800 Subject: [PATCH 28/86] Fix pipifying pipes-in-pipes (Closes #204) --- CHANGELOG.md | 6 ++++++ lib/style/pipes.ex | 2 +- test/style/pipes_test.exs | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffeaa3dc..e3cb4bc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +## 1.2.1 + +### Fixes + +* `|>` don't pipify when the call is itself in a pipe (aka don't touch `a |> b(c |> d() |>e()) |> f()`) (Closes #204, h/t @paulswartz) + ## 1.2.0 ### Improvements diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index 76a97b44..cb269a7f 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -154,7 +154,7 @@ defmodule Styler.Style.Pipes do {:cont, zipper, ctx} # string interpolation, module attribute assignment, or prettier bools with not - parent in [:"::", :@, :not] -> + parent in [:"::", :@, :not, :|>] -> {:cont, zipper, ctx} # double down on being good to exunit macros, and any other special ops diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 0da3eb5d..3feae8ec 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -934,6 +934,14 @@ defmodule Styler.Style.PipesTest do assert_style ~s<"\#{#{pipe}}"> end + test "when it's not actually the first argument!" do + assert_style """ + a + |> M.f0(b |> M.f1() |> M.f2()) + |> M.f3() + """ + end + test "pipifying" do assert_style "d(a |> b |> c)", "a |> b() |> c() |> d()" From 4ec6ba7824ea626db12ff5751b6a754f1715250d Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 21 Nov 2024 09:39:37 -0800 Subject: [PATCH 29/86] v1.2.1 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 1bea0327..e7297a53 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.2.0" + @version "1.2.1" @url "https://github.com/adobe/elixir-styler" def project do From b7dcfd5057cfc0ef7311a22818f0030e293a93f8 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 22 Nov 2024 18:37:37 -0700 Subject: [PATCH 30/86] introduce `# styler:sort` comment directive (#205) closes #167 --- CHANGELOG.md | 66 +++++++++++ lib/style/comment_directives.ex | 67 +++++++++++ lib/styler.ex | 3 +- test/style/comment_directives_test.exs | 157 +++++++++++++++++++++++++ 4 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 lib/style/comment_directives.ex create mode 100644 test/style/comment_directives_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index e3cb4bc1..e40e0c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,72 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +### Improvements + +#### `# styler:sort` Styler's first comment directive + +Styler will now keep a user-designated list or wordlist (`~w` sigil) sorted as part of formatting via the use of comments. + +The intention is to remove comments to humans, like `# Please keep this list sorted!`, in favor of comments to robots: `# styler:sort`. Personally speaking, Styler is much better at alphabetical-order than I ever will be. + +To use the new directive, put it on the line before a list or wordlist. + +This example: + +```elixir +# styler:sort +[:c, :a, :b] + +# styler:sort +~w(a list of words) + +# styler:sort +@country_codes ~w( + en_US + po_PO + fr_CA + ja_JP +) + +# styler:sort +a_var = + [ + Modules, + In, + A, + List + ] +``` + +Would yield: + +```elixir +# styler:sort +[:a, :b, :c] + +# styler:sort +~w(a list of words) + +# styler:sort +@country_codes ~w( + en_US + fr_CA + ja_JP + po_PO +) + +# styler:sort +a_var = + [ + A, + In, + List, + Modules + ] +``` + +Sorting is done according to erlang term ordering, so lists with elements of multiple types will work just fine. + ## 1.2.1 ### Fixes diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex new file mode 100644 index 00000000..d41e0881 --- /dev/null +++ b/lib/style/comment_directives.ex @@ -0,0 +1,67 @@ +# Copyright 2024 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. + +defmodule Styler.Style.CommentDirectives do + @moduledoc "TODO" + + @behaviour Styler.Style + + alias Styler.Zipper + + def run(zipper, ctx) do + zipper = + ctx.comments + |> Enum.filter(&(&1.text == "# styler:sort")) + |> Enum.map(& &1.line) + |> Enum.reduce(zipper, fn line, zipper -> + found = + Zipper.find(zipper, fn + {_, meta, _} -> Keyword.get(meta, :line, -1) >= line + _ -> false + end) + + if found do + Zipper.update(found, &sort/1) + else + zipper + end + end) + + {:halt, zipper, ctx} + end + + defp sort({:__block__, meta, [list]}) when is_list(list) do + list = Enum.sort_by(list, fn {f, _, a} -> {f, a} end) + {:__block__, meta, [list]} + end + + defp sort({:sigil_w, sm, [{:<<>>, bm, [string]}, modifiers]}) do + # ew. gotta be a better way. + # this keeps indentation for the sigil via joiner, while prepend and append are the bookending whitespace + {prepend, joiner, append} = + case Regex.run(~r|^\s+|, string) do + # oneliner like `~w|c a b|` + nil -> {"", " ", ""} + # multline like + # `"\n a\n list\n long\n of\n static\n values\n"` + # ^^^^ `prepend` ^^^^ `joiner` ^^ `append` + # note that joiner and prepend are the same in a multiline (unsure if this is always true) + # @TODO: get all 3 in one pass of a regex. probably have to turn off greedy or something... + [joiner] -> {joiner, joiner, ~r|\s+$| |> Regex.run(string) |> hd()} + end + + string = string |> String.split() |> Enum.sort() |> Enum.join(joiner) + {:sigil_w, sm, [{:<<>>, bm, [prepend, string, append]}, modifiers]} + end + + defp sort({:=, m, [lhs, rhs]}), do: {:=, m, [lhs, sort(rhs)]} + defp sort({:@, m, [{a, am, [assignment]}]}), do: {:@, m, [{a, am, [sort(assignment)]}]} + defp sort(x), do: x +end diff --git a/lib/styler.ex b/lib/styler.ex index 8179c415..50e0d20b 100644 --- a/lib/styler.ex +++ b/lib/styler.ex @@ -25,7 +25,8 @@ defmodule Styler do Styler.Style.Defs, Styler.Style.Blocks, Styler.Style.Deprecations, - Styler.Style.Configs + Styler.Style.Configs, + Styler.Style.CommentDirectives ] @doc false diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs new file mode 100644 index 00000000..16c8621a --- /dev/null +++ b/test/style/comment_directives_test.exs @@ -0,0 +1,157 @@ +# Copyright 2024 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. + +defmodule Styler.Style.CommentDirectivesTest do + @moduledoc false + use Styler.StyleCase, async: true + + describe "sort" do + test "we dont just sort by accident" do + assert_style "[:c, :b, :a]" + end + + test "sorts lists of atoms" do + assert_style( + """ + # styler:sort + [ + :c, + :b, + :a + ] + """, + """ + # styler:sort + [ + :a, + :b, + :c + ] + """ + ) + end + + test "sorts sigils" do + assert_style("# styler:sort\n~w|c a b|", "# styler:sort\n~w|a b c|") + + assert_style( + """ + # styler:sort + ~w( + a + long + list + of + static + values + ) + """, + """ + # styler:sort + ~w( + a + list + long + of + static + values + ) + """ + ) + end + + test "assignments" do + assert_style( + """ + # styler:sort + my_var = + ~w( + a + long + list + of + static + values + ) + """, + """ + # styler:sort + my_var = + ~w( + a + list + long + of + static + values + ) + """ + ) + + assert_style( + """ + defmodule M do + @moduledoc false + # styler:sort + @attr ~w( + a + long + list + of + static + values + ) + end + """, + """ + defmodule M do + @moduledoc false + # styler:sort + @attr ~w( + a + list + long + of + static + values + ) + end + """ + ) + end + + test "doesnt affect downstream nodes" do + assert_style( + """ + # styler:sort + [:c, :a, :b] + + @country_codes ~w( + po_PO + en_US + fr_CA + ja_JP + ) + """, + """ + # styler:sort + [:a, :b, :c] + + @country_codes ~w( + po_PO + en_US + fr_CA + ja_JP + ) + """ + ) + end + end +end From 411cc93f6e5a1002a8f162fbbaa53dc379241205 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Sat, 23 Nov 2024 10:25:02 -0700 Subject: [PATCH 31/86] styler:sort - handle tuples --- lib/style.ex | 13 ++++++++++++- lib/style/comment_directives.ex | 7 +++---- lib/style/module_directives.ex | 5 +---- test/style/comment_directives_test.exs | 24 ++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/lib/style.ex b/lib/style.ex index da93416b..4b3a9a9b 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -59,9 +59,20 @@ defmodule Styler.Style do @doc "Traverses an ast node, updating all nodes' meta with `meta_fun`" def update_all_meta(node, meta_fun), do: Macro.prewalk(node, &Macro.update_meta(&1, meta_fun)) - # useful for comparing AST without meta (line numbers, etc) interfering + @doc "prewalks ast and sets all meta to `nil`. useful for comparing AST without meta (line numbers, etc) interfering" def without_meta(ast), do: update_all_meta(ast, fn _ -> nil end) + @doc "sorts a list of nodes according to their string representations" + def sort(ast, opts \\ []) when is_list(ast) do + format = if opts[:format] == :downcase, do: &String.downcase/1, else: &(&1) + + ast + |> Enum.map(&{&1, &1 |> Macro.to_string() |> format.()}) + |> Enum.uniq_by(&elem(&1, 1)) + |> List.keysort(1) + |> Enum.map(&elem(&1, 0)) + end + @doc """ Returns the current node (wrapped in a `__block__` if necessary) if it's a valid place to insert additional nodes """ diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index d41e0881..fb783910 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -13,6 +13,7 @@ defmodule Styler.Style.CommentDirectives do @behaviour Styler.Style + alias Styler.Style alias Styler.Zipper def run(zipper, ctx) do @@ -28,6 +29,7 @@ defmodule Styler.Style.CommentDirectives do end) if found do + #@TODO fix line numbers, move comments Zipper.update(found, &sort/1) else zipper @@ -37,10 +39,7 @@ defmodule Styler.Style.CommentDirectives do {:halt, zipper, ctx} end - defp sort({:__block__, meta, [list]}) when is_list(list) do - list = Enum.sort_by(list, fn {f, _, a} -> {f, a} end) - {:__block__, meta, [list]} - end + defp sort({:__block__, meta, [list]}) when is_list(list), do: {:__block__, meta, [Style.sort(list)]} defp sort({:sigil_w, sm, [{:<<>>, bm, [string]}, modifiers]}) do # ew. gotta be a better way. diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index 623ce5f0..f9eba852 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -413,10 +413,7 @@ defmodule Styler.Style.ModuleDirectives do defp sort(directives) do # sorting is done with `downcase` to match Credo directives - |> Enum.map(&{&1, &1 |> Macro.to_string() |> String.downcase()}) - |> Enum.uniq_by(&elem(&1, 1)) - |> List.keysort(1) - |> Enum.map(&elem(&1, 0)) + |> Style.sort(format: :downcase) |> Style.reset_newlines() end end diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs index 16c8621a..7c889f3f 100644 --- a/test/style/comment_directives_test.exs +++ b/test/style/comment_directives_test.exs @@ -153,5 +153,29 @@ defmodule Styler.Style.CommentDirectivesTest do """ ) end + + test "list of tuples" do + # 2ples are represented as block literals while >2ples are created via `:{}` + # decided the easiest way to handle this is to just use string representation for meow + assert_style """ + # styler:sort + [ + {:styler, github: "adobe/elixir-styler"}, + {:ash, "~> 3.0"}, + {:fluxon, "~> 1.0.0", repo: :fluxon}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:tailwind, "~> 0.2", runtime: Mix.env() == :dev} + ] + """,""" + # styler:sort + [ + {:ash, "~> 3.0"}, + {:fluxon, "~> 1.0.0", repo: :fluxon}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:styler, github: "adobe/elixir-styler"}, + {:tailwind, "~> 0.2", runtime: Mix.env() == :dev} + ] + """ + end end end From b13d045cb32f499afc58fabbd60b0e240e4cd599 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Sat, 23 Nov 2024 10:35:16 -0700 Subject: [PATCH 32/86] mix format :X --- lib/style.ex | 2 +- lib/style/comment_directives.ex | 2 +- test/style/comment_directives_test.exs | 41 ++++++++++++++------------ 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/style.ex b/lib/style.ex index 4b3a9a9b..9854a6d0 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -64,7 +64,7 @@ defmodule Styler.Style do @doc "sorts a list of nodes according to their string representations" def sort(ast, opts \\ []) when is_list(ast) do - format = if opts[:format] == :downcase, do: &String.downcase/1, else: &(&1) + format = if opts[:format] == :downcase, do: &String.downcase/1, else: & &1 ast |> Enum.map(&{&1, &1 |> Macro.to_string() |> format.()}) diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index fb783910..12b8993b 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -29,7 +29,7 @@ defmodule Styler.Style.CommentDirectives do end) if found do - #@TODO fix line numbers, move comments + # @TODO fix line numbers, move comments Zipper.update(found, &sort/1) else zipper diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs index 7c889f3f..fe299199 100644 --- a/test/style/comment_directives_test.exs +++ b/test/style/comment_directives_test.exs @@ -157,25 +157,28 @@ defmodule Styler.Style.CommentDirectivesTest do test "list of tuples" do # 2ples are represented as block literals while >2ples are created via `:{}` # decided the easiest way to handle this is to just use string representation for meow - assert_style """ - # styler:sort - [ - {:styler, github: "adobe/elixir-styler"}, - {:ash, "~> 3.0"}, - {:fluxon, "~> 1.0.0", repo: :fluxon}, - {:phoenix_live_reload, "~> 1.2", only: :dev}, - {:tailwind, "~> 0.2", runtime: Mix.env() == :dev} - ] - """,""" - # styler:sort - [ - {:ash, "~> 3.0"}, - {:fluxon, "~> 1.0.0", repo: :fluxon}, - {:phoenix_live_reload, "~> 1.2", only: :dev}, - {:styler, github: "adobe/elixir-styler"}, - {:tailwind, "~> 0.2", runtime: Mix.env() == :dev} - ] - """ + assert_style( + """ + # styler:sort + [ + {:styler, github: "adobe/elixir-styler"}, + {:ash, "~> 3.0"}, + {:fluxon, "~> 1.0.0", repo: :fluxon}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:tailwind, "~> 0.2", runtime: Mix.env() == :dev} + ] + """, + """ + # styler:sort + [ + {:ash, "~> 3.0"}, + {:fluxon, "~> 1.0.0", repo: :fluxon}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:styler, github: "adobe/elixir-styler"}, + {:tailwind, "~> 0.2", runtime: Mix.env() == :dev} + ] + """ + ) end end end From 0e8485b108b8ca9596a0ca51f5f8cf528a1251f7 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Sat, 23 Nov 2024 13:48:15 -0700 Subject: [PATCH 33/86] style:sort - dont dedupe --- lib/style.ex | 11 ----------- lib/style/comment_directives.ex | 4 ++-- lib/style/module_directives.ex | 5 ++++- test/style/comment_directives_test.exs | 2 ++ 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/style.ex b/lib/style.ex index 9854a6d0..d58ad1cb 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -62,17 +62,6 @@ defmodule Styler.Style do @doc "prewalks ast and sets all meta to `nil`. useful for comparing AST without meta (line numbers, etc) interfering" def without_meta(ast), do: update_all_meta(ast, fn _ -> nil end) - @doc "sorts a list of nodes according to their string representations" - def sort(ast, opts \\ []) when is_list(ast) do - format = if opts[:format] == :downcase, do: &String.downcase/1, else: & &1 - - ast - |> Enum.map(&{&1, &1 |> Macro.to_string() |> format.()}) - |> Enum.uniq_by(&elem(&1, 1)) - |> List.keysort(1) - |> Enum.map(&elem(&1, 0)) - end - @doc """ Returns the current node (wrapped in a `__block__` if necessary) if it's a valid place to insert additional nodes """ diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index 12b8993b..214a1a5b 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -13,7 +13,6 @@ defmodule Styler.Style.CommentDirectives do @behaviour Styler.Style - alias Styler.Style alias Styler.Zipper def run(zipper, ctx) do @@ -39,7 +38,8 @@ defmodule Styler.Style.CommentDirectives do {:halt, zipper, ctx} end - defp sort({:__block__, meta, [list]}) when is_list(list), do: {:__block__, meta, [Style.sort(list)]} + defp sort({:__block__, meta, [list]}) when is_list(list), do: {:__block__, meta, [sort(list)]} + defp sort(list) when is_list(list), do: Enum.sort_by(list, &Macro.to_string/1) defp sort({:sigil_w, sm, [{:<<>>, bm, [string]}, modifiers]}) do # ew. gotta be a better way. diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index f9eba852..623ce5f0 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -413,7 +413,10 @@ defmodule Styler.Style.ModuleDirectives do defp sort(directives) do # sorting is done with `downcase` to match Credo directives - |> Style.sort(format: :downcase) + |> Enum.map(&{&1, &1 |> Macro.to_string() |> String.downcase()}) + |> Enum.uniq_by(&elem(&1, 1)) + |> List.keysort(1) + |> Enum.map(&elem(&1, 0)) |> Style.reset_newlines() end end diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs index fe299199..487be6c1 100644 --- a/test/style/comment_directives_test.exs +++ b/test/style/comment_directives_test.exs @@ -24,6 +24,7 @@ defmodule Styler.Style.CommentDirectivesTest do [ :c, :b, + :c, :a ] """, @@ -32,6 +33,7 @@ defmodule Styler.Style.CommentDirectivesTest do [ :a, :b, + :c, :c ] """ From 5b56613d8738ca28565e8538b9385a2783be348c Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 25 Nov 2024 10:42:06 -0700 Subject: [PATCH 34/86] docs and fix a typo --- lib/style/comment_directives.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index 214a1a5b..7b1ccb70 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -9,7 +9,11 @@ # governing permissions and limitations under the License. defmodule Styler.Style.CommentDirectives do - @moduledoc "TODO" + @moduledoc """ + Leave a comment for Styler asking it to maintain code in a certain way. + + `# styler:sort` maintains sorting of wordlists (by string comparison) and lists (string comparison of code representation) + """ @behaviour Styler.Style @@ -48,7 +52,7 @@ defmodule Styler.Style.CommentDirectives do case Regex.run(~r|^\s+|, string) do # oneliner like `~w|c a b|` nil -> {"", " ", ""} - # multline like + # multiline like # `"\n a\n list\n long\n of\n static\n values\n"` # ^^^^ `prepend` ^^^^ `joiner` ^^ `append` # note that joiner and prepend are the same in a multiline (unsure if this is always true) From d35a28dfc4e4c64d53df7245b1d8272c6ebbeb32 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 27 Nov 2024 11:23:22 -0700 Subject: [PATCH 35/86] fiddle with private apis to ease iex playing --- lib/styler.ex | 13 ++++++------- lib/zipper.ex | 24 ++++++++++++------------ test/style/configs_test.exs | 2 +- test/style_test.exs | 2 +- test/support/style_case.ex | 10 +++++----- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/lib/styler.ex b/lib/styler.ex index 50e0d20b..dbb3992e 100644 --- a/lib/styler.ex +++ b/lib/styler.ex @@ -63,21 +63,21 @@ defmodule Styler do def features(_opts), do: [sigils: [], extensions: [".ex", ".exs"]] @impl Format - def format(input, formatter_opts) do + def format(input, formatter_opts \\ []) do file = formatter_opts[:file] styler_opts = formatter_opts[:styler] || [] {ast, comments} = input - |> string_to_quoted_with_comments(to_string(file)) + |> string_to_ast(to_string(file)) |> style(file, styler_opts) - quoted_to_string(ast, comments, formatter_opts) + ast_to_string(ast, comments, formatter_opts) end @doc false # Wrap `Code.string_to_quoted_with_comments` with our desired options - def string_to_quoted_with_comments(code, file \\ "nofile") when is_binary(code) do + def string_to_ast(code, file \\ "nofile") when is_binary(code) do Code.string_to_quoted_with_comments!(code, literal_encoder: &__MODULE__.literal_encoder/2, token_metadata: true, @@ -89,9 +89,8 @@ defmodule Styler do @doc false def literal_encoder(literal, meta), do: {:ok, {:__block__, meta, [literal]}} - @doc false - # Turns an ast and comments back into code, formatting it along the way. - def quoted_to_string(ast, comments, formatter_opts \\ []) do + @doc "Turns an ast and comments back into code, formatting it along the way." + def ast_to_string(ast, comments \\ [], formatter_opts \\ []) do opts = [{:comments, comments}, {:escape, false} | formatter_opts] {line_length, opts} = Keyword.pop(opts, :line_length, 122) diff --git a/lib/zipper.ex b/lib/zipper.ex index a5728e03..969b7577 100644 --- a/lib/zipper.ex +++ b/lib/zipper.ex @@ -317,8 +317,11 @@ defmodule Styler.Zipper do if next = next(zipper), do: do_traverse(next, acc, fun), else: {top(zipper), acc} end - # Same as `traverse/3`, but doesn't waste cycles going back to the top of the tree when traversal is finished - @doc false + @doc """ + Same as `traverse/3`, but doesn't waste cycles going back to the top of the tree when traversal is finished + + Useful when only the accumulator is of interest, and no updates to the zipper are. + """ @spec reduce(zipper, term, (zipper, term -> {zipper, term})) :: term def reduce({_, nil} = zipper, acc, fun) do do_reduce(zipper, acc, fun) @@ -390,17 +393,14 @@ defmodule Styler.Zipper do end end - @doc false - # Similar to traverse_while/3, but returns the `acc` directly, skipping the return to the top of the zipper. - # For that reason the :halt tuple is instead just a 2-ple of `{:halt, acc}` - @spec reduce_while(zipper, term, (zipper, term -> {command, zipper, term})) :: {zipper, term} - def reduce_while({_tree, nil} = zipper, acc, fun) do - do_reduce_while(zipper, acc, fun) - end + @doc """ + Same as `traverse_while/3` except it only returns the acc, saving the work of returning to the top of the zipper. - def reduce_while({tree, meta}, acc, fun) do - {{updated, _meta}, acc} = do_reduce_while({tree, nil}, acc, fun) - {{updated, meta}, acc} + For that reason the `:halt` tuple is instead just a 2-ple of `{:halt, acc}` + """ + @spec reduce_while(zipper, term, (zipper, term -> {:cont | :skip, zipper, term} | {:halt, term})) :: term + def reduce_while({tree, _meta}, acc, fun) do + do_reduce_while({tree, nil}, acc, fun) end defp do_reduce_while(zipper, acc, fun) do diff --git a/test/style/configs_test.exs b/test/style/configs_test.exs index f4c0cdb5..3f3db06f 100644 --- a/test/style/configs_test.exs +++ b/test/style/configs_test.exs @@ -15,7 +15,7 @@ defmodule Styler.Style.ConfigsTest do alias Styler.Style.Configs test "only runs on exs files in config folders" do - {ast, _} = Styler.string_to_quoted_with_comments("import Config\n\nconfig :bar, boop: :baz") + {ast, _} = Styler.string_to_ast("import Config\n\nconfig :bar, boop: :baz") zipper = Styler.Zipper.zip(ast) for file <- ~w(dev.exs my_app.exs config.exs) do diff --git a/test/style_test.exs b/test/style_test.exs index fbe7a009..0f7f6785 100644 --- a/test/style_test.exs +++ b/test/style_test.exs @@ -35,7 +35,7 @@ defmodule Styler.StyleTest do # After module """ - @comments @code |> Styler.string_to_quoted_with_comments() |> elem(1) + @comments @code |> Styler.string_to_ast() |> elem(1) describe "displace_comments/2" do test "Doesn't lose any comments" do diff --git a/test/support/style_case.ex b/test/support/style_case.ex index f87c7bd3..a7425772 100644 --- a/test/support/style_case.ex +++ b/test/support/style_case.ex @@ -40,11 +40,11 @@ defmodule Styler.StyleCase do if styled != expected and ExUnit.configuration()[:trace] do IO.puts("\n======Given=============\n") IO.puts(before) - {before_ast, before_comments} = Styler.string_to_quoted_with_comments(before) + {before_ast, before_comments} = Styler.string_to_ast(before) dbg(before_ast) dbg(before_comments) IO.puts("======Expected AST==========\n") - {expected_ast, expected_comments} = Styler.string_to_quoted_with_comments(expected) + {expected_ast, expected_comments} = Styler.string_to_ast(expected) dbg(expected_ast) dbg(expected_comments) IO.puts("======Got AST===============\n") @@ -107,7 +107,7 @@ defmodule Styler.StyleCase do dbg(styled_ast) IO.puts("expected:") - dbg(elem(Styler.string_to_quoted_with_comments(expected), 0)) + dbg(elem(Styler.string_to_ast(expected), 0)) IO.puts("code:\n#{styled}") flunk("") @@ -133,11 +133,11 @@ defmodule Styler.StyleCase do end def style(code, filename \\ "testfile") do - {ast, comments} = Styler.string_to_quoted_with_comments(code) + {ast, comments} = Styler.string_to_ast(code) {styled_ast, comments} = Styler.style({ast, comments}, filename, on_error: :raise) try do - styled_code = styled_ast |> Styler.quoted_to_string(comments) |> String.trim_trailing("\n") + styled_code = styled_ast |> Styler.ast_to_string(comments) |> String.trim_trailing("\n") {styled_ast, styled_code, comments} rescue exception -> From 98c5c1c7e39f743b2dc36dd7b1539bb9aba95673 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 27 Nov 2024 11:47:09 -0700 Subject: [PATCH 36/86] remove Zipper.reduce/3 - bugged, but more importantly unused --- lib/zipper.ex | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/lib/zipper.ex b/lib/zipper.ex index 969b7577..0cdb17e8 100644 --- a/lib/zipper.ex +++ b/lib/zipper.ex @@ -317,26 +317,6 @@ defmodule Styler.Zipper do if next = next(zipper), do: do_traverse(next, acc, fun), else: {top(zipper), acc} end - @doc """ - Same as `traverse/3`, but doesn't waste cycles going back to the top of the tree when traversal is finished - - Useful when only the accumulator is of interest, and no updates to the zipper are. - """ - @spec reduce(zipper, term, (zipper, term -> {zipper, term})) :: term - def reduce({_, nil} = zipper, acc, fun) do - do_reduce(zipper, acc, fun) - end - - def reduce({tree, meta}, acc, fun) do - {{updated, _meta}, acc} = do_reduce({tree, nil}, acc, fun) - {{updated, meta}, acc} - end - - defp do_reduce(zipper, acc, fun) do - {zipper, acc} = fun.(zipper, acc) - if next = next(zipper), do: do_reduce(next, acc, fun), else: acc - end - @doc """ Traverses the tree in depth-first pre-order calling the given function for each node. From cef60157c18096527b4058f6eb3684b61f9d1fd8 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 13 Jan 2025 12:02:06 -0700 Subject: [PATCH 37/86] Maintain comment-node relations when applying `#styler:sort` directive (#207) Co-authored-by: Greg Mefford Closes #167 --- lib/style.ex | 147 ++++++++++++++----------- lib/style/comment_directives.ex | 45 +++++--- lib/style/configs.ex | 76 +------------ lib/style/module_directives.ex | 64 ++++++++++- test/style/comment_directives_test.exs | 51 +++++++++ test/style_test.exs | 14 --- 6 files changed, 232 insertions(+), 165 deletions(-) diff --git a/lib/style.ex b/lib/style.ex index d58ad1cb..80c50b23 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -170,69 +170,6 @@ defmodule Styler.Style do {directive, updated_meta, children} end - @doc """ - "Fixes" the line numbers of nodes who have had their orders changed via sorting or other methods. - This "fix" simply ensures that comments don't get wrecked as part of us moving AST nodes willy-nilly. - - The fix is rather naive, and simply enforces the following property on the code: - A given node must have a line number less than the following node. - Et voila! Comments behave much better. - - ## In Detail - - For example, given document - - 1: defmodule ... - 2: alias B - 3: # this is foo - 4: def foo ... - 5: alias A - - Sorting aliases the ast node for would put `alias A` (line 5) before `alias B` (line 2). - - 1: defmodule ... - 5: alias A - 2: alias B - 3: # this is foo - 4: def foo ... - - Elixir's document algebra would then encounter `line: 5` and immediately dump all comments with `line <= 5`, - meaning after running through the formatter we'd end up with - - 1: defmodule - 2: # hi - 3: # this is foo - 4: alias A - 5: alias B - 6: - 7: def foo ... - - This function fixes that by seeing that `alias A` has a higher line number than its following sibling `alias B` and so - updates `alias A`'s line to be preceding `alias B`'s line. - - Running the results of this function through the formatter now no longer dumps the comments prematurely - - 1: defmodule ... - 2: alias A - 3: alias B - 4: # this is foo - 5: def foo ... - """ - def fix_line_numbers(nodes, nil), do: fix_line_numbers(nodes, 999_999) - def fix_line_numbers(nodes, {_, meta, _}), do: fix_line_numbers(nodes, meta[:line]) - def fix_line_numbers(nodes, max), do: nodes |> Enum.reverse() |> do_fix_lines(max, []) - - defp do_fix_lines([], _, acc), do: acc - - defp do_fix_lines([{_, meta, _} = node | nodes], max, acc) do - line = meta[:line] - - # the -2 is just an ugly hack to leave room for one-liner comments and not hijack them. - if line > max, - do: do_fix_lines(nodes, max, [shift_line(node, max - line - 2) | acc]), - else: do_fix_lines(nodes, line, [node | acc]) - end - def max_line([_ | _] = list), do: list |> List.last() |> max_line() def max_line(ast) do @@ -257,4 +194,88 @@ defmodule Styler.Style do max_line end end + + def order_line_meta_and_comments(nodes, comments, first_line) do + {nodes, comments, node_comments} = fix_lines(nodes, comments, first_line, [], []) + {nodes, Enum.sort_by(comments ++ node_comments, & &1.line)} + end + + defp fix_lines([], comments, _, node_acc, c_acc), do: {Enum.reverse(node_acc), comments, c_acc} + + defp fix_lines([{_, meta, _} = node | nodes], comments, start_line, n_acc, c_acc) do + line = meta[:line] + last_line = meta[:end_of_expression][:line] || max_line(node) + + {node, node_comments, comments} = + if start_line == line do + {node, [], comments} + else + {mine, comments} = comments_for_lines(comments, line, last_line) + line_with_comments = (List.first(mine)[:line] || line) - (List.first(mine)[:previous_eol_count] || 1) + 1 + + if line_with_comments == start_line do + {node, mine, comments} + else + shift = start_line - line_with_comments + # fix the node's line + node = shift_line(node, shift) + # fix the comment's line + mine = Enum.map(mine, &%{&1 | line: &1.line + shift}) + {node, mine, comments} + end + end + + {_, meta, _} = node + # @TODO what about comments that were free floating between blocks? i'm just ignoring them and maybe always will... + # kind of just want to shove them to the end though, so that they don't interrupt existing stanzas. + # i think that's accomplishable by doing a final call above that finds all comments in the comments list that weren't moved + # and which are in the range of start..finish and sets their lines to finish! + last_line = meta[:end_of_expression][:line] || max_line(node) + last_line = (meta[:end_of_expression][:newlines] || 1) + last_line + fix_lines(nodes, comments, last_line, [node | n_acc], node_comments ++ c_acc) + end + + @doc """ + Returns all comments "for" a node, including on the line before it. + see `comments_for_lines` for more + """ + def comments_for_node({_, m, _} = node, comments) do + last_line = m[:end_of_expression][:line] || max_line(node) + comments_for_lines(comments, m[:line], last_line) + end + + @doc """ + Gets all comments in range start_line..last_line, and any comments immediately before start_line.s + + 1. code + 2. # a + 3. # b + 4. code # c + 5. # d + 6. code + 7. # e + + here, comments_for_lines(comments, 4, 6) is "a", "b", "c", "d" + """ + def comments_for_lines(comments, start_line, last_line) do + comments |> Enum.reverse() |> comments_for_lines(start_line, last_line, [], []) + end + + defp comments_for_lines(reversed_comments, start, last, match, acc) + + defp comments_for_lines([], _, _, match, acc), do: {Enum.reverse(match), acc} + + defp comments_for_lines([%{line: line} = comment | rev_comments], start, last, match, acc) do + cond do + # after our block - no match + line > last -> comments_for_lines(rev_comments, start, last, match, [comment | acc]) + # after start, before last -- it's a match! + line >= start -> comments_for_lines(rev_comments, start, last, [comment | match], acc) + # this is a comment immediately before start, which means it's modifying this block... + # we count that as a match, and look above it to see if it's a multiline comment + line == start - 1 -> comments_for_lines(rev_comments, start - 1, last, [comment | match], acc) + # comment before start - we've thus iterated through all comments which could be in our range + true -> {match, Enum.reverse(rev_comments, [comment | acc])} + end + end end diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index 7b1ccb70..34e59b84 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -17,14 +17,15 @@ defmodule Styler.Style.CommentDirectives do @behaviour Styler.Style + alias Styler.Style alias Styler.Zipper def run(zipper, ctx) do - zipper = + {zipper, comments} = ctx.comments |> Enum.filter(&(&1.text == "# styler:sort")) |> Enum.map(& &1.line) - |> Enum.reduce(zipper, fn line, zipper -> + |> Enum.reduce({zipper, ctx.comments}, fn line, {zipper, comments} -> found = Zipper.find(zipper, fn {_, meta, _} -> Keyword.get(meta, :line, -1) >= line @@ -32,20 +33,30 @@ defmodule Styler.Style.CommentDirectives do end) if found do - # @TODO fix line numbers, move comments - Zipper.update(found, &sort/1) + {node, _} = found + {sorted, comments} = sort(node, ctx.comments) + {Zipper.replace(found, sorted), comments} else - zipper + {zipper, comments} end end) - {:halt, zipper, ctx} + {:halt, zipper, %{ctx | comments: comments}} end - defp sort({:__block__, meta, [list]}) when is_list(list), do: {:__block__, meta, [sort(list)]} - defp sort(list) when is_list(list), do: Enum.sort_by(list, &Macro.to_string/1) + defp sort({:__block__, meta, [list]} = node, comments) when is_list(list) do + list = Enum.sort_by(list, &Macro.to_string/1) + line = meta[:line] + # no need to fix line numbers if it's a single line structure + {list, comments} = + if line == Style.max_line(node), + do: {list, comments}, + else: Style.order_line_meta_and_comments(list, comments, line) - defp sort({:sigil_w, sm, [{:<<>>, bm, [string]}, modifiers]}) do + {{:__block__, meta, [list]}, comments} + end + + defp sort({:sigil_w, sm, [{:<<>>, bm, [string]}, modifiers]}, comments) do # ew. gotta be a better way. # this keeps indentation for the sigil via joiner, while prepend and append are the bookending whitespace {prepend, joiner, append} = @@ -61,10 +72,18 @@ defmodule Styler.Style.CommentDirectives do end string = string |> String.split() |> Enum.sort() |> Enum.join(joiner) - {:sigil_w, sm, [{:<<>>, bm, [prepend, string, append]}, modifiers]} + {{:sigil_w, sm, [{:<<>>, bm, [prepend, string, append]}, modifiers]}, comments} + end + + defp sort({:=, m, [lhs, rhs]}, comments) do + {rhs, comments} = sort(rhs, comments) + {{:=, m, [lhs, rhs]}, comments} + end + + defp sort({:@, m, [{a, am, [assignment]}]}, comments) do + {assignment, comments} = sort(assignment, comments) + {{:@, m, [{a, am, [assignment]}]}, comments} end - defp sort({:=, m, [lhs, rhs]}), do: {:=, m, [lhs, sort(rhs)]} - defp sort({:@, m, [{a, am, [assignment]}]}), do: {:@, m, [{a, am, [sort(assignment)]}]} - defp sort(x), do: x + defp sort(x, comments), do: {x, comments} end diff --git a/lib/style/configs.ex b/lib/style/configs.ex index ab110a0b..75a0d750 100644 --- a/lib/style/configs.ex +++ b/lib/style/configs.ex @@ -80,19 +80,14 @@ defmodule Styler.Style.Configs do |> Style.reset_newlines() |> Enum.concat(configs) - # `set_lines` performs better than `fix_line_numbers` for a large number of nodes moving, as it moves their comments with them - # however, it will also move any comments not associated with a node, causing wildly unpredictable sad times! - # so i'm trying to guess which change will be less damaging. - # moving >=3 nodes hints that this is an initial run, where `set_lines` definitely outperforms. {nodes, comments} = if changed?(nodes) do # after running, this block should take up the same # of lines that it did before # the first node of `rest` is greater than the highest line in configs, assignments # config line is the first line to be used as part of this block - # that will change when we consider preceding comments - {node_comments, _} = comments_for_node(config, comments) + {node_comments, _} = Style.comments_for_node(config, comments) first_line = min(List.last(node_comments)[:line] || cfm[:line], cfm[:line]) - set_lines(nodes, comments, first_line) + Style.order_line_meta_and_comments(nodes, comments, first_line) else {nodes, comments} end @@ -121,73 +116,6 @@ defmodule Styler.Style.Configs do defp changed?(_), do: false - defp set_lines(nodes, comments, first_line) do - {nodes, comments, node_comments} = set_lines(nodes, comments, first_line, [], []) - # @TODO if there are dangling comments between the nodes min/max, push them somewhere? - # likewise deal with conflicting line comments? - {nodes, Enum.sort_by(comments ++ node_comments, & &1.line)} - end - - def set_lines([], comments, _, node_acc, c_acc), do: {Enum.reverse(node_acc), comments, c_acc} - - def set_lines([{_, meta, _} = node | nodes], comments, start_line, n_acc, c_acc) do - line = meta[:line] - last_line = meta[:end_of_expression][:line] || Style.max_line(node) - - {node, node_comments, comments} = - if start_line == line do - {node, [], comments} - else - {mine, comments} = comments_for_lines(comments, line, last_line) - line_with_comments = (List.first(mine)[:line] || line) - (List.first(mine)[:previous_eol_count] || 1) + 1 - - if line_with_comments == start_line do - {node, mine, comments} - else - shift = start_line - line_with_comments - node = Style.shift_line(node, shift) - - mine = Enum.map(mine, &%{&1 | line: &1.line + shift}) - {node, mine, comments} - end - end - - {_, meta, _} = node - # @TODO what about comments that were free floating between blocks? i'm just ignoring them and maybe always will... - # kind of just want to shove them to the end though, so that they don't interrupt existing stanzas. - # i think that's accomplishable by doing a final call above that finds all comments in the comments list that weren't moved - # and which are in the range of start..finish and sets their lines to finish! - last_line = meta[:end_of_expression][:line] || Style.max_line(node) - last_line = (meta[:end_of_expression][:newlines] || 1) + last_line - set_lines(nodes, comments, last_line, [node | n_acc], node_comments ++ c_acc) - end - - defp comments_for_node({_, m, _} = node, comments) do - last_line = m[:end_of_expression][:line] || Style.max_line(node) - comments_for_lines(comments, m[:line], last_line) - end - - defp comments_for_lines(comments, start_line, last_line) do - comments - |> Enum.reverse() - |> comments_for_lines(start_line, last_line, [], []) - end - - defp comments_for_lines(reversed_comments, start, last, match, acc) - - defp comments_for_lines([], _, _, match, acc), do: {Enum.reverse(match), acc} - - defp comments_for_lines([%{line: line} = comment | rev_comments], start, last, match, acc) do - cond do - line > last -> comments_for_lines(rev_comments, start, last, match, [comment | acc]) - line >= start -> comments_for_lines(rev_comments, start, last, [comment | match], acc) - # @TODO bug: match line looks like `x = :foo # comment for x` - # could account for that by pre-running the formatter on config files :/ - line == start - 1 -> comments_for_lines(rev_comments, start - 1, last, [comment | match], acc) - true -> {match, Enum.reverse(rev_comments, [comment | acc])} - end - end - defp accumulate([{:config, _, [_, _ | _]} = c | siblings], cs, as), do: accumulate(siblings, [c | cs], as) defp accumulate([{:=, _, [_lhs, _rhs]} = a | siblings], cs, as), do: accumulate(siblings, cs, [a | as]) defp accumulate(rest, configs, assignments), do: {configs, assignments, rest} diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index 623ce5f0..eb10761e 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -263,7 +263,7 @@ defmodule Styler.Style.ModuleDirectives do acc.require ] |> Stream.concat() - |> Style.fix_line_numbers(List.first(nondirectives)) + |> fix_line_numbers(List.first(nondirectives)) # the # of aliases can be decreased during sorting - if there were any, we need to be sure to write the deletion if Enum.empty?(directives) do @@ -419,4 +419,66 @@ defmodule Styler.Style.ModuleDirectives do |> Enum.map(&elem(&1, 0)) |> Style.reset_newlines() end + + # TODO investigate removing this in favor of the Style.post_sort_cleanup(node, comments) + # "Fixes" the line numbers of nodes who have had their orders changed via sorting or other methods. + # This "fix" simply ensures that comments don't get wrecked as part of us moving AST nodes willy-nilly. + # + # The fix is rather naive, and simply enforces the following property on the code: + # A given node must have a line number less than the following node. + # Et voila! Comments behave much better. + # + # ## In Detail + # + # For example, given document + # + # 1: defmodule ... + # 2: alias B + # 3: # this is foo + # 4: def foo ... + # 5: alias A + # + # Sorting aliases the ast node for would put `alias A` (line 5) before `alias B` (line 2). + # + # 1: defmodule ... + # 5: alias A + # 2: alias B + # 3: # this is foo + # 4: def foo ... + # + # Elixir's document algebra would then encounter `line: 5` and immediately dump all comments with `line <= 5`, + # meaning after running through the formatter we'd end up with + # + # 1: defmodule + # 2: # hi + # 3: # this is foo + # 4: alias A + # 5: alias B + # 6: + # 7: def foo ... + # + # This function fixes that by seeing that `alias A` has a higher line number than its following sibling `alias B` and so + # updates `alias A`'s line to be preceding `alias B`'s line. + # + # Running the results of this function through the formatter now no longer dumps the comments prematurely + # + # 1: defmodule ... + # 2: alias A + # 3: alias B + # 4: # this is foo + # 5: def foo ... + defp fix_line_numbers(nodes, nil), do: fix_line_numbers(nodes, 999_999) + defp fix_line_numbers(nodes, {_, meta, _}), do: fix_line_numbers(nodes, meta[:line]) + defp fix_line_numbers(nodes, max), do: nodes |> Enum.reverse() |> do_fix_lines(max, []) + + defp do_fix_lines([], _, acc), do: acc + + defp do_fix_lines([{_, meta, _} = node | nodes], max, acc) do + line = meta[:line] + + # the -2 is just an ugly hack to leave room for one-liner comments and not hijack them. + if line > max, + do: do_fix_lines(nodes, max, [Style.shift_line(node, max - line - 2) | acc]), + else: do_fix_lines(nodes, line, [node | acc]) + end end diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs index 487be6c1..def6af55 100644 --- a/test/style/comment_directives_test.exs +++ b/test/style/comment_directives_test.exs @@ -182,5 +182,56 @@ defmodule Styler.Style.CommentDirectivesTest do """ ) end + + test "treats comments nicely" do + assert_style( + """ + # pre-amble comment + # styler:sort + [ + {:phoenix, "~> 1.7"}, + # hackney comment + {:hackney, "1.18.1", override: true}, + {:styler, "~> 1.2", only: [:dev, :test], runtime: false}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:excellent_migrations, "~> 0.1", only: [:dev, :test], runtime: false}, + # ecto + {:ecto, "~> 3.12"}, + {:ecto_sql, "~> 3.12"}, + # genstage comment 1 + # genstage comment 2 + {:gen_stage, "~> 1.0", override: true}, + # telemetry + {:telemetry, "~> 1.0", override: true}, + # dangling comment + ] + + # some other comment + """, + """ + # pre-amble comment + # styler:sort + [ + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + # ecto + {:ecto, "~> 3.12"}, + {:ecto_sql, "~> 3.12"}, + {:excellent_migrations, "~> 0.1", only: [:dev, :test], runtime: false}, + # genstage comment 1 + # genstage comment 2 + {:gen_stage, "~> 1.0", override: true}, + # hackney comment + {:hackney, "1.18.1", override: true}, + {:phoenix, "~> 1.7"}, + {:styler, "~> 1.2", only: [:dev, :test], runtime: false}, + # telemetry + {:telemetry, "~> 1.0", override: true} + # dangling comment + ] + + # some other comment + """ + ) + end end end diff --git a/test/style_test.exs b/test/style_test.exs index 0f7f6785..43bc8b27 100644 --- a/test/style_test.exs +++ b/test/style_test.exs @@ -3,8 +3,6 @@ defmodule Styler.StyleTest do import Styler.Style, only: [displace_comments: 2, shift_comments: 3] - alias Styler.Style - @code """ # Above module defmodule Foo do @@ -117,16 +115,4 @@ defmodule Styler.StyleTest do end end end - - describe "fix_line_numbers" do - test "returns ast list with increasing line numbers" do - nodes = for n <- [1, 2, 999, 1000, 5, 6], do: {:node, [line: n], [n]} - fixed = Style.fix_line_numbers(nodes, 7) - - Enum.scan(fixed, fn {_, [line: this_line], _} = this_node, {_, [line: previous_line], _} -> - assert this_line >= previous_line - this_node - end) - end - end end From 13bc947745afa3e82c55ec37845f03def6aff896 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 13 Jan 2025 12:09:05 -0700 Subject: [PATCH 38/86] v1.3.0 --- CHANGELOG.md | 2 ++ README.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++-- mix.exs | 2 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e40e0c66..bdacbfcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +## 1.3.0 + ### Improvements #### `# styler:sort` Styler's first comment directive diff --git a/README.md b/README.md index 840b221e..6042da5c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,11 @@ You can learn more about the history, purpose and implementation of Styler from ## Features -- auto-fixes [many credo rules](docs/credo.md), meaning you can turn them off to speed credo up +### AST Rewrites as part of `mix format` + +[See our Rewrites documentation on hexdocs](https://hexdocs.pm/styler/styles.html) +Styler fixes a plethora of elixir style and optimization issues automatically as part of `mix format`. In addition to automating corrections for [many credo rules](docs/credo.md) (meaning you can turn them off to speed credo up), Styler: + - [keeps a strict module layout](docs/module_directives.md#directive-organization) - alphabetizes module directives - [extracts repeated aliases](docs/module_directives.md#alias-lifting) @@ -21,7 +25,63 @@ You can learn more about the history, purpose and implementation of Styler from - replaces strings with sigils when the string has many escaped quotes - ... and so much more -[See our Rewrites documentation on hexdocs](https://hexdocs.pm/styler/styles.html) +### Maintain static list order via `# styler:sort` + +Styler can keep static values sorted for your team as part of its formatting pass. To instruct it to do so, replace any `# Please keep this list sorted!` notes you wrote to your teammates with `# styler:sort`. + +#### Examples + +```elixir +# styler:sort +[:c, :a, :b] + +# styler:sort +~w(a list of words) + +# styler:sort +@country_codes ~w( + en_US + po_PO + fr_CA + ja_JP +) + +# styler:sort +a_var = + [ + Modules, + In, + A, + List + ] +``` + +Would yield: + +```elixir +# styler:sort +[:a, :b, :c] + +# styler:sort +~w(a list of words) + +# styler:sort +@country_codes ~w( + en_US + fr_CA + ja_JP + po_PO +) + +# styler:sort +a_var = + [ + A, + In, + List, + Modules + ] +``` ## Who is Styler for? diff --git a/mix.exs b/mix.exs index e7297a53..692511e5 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.2.1" + @version "1.3.0" @url "https://github.com/adobe/elixir-styler" def project do From 03b1ee6a706e858534d870ba961912ca456ed380 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 13 Jan 2025 12:14:00 -0700 Subject: [PATCH 39/86] correct changelog --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdacbfcf..9938679e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ they can and will change without that change being reflected in Styler's semanti #### `# styler:sort` Styler's first comment directive -Styler will now keep a user-designated list or wordlist (`~w` sigil) sorted as part of formatting via the use of comments. +Styler will now keep a user-designated list or wordlist (`~w` sigil) sorted as part of formatting via the use of comments. Elements of the list are sorted by their string representation. The intention is to remove comments to humans, like `# Please keep this list sorted!`, in favor of comments to robots: `# styler:sort`. Personally speaking, Styler is much better at alphabetical-order than I ever will be. @@ -70,8 +70,6 @@ a_var = ] ``` -Sorting is done according to erlang term ordering, so lists with elements of multiple types will work just fine. - ## 1.2.1 ### Fixes From d6a2a5e2f91c5aceede2fb4b78c07c0fda223e4f Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 13 Jan 2025 12:43:39 -0700 Subject: [PATCH 40/86] fix twople bug in sort directive, add map sorting --- CHANGELOG.md | 12 ++++ lib/style.ex | 10 +++- lib/style/comment_directives.ex | 14 ++++- test/style/comment_directives_test.exs | 76 ++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9938679e..5b94ed85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +## 1.3.1 + +### Improvements + +- `# styler:sort` now works with maps and the `defstruct` macro + +### Fixes + +- `# styler:sort` no longer blows up on keyword lists :X + +### Fixes + ## 1.3.0 ### Improvements diff --git a/lib/style.ex b/lib/style.ex index 80c50b23..52a2fd35 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -202,7 +202,8 @@ defmodule Styler.Style do defp fix_lines([], comments, _, node_acc, c_acc), do: {Enum.reverse(node_acc), comments, c_acc} - defp fix_lines([{_, meta, _} = node | nodes], comments, start_line, n_acc, c_acc) do + defp fix_lines([node | nodes], comments, start_line, n_acc, c_acc) do + meta = meta(node) line = meta[:line] last_line = meta[:end_of_expression][:line] || max_line(node) @@ -225,7 +226,7 @@ defmodule Styler.Style do end end - {_, meta, _} = node + meta = meta(node) # @TODO what about comments that were free floating between blocks? i'm just ignoring them and maybe always will... # kind of just want to shove them to the end though, so that they don't interrupt existing stanzas. # i think that's accomplishable by doing a final call above that finds all comments in the comments list that weren't moved @@ -235,6 +236,11 @@ defmodule Styler.Style do fix_lines(nodes, comments, last_line, [node | n_acc], node_comments ++ c_acc) end + # typical node + def meta({_, meta, _}), do: meta + # kwl tuple ala a: :b + def meta({{_, meta, _}, _}), do: meta + @doc """ Returns all comments "for" a node, including on the line before it. see `comments_for_lines` for more diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index 34e59b84..7eade124 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -44,7 +44,7 @@ defmodule Styler.Style.CommentDirectives do {:halt, zipper, %{ctx | comments: comments}} end - defp sort({:__block__, meta, [list]} = node, comments) when is_list(list) do + defp sort({parent, meta, [list]} = node, comments) when parent in ~w(defstruct __block__)a and is_list(list) do list = Enum.sort_by(list, &Macro.to_string/1) line = meta[:line] # no need to fix line numbers if it's a single line structure @@ -53,7 +53,17 @@ defmodule Styler.Style.CommentDirectives do do: {list, comments}, else: Style.order_line_meta_and_comments(list, comments, line) - {{:__block__, meta, [list]}, comments} + {{parent, meta, [list]}, comments} + end + + defp sort({:%{}, meta, list}, comments) when is_list(list) do + {{:__block__, meta, [list]}, comments} = sort({:__block__, meta, [list]}, comments) + {{:%{}, meta, list}, comments} + end + + defp sort({:%, m, [struct, map]}, comments) do + {map, comments} = sort(map, comments) + {{:%, m, [struct, map]}, comments} end defp sort({:sigil_w, sm, [{:<<>>, bm, [string]}, modifiers]}, comments) do diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs index def6af55..13dc0bfa 100644 --- a/test/style/comment_directives_test.exs +++ b/test/style/comment_directives_test.exs @@ -40,6 +40,82 @@ defmodule Styler.Style.CommentDirectivesTest do ) end + test "sort keywordy things" do + assert_style( + """ + # styler:sort + [ + c: 2, + b: 3, + a: 4, + d: 1 + ] + """, + """ + # styler:sort + [ + a: 4, + b: 3, + c: 2, + d: 1 + ] + """ + ) + + assert_style( + """ + # styler:sort + %{ + c: 2, + b: 3, + a: 4, + d: 1 + } + """, + """ + # styler:sort + %{ + a: 4, + b: 3, + c: 2, + d: 1 + } + """ + ) + + assert_style( + """ + # styler:sort + %Struct{ + c: 2, + b: 3, + a: 4, + d: 1 + } + """, + """ + # styler:sort + %Struct{ + a: 4, + b: 3, + c: 2, + d: 1 + } + """ + ) + + assert_style( + """ + # styler:sort + defstruct c: 2, b: 3, a: 4, d: 1 + """, + """ + # styler:sort + defstruct a: 4, b: 3, c: 2, d: 1 + """ + ) + end + test "sorts sigils" do assert_style("# styler:sort\n~w|c a b|", "# styler:sort\n~w|a b c|") From 30446bf75db5a7df6b3d40a78694a2e7ce7036f7 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 13 Jan 2025 12:43:56 -0700 Subject: [PATCH 41/86] v1.3.1 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 692511e5..2c3040b3 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.3.0" + @version "1.3.1" @url "https://github.com/adobe/elixir-styler" def project do From 5d3d620406ca64f29c8fb7161fe36957408e12a0 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 13 Jan 2025 12:50:11 -0700 Subject: [PATCH 42/86] defstruct with list literal --- lib/style/comment_directives.ex | 7 +++++++ test/style/comment_directives_test.exs | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index 7eade124..1677570d 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -44,6 +44,7 @@ defmodule Styler.Style.CommentDirectives do {:halt, zipper, %{ctx | comments: comments}} end + # defstruct with a syntax-sugared keyword list hits here defp sort({parent, meta, [list]} = node, comments) when parent in ~w(defstruct __block__)a and is_list(list) do list = Enum.sort_by(list, &Macro.to_string/1) line = meta[:line] @@ -56,6 +57,12 @@ defmodule Styler.Style.CommentDirectives do {{parent, meta, [list]}, comments} end + # defstruct with a literal list + defp sort({:defstruct, meta, [{:__block__, _, [_]} = list]}, comments) do + {list, comments} = sort(list, comments) + {{:defstruct, meta, [list]}, comments} + end + defp sort({:%{}, meta, list}, comments) when is_list(list) do {{:__block__, meta, [list]}, comments} = sort({:__block__, meta, [list]}, comments) {{:%{}, meta, list}, comments} diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs index 13dc0bfa..5fe60c87 100644 --- a/test/style/comment_directives_test.exs +++ b/test/style/comment_directives_test.exs @@ -114,6 +114,31 @@ defmodule Styler.Style.CommentDirectivesTest do defstruct a: 4, b: 3, c: 2, d: 1 """ ) + + assert_style( + """ + # styler:sort + defstruct [ + :repo, + :query, + :order, + :chunk_size, + :timeout, + :cursor + ] + """, + """ + # styler:sort + defstruct [ + :chunk_size, + :cursor, + :order, + :query, + :repo, + :timeout + ] + """ + ) end test "sorts sigils" do From bef00c9a88940014aa6eb2afb725a3579f96008f Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 13 Jan 2025 21:54:02 +0100 Subject: [PATCH 43/86] ci: update elixir and erlang versions --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c233861a..a51e0fc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ jobs: name: Ex${{matrix.elixir}}/OTP${{matrix.otp}} strategy: matrix: - elixir: ['1.15.7', '1.16.0', '1.17.0-rc.0'] - otp: ['25.1.2'] + elixir: ['1.15.8', '1.16.3', '1.17.3'] + otp: ['25.3.2'] steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 From d132d7978c08d8af9d3b8a33487e870e06a5657d Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 13 Jan 2025 14:01:03 -0700 Subject: [PATCH 44/86] sort directive: sort values of keys. Closes #208 --- CHANGELOG.md | 4 +++ lib/style.ex | 1 + lib/style/comment_directives.ex | 11 ++++-- test/style/comment_directives_test.exs | 49 ++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b94ed85..0056aa7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +### Improvements + +- `# styler:sort` can be used to sort values of key-value pairs. eg, sort the value of a single key in a map (Closes #208, h/t @ypconstante) + ## 1.3.1 ### Improvements diff --git a/lib/style.ex b/lib/style.ex index 52a2fd35..f1a2dcf8 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -240,6 +240,7 @@ defmodule Styler.Style do def meta({_, meta, _}), do: meta # kwl tuple ala a: :b def meta({{_, meta, _}, _}), do: meta + def meta(_), do: nil @doc """ Returns all comments "for" a node, including on the line before it. diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index 1677570d..5f70aa30 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -27,9 +27,9 @@ defmodule Styler.Style.CommentDirectives do |> Enum.map(& &1.line) |> Enum.reduce({zipper, ctx.comments}, fn line, {zipper, comments} -> found = - Zipper.find(zipper, fn - {_, meta, _} -> Keyword.get(meta, :line, -1) >= line - _ -> false + Zipper.find(zipper, fn node -> + node_line = Style.meta(node)[:line] || -1 + node_line >= line end) if found do @@ -102,5 +102,10 @@ defmodule Styler.Style.CommentDirectives do {{:@, m, [{a, am, [assignment]}]}, comments} end + defp sort({key, value}, comments) do + {value, comments} = sort(value, comments) + {{key, value}, comments} + end + defp sort(x, comments), do: {x, comments} end diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs index 5fe60c87..f7fa7b12 100644 --- a/test/style/comment_directives_test.exs +++ b/test/style/comment_directives_test.exs @@ -141,6 +141,55 @@ defmodule Styler.Style.CommentDirectivesTest do ) end + test "inside keywords" do + assert_style( + """ + %{ + key: + # styler:sort + [ + 3, + 2, + 1 + ] + } + """, + """ + %{ + # styler:sort + key: [ + 1, + 2, + 3 + ] + } + """ + ) + + assert_style( + """ + %{ + # styler:sort + key: [ + 3, + 2, + 1 + ] + } + """, + """ + %{ + # styler:sort + key: [ + 1, + 2, + 3 + ] + } + """ + ) + end + test "sorts sigils" do assert_style("# styler:sort\n~w|c a b|", "# styler:sort\n~w|a b c|") From 88d3334811de7407cd95d0e784f1e2ab20f6e162 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 14 Jan 2025 11:00:30 -0700 Subject: [PATCH 45/86] v1.3.2 --- CHANGELOG.md | 2 ++ mix.exs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0056aa7f..4f65224e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +## 1.3.2 + ### Improvements - `# styler:sort` can be used to sort values of key-value pairs. eg, sort the value of a single key in a map (Closes #208, h/t @ypconstante) diff --git a/mix.exs b/mix.exs index 2c3040b3..7834455d 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.3.1" + @version "1.3.2" @url "https://github.com/adobe/elixir-styler" def project do From 9b0207b89547fbd4d613663bafdceaddf8acaf2d Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 15 Jan 2025 15:42:51 -0700 Subject: [PATCH 46/86] fix comments bug in styler:sort directive --- CHANGELOG.md | 4 ++++ lib/style/comment_directives.ex | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f65224e..327c8960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +### Fixes + +- fix a bug in comment-movement when multiple `# styler:sort` directives are added to a file at the same time + ## 1.3.2 ### Improvements diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index 5f70aa30..3c25c7c4 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -33,8 +33,7 @@ defmodule Styler.Style.CommentDirectives do end) if found do - {node, _} = found - {sorted, comments} = sort(node, ctx.comments) + {sorted, comments} = found |> Zipper.node() |> sort(comments) {Zipper.replace(found, sorted), comments} else {zipper, comments} From efb2cb9c5cf87c5ecbf5123455a35cf2f0f2c544 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 16 Jan 2025 14:13:59 -0700 Subject: [PATCH 47/86] styler:sort arbitrary ast within do end blocks --- CHANGELOG.md | 28 ++++++++++++++++++++++ lib/style/comment_directives.ex | 18 ++++++++++++++ test/style/comment_directives_test.exs | 33 ++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 327c8960..52ac2024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ they can and will change without that change being reflected in Styler's semantic version. ## main +### Improvements + +- `# styler:sort` will sort arbitrary ast nodes within a `do end` block: + + Given: + # styler:sort + my_macro "some arg" do + another_macro :q + another_macro :w + another_macro :e + another_macro :r + another_macro :t + another_macro :y + end + + We get + # styler:sort + my_macro "some arg" do + another_macro :e + another_macro :q + another_macro :r + another_macro :t + another_macro :w + another_macro :y + end + + + ### Fixes - fix a bug in comment-movement when multiple `# styler:sort` directives are added to a file at the same time diff --git a/lib/style/comment_directives.ex b/lib/style/comment_directives.ex index 3c25c7c4..95edec58 100644 --- a/lib/style/comment_directives.ex +++ b/lib/style/comment_directives.ex @@ -106,5 +106,23 @@ defmodule Styler.Style.CommentDirectives do {{key, value}, comments} end + # sorts arbitrary ast nodes within a `do end` list + defp sort({f, m, args} = node, comments) do + if m[:do] && m[:end] && match?([{{:__block__, _, [:do]}, {:__block__, _, _}}], List.last(args)) do + {[{{:__block__, m1, [:do]}, {:__block__, m2, nodes}}], args} = List.pop_at(args, -1) + + {nodes, comments} = + nodes + |> Enum.sort_by(&Macro.to_string/1) + |> Style.order_line_meta_and_comments(comments, m[:line]) + + args = List.insert_at(args, -1, [{{:__block__, m1, [:do]}, {:__block__, m2, nodes}}]) + + {{f, m, args}, comments} + else + {node, comments} + end + end + defp sort(x, comments), do: {x, comments} end diff --git a/test/style/comment_directives_test.exs b/test/style/comment_directives_test.exs index f7fa7b12..68b6f4f9 100644 --- a/test/style/comment_directives_test.exs +++ b/test/style/comment_directives_test.exs @@ -333,6 +333,39 @@ defmodule Styler.Style.CommentDirectivesTest do ) end + test "nodes within a do end block" do + assert_style( + """ + # styler:sort + my_macro "some arg" do + another_macro :q + # w + another_macro :w + another_macro :e + # r comment 1 + # r comment 2 + another_macro :r + another_macro :t + another_macro :y + end + """, + """ + # styler:sort + my_macro "some arg" do + another_macro(:e) + another_macro(:q) + # r comment 1 + # r comment 2 + another_macro(:r) + another_macro(:t) + # w + another_macro(:w) + another_macro(:y) + end + """ + ) + end + test "treats comments nicely" do assert_style( """ From 9983bf2ae08837ea1d16b1e87e782a41265800b0 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 21 Jan 2025 16:52:12 -0700 Subject: [PATCH 48/86] improve `with` statement replacements --- CHANGELOG.md | 3 +- lib/style/blocks.ex | 210 ++++++++++++++++++++----------------- lib/zipper.ex | 12 +-- test/style/blocks_test.exs | 7 +- test/zipper_test.exs | 6 +- 5 files changed, 131 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ac2024..2c95620c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ they can and will change without that change being reflected in Styler's semanti ### Improvements +- `with do: body` and variations with no arrows in the head will be rewritten to just `body` - `# styler:sort` will sort arbitrary ast nodes within a `do end` block: Given: @@ -30,8 +31,6 @@ they can and will change without that change being reflected in Styler's semanti another_macro :y end - - ### Fixes - fix a bug in comment-movement when multiple `# styler:sort` directives are added to a file at the same time diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index 96e10ce9..3cb1ed47 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -75,103 +75,32 @@ defmodule Styler.Style.Blocks do {:cont, Zipper.replace(zipper, {:if, m, children}), ctx} end - # Credo.Check.Refactor.WithClauses - def run({{:with, with_meta, children}, _} = zipper, ctx) when is_list(children) do - # a std lib `with` block will have at least one left arrow and a `do` body. anything else we skip ¯\_(ツ)_/¯ - arrow_or_match? = &(left_arrow?(&1) || match?({:=, _, _}, &1)) - - if Enum.any?(children, arrow_or_match?) and Enum.any?(children, &Style.do_block?/1) do - {preroll, children} = - children - |> Enum.map(fn - # `_ <- rhs` => `rhs` - {:<-, _, [{:_, _, _}, rhs]} -> rhs - # `lhs <- rhs` => `lhs = rhs` - {:<-, m, [{atom, _, nil} = lhs, rhs]} when is_atom(atom) -> {:=, m, [lhs, rhs]} - child -> child - end) - |> Enum.split_while(&(not left_arrow?(&1))) - - # after rewriting `x <- y()` to `x = y()` there are no more arrows. - # this never should've been a with statement at all! we can just replace it with assignments - if Enum.empty?(children) do - {:cont, replace_with_statement(zipper, preroll), ctx} - else - [[{{_, do_meta, _} = do_block, do_body} | elses] | reversed_clauses] = Enum.reverse(children) - {postroll, reversed_clauses} = Enum.split_while(reversed_clauses, &(not left_arrow?(&1))) - [{:<-, final_clause_meta, [lhs, rhs]} = _final_clause | rest] = reversed_clauses - - # drop singleton identity else clauses like `else foo -> foo end` - elses = - case elses do - [{{_, _, [:else]}, [{:->, _, [[left], right]}]}] -> if nodes_equivalent?(left, right), do: [], else: elses - _ -> elses - end - - {reversed_clauses, do_body} = - cond do - # Put the postroll into the body - Enum.any?(postroll) -> - {node, do_body_meta, do_children} = do_body - do_children = if node == :__block__, do: do_children, else: [do_body] - do_body = {:__block__, Keyword.take(do_body_meta, [:line]), Enum.reverse(postroll, do_children)} - {reversed_clauses, do_body} - - # Credo.Check.Refactor.RedundantWithClauseResult - Enum.empty?(elses) and nodes_equivalent?(lhs, do_body) -> - {rest, rhs} - - # no change - true -> - {reversed_clauses, do_body} - end - - do_line = do_meta[:line] - final_clause_line = final_clause_meta[:line] - - do_line = - cond do - do_meta[:format] == :keyword && final_clause_line + 1 >= do_line -> do_line - do_meta[:format] == :keyword -> final_clause_line + 1 - true -> final_clause_line - end - - do_block = Macro.update_meta(do_block, &Keyword.put(&1, :line, do_line)) - # disable keyword `, do:` since there will be multiple statements in the body - with_meta = - if Enum.any?(postroll), - do: Keyword.merge(with_meta, do: [line: with_meta[:line]], end: [line: Style.max_line(children) + 1]), - else: with_meta - - with_children = Enum.reverse(reversed_clauses, [[{do_block, do_body} | elses]]) - zipper = Zipper.replace(zipper, {:with, with_meta, with_children}) + def run({{:with, _, [[{{:__block__, _, [:do]}, body} | _]]}, _} = zipper, ctx) do + {:cont, Zipper.replace(zipper, body), ctx} + end - cond do - # oops! RedundantWithClauseResult removed the final arrow in this. no more need for a with statement! - Enum.empty?(reversed_clauses) -> - {:cont, replace_with_statement(zipper, preroll ++ with_children), ctx} - - # recurse if the # of `<-` have changed (this `with` could now be eligible for a `case` rewrite) - Enum.any?(preroll) -> - # put the preroll before the with statement in either a block we create or the existing parent block - zipper - |> Style.find_nearest_block() - |> Zipper.prepend_siblings(preroll) - |> run(ctx) - - # the # of `<-` canged, so we should have another look at this with statement - Enum.any?(postroll) -> - run(zipper, ctx) + # Credo.Check.Refactor.WithClauses + def run({{:with, _, children}, _} = zipper, ctx) when is_list(children) do + do_block? = Enum.any?(children, &Style.do_block?/1) + arrow_or_match? = Enum.any?(children, &(left_arrow?(&1) || match?({:=, _, _}, &1))) + + cond do + # we can style this! + do_block? and arrow_or_match? -> + style_with_statement(zipper, ctx) + + # `with (head_statements) do: x (else ...)` + do_block? -> + # head statements can be the empty list, if it matters + {head_statements, [[{{:__block__, _, [:do]}, body} | _]]} = Enum.split_while(children, &(not Style.do_block?(&1))) + [first | rest] = head_statements ++ [body] + # replace this `with` statement with its headers + body + zipper = zipper |> Zipper.replace(first) |> Zipper.insert_siblings(rest) + {:cont, zipper, ctx} - true -> - # of clauess didn't change, so don't reecurse or we'll loop FOREEEVEERR - {:cont, zipper, ctx} - end - end - else - # maybe this isn't a with statement - could be a function named `with` - # or it's just a with statement with no arrows, but that's too saddening to imagine - {:cont, zipper, ctx} + # maybe this isn't a with statement - could be a function named `with` or something. + true -> + {:cont, zipper, ctx} end end @@ -217,6 +146,97 @@ defmodule Styler.Style.Blocks do def run(zipper, ctx), do: {:cont, zipper, ctx} + # with statements can do _a lot_, so this beast of a function likewise does a lot. + defp style_with_statement({{:with, with_meta, children}, _} = zipper, ctx) do + {preroll, children} = + children + |> Enum.map(fn + # `_ <- rhs` => `rhs` + {:<-, _, [{:_, _, _}, rhs]} -> rhs + # `lhs <- rhs` => `lhs = rhs` + {:<-, m, [{atom, _, nil} = lhs, rhs]} when is_atom(atom) -> {:=, m, [lhs, rhs]} + child -> child + end) + |> Enum.split_while(&(not left_arrow?(&1))) + + # after rewriting `x <- y()` to `x = y()` there are no more arrows. + # this never should've been a with statement at all! we can just replace it with assignments + if Enum.empty?(children) do + {:cont, replace_with_statement(zipper, preroll), ctx} + else + [[{{_, do_meta, _} = do_block, do_body} | elses] | reversed_clauses] = Enum.reverse(children) + {postroll, reversed_clauses} = Enum.split_while(reversed_clauses, &(not left_arrow?(&1))) + [{:<-, final_clause_meta, [lhs, rhs]} = _final_clause | rest] = reversed_clauses + + # drop singleton identity else clauses like `else foo -> foo end` + elses = + case elses do + [{{_, _, [:else]}, [{:->, _, [[left], right]}]}] -> if nodes_equivalent?(left, right), do: [], else: elses + _ -> elses + end + + {reversed_clauses, do_body} = + cond do + # Put the postroll into the body + Enum.any?(postroll) -> + {node, do_body_meta, do_children} = do_body + do_children = if node == :__block__, do: do_children, else: [do_body] + do_body = {:__block__, Keyword.take(do_body_meta, [:line]), Enum.reverse(postroll, do_children)} + {reversed_clauses, do_body} + + # Credo.Check.Refactor.RedundantWithClauseResult + Enum.empty?(elses) and nodes_equivalent?(lhs, do_body) -> + {rest, rhs} + + # no change + true -> + {reversed_clauses, do_body} + end + + do_line = do_meta[:line] + final_clause_line = final_clause_meta[:line] + + do_line = + cond do + do_meta[:format] == :keyword && final_clause_line + 1 >= do_line -> do_line + do_meta[:format] == :keyword -> final_clause_line + 1 + true -> final_clause_line + end + + do_block = Macro.update_meta(do_block, &Keyword.put(&1, :line, do_line)) + # disable keyword `, do:` since there will be multiple statements in the body + with_meta = + if Enum.any?(postroll), + do: Keyword.merge(with_meta, do: [line: with_meta[:line]], end: [line: Style.max_line(children) + 1]), + else: with_meta + + with_children = Enum.reverse(reversed_clauses, [[{do_block, do_body} | elses]]) + zipper = Zipper.replace(zipper, {:with, with_meta, with_children}) + + cond do + # oops! RedundantWithClauseResult removed the final arrow in this. no more need for a with statement! + Enum.empty?(reversed_clauses) -> + {:cont, replace_with_statement(zipper, preroll ++ with_children), ctx} + + # recurse if the # of `<-` have changed (this `with` could now be eligible for a `case` rewrite) + Enum.any?(preroll) -> + # put the preroll before the with statement in either a block we create or the existing parent block + zipper + |> Style.find_nearest_block() + |> Zipper.prepend_siblings(preroll) + |> run(ctx) + + # the # of `<-` canged, so we should have another look at this with statement + Enum.any?(postroll) -> + run(zipper, ctx) + + true -> + # of clauess didn't change, so don't reecurse or we'll loop FOREEEVEERR + {:cont, zipper, ctx} + end + end + end + # `with a <- b(), c <- d(), do: :ok, else: (_ -> :error)` # => # `a = b(); c = d(); :ok` diff --git a/lib/zipper.ex b/lib/zipper.ex index 0cdb17e8..9b43d1e1 100644 --- a/lib/zipper.ex +++ b/lib/zipper.ex @@ -172,18 +172,18 @@ defmodule Styler.Zipper do top level. """ @spec insert_left(zipper, tree) :: zipper - def insert_left({_, nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.") - def insert_left({tree, meta}, child), do: {tree, %{meta | l: [child | meta.l]}} + def insert_left(zipper, child), do: prepend_siblings(zipper, [child]) @doc """ Inserts many siblings to the left. + If the node is at the top of the tree, builds a new root `:__block__` while maintaining focus on the current node. Equivalent to Enum.reduce(siblings, zipper, &Zipper.insert_left(&2, &1)) """ @spec prepend_siblings(zipper, [tree]) :: zipper - def prepend_siblings({_, nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.") + def prepend_siblings({node, nil}, siblings), do: {:__block__, [], siblings ++ [node]} |> zip() |> down() |> rightmost() def prepend_siblings({tree, meta}, siblings), do: {tree, %{meta | l: Enum.reverse(siblings, meta.l)}} @doc """ @@ -192,18 +192,18 @@ defmodule Styler.Zipper do top level. """ @spec insert_right(zipper, tree) :: zipper - def insert_right({_, nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.") - def insert_right({tree, meta}, child), do: {tree, %{meta | r: [child | meta.r]}} + def insert_right(zipper, child), do: insert_siblings(zipper, [child]) @doc """ Inserts many siblings to the right. + If the node is at the top of the tree, builds a new root `:__block__` while maintaining focus on the current node. Equivalent to Enum.reduce(siblings, zipper, &Zipper.insert_right(&2, &1)) """ @spec insert_siblings(zipper, [tree]) :: zipper - def insert_siblings({_, nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.") + def insert_siblings({node, nil}, siblings), do: {:__block__, [], [node | siblings]} |> zip() |> down() def insert_siblings({tree, meta}, siblings), do: {tree, %{meta | r: siblings ++ meta.r}} @doc """ diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index fae1144b..83cbab40 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -384,6 +384,11 @@ defmodule Styler.Style.BlocksTest do z """ ) + + assert_style "with do: x", "x" + assert_style "with do x end", "x" + assert_style "with do x else foo -> bar end", "x" + assert_style "with foo() do bar() else _ -> baz() end", "foo()\nbar()" end test "doesn't false positive with vars" do @@ -430,7 +435,7 @@ defmodule Styler.Style.BlocksTest do """ ) - for nontrivial_head <- ["foo", ":ok <- foo, :ok <- bar"] do + for nontrivial_head <- [":ok <- foo, :ok <- bar"] do assert_style(""" with #{nontrivial_head} do :success diff --git a/test/zipper_test.exs b/test/zipper_test.exs index fc03cd28..2359d481 100644 --- a/test/zipper_test.exs +++ b/test/zipper_test.exs @@ -470,9 +470,9 @@ defmodule StylerTest.ZipperTest do |> Zipper.root() == [1, :left, 2, :right, 3] end - test "raise when attempting to insert a sibling at the root" do - assert_raise ArgumentError, fn -> 42 |> Zipper.zip() |> Zipper.insert_left(:nope) end - assert_raise ArgumentError, fn -> 42 |> Zipper.zip() |> Zipper.insert_right(:nope) end + test "builds a new root node made of a block" do + assert {42, %{l: [:nope], ptree: {{:__block__, _, _}, nil}}} = 42 |> Zipper.zip() |> Zipper.insert_left(:nope) + assert {42, %{r: [:nope], ptree: {{:__block__, _, _}, nil}}} = 42 |> Zipper.zip() |> Zipper.insert_right(:nope) end end From 470b3906fbcf16482b0784f3bfc6c1b9ef37be22 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 21 Jan 2025 16:54:20 -0700 Subject: [PATCH 49/86] v1.3.3 --- CHANGELOG.md | 3 +++ mix.exs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c95620c..ca40bf50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ **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. + ## main +## 1.3.3 + ### Improvements - `with do: body` and variations with no arrows in the head will be rewritten to just `body` diff --git a/mix.exs b/mix.exs index 7834455d..8d3df8e5 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.3.2" + @version "1.3.3" @url "https://github.com/adobe/elixir-styler" def project do From 7932147e5e986b954eedf1bc38d3f080c04fea89 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 24 Jan 2025 07:13:16 -0500 Subject: [PATCH 50/86] alias lifting: shrink when alias already exists. closes #201 --- CHANGELOG.md | 18 +++++++++++ lib/style/module_directives.ex | 32 ++++++++++++------- .../module_directives/alias_lifting_test.exs | 19 +++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca40bf50..2902eae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ they can and will change without that change being reflected in Styler's semanti ## main +### Improvements + +- alias lifting: styler will now replace an expanded alias with its alias when the user has already defined that alias (#201, h/t me) + + example: + alias A.B.C + + A.B.C.foo() + A.B.C.bar() + A.B.C.baz() + + becomes: + alias A.B.C + + C.foo() + C.bar() + C.baz() + ## 1.3.3 ### Improvements diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index eb10761e..6d442dd5 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -282,8 +282,7 @@ defmodule Styler.Style.ModuleDirectives do # we can't use the dealias map built into state as that's what things look like before sorting # now that we've sorted, it could be different! dealiases = AliasEnv.define(aliases) - excluded = dealiases |> Map.keys() |> Enum.into(Styler.Config.get(:lifting_excludes)) - liftable = find_liftable_aliases(requires ++ nondirectives, excluded) + liftable = find_liftable_aliases(requires ++ nondirectives, dealiases) if Enum.any?(liftable) do # This is a silly hack that helps comments stay put. @@ -306,7 +305,9 @@ defmodule Styler.Style.ModuleDirectives do end end - defp find_liftable_aliases(ast, excluded) do + defp find_liftable_aliases(ast, dealiases) do + excluded = dealiases |> Map.keys() |> Enum.into(Styler.Config.get(:lifting_excludes)) + ast |> Zipper.zip() |> Zipper.reduce_while(%{}, fn @@ -333,15 +334,22 @@ defmodule Styler.Style.ModuleDirectives do last = List.last(aliases) lifts = - if last in excluded or not Enum.all?(aliases, &is_atom/1) do - lifts - else - Map.update(lifts, last, {aliases, false}, fn - {^aliases, _} -> {aliases, true} - # if we have `Foo.Bar.Baz` and `Foo.Bar.Bop.Baz` both not aliased, we'll create a collision by lifting both - # grouping by last alias lets us detect these collisions - _ -> :collision_with_last - end) + cond do + # this alias already exists, they just wrote it out fully and are leaving it up to us to shorten it down! + dealiases[last] == aliases -> + Map.put(lifts, last, {aliases, true}) + + last in excluded or Enum.any?(aliases, &(not is_atom(&1))) -> + lifts + + # track how often we see this alias - once we've seen it a second time we'll known + true -> + Map.update(lifts, last, {aliases, false}, fn + {^aliases, _} -> {aliases, true} + # if we have `Foo.Bar.Baz` and `Foo.Bar.Bop.Baz` both not aliased, we'll create a collision by lifting both + # grouping by last alias lets us detect these collisions + _ -> :collision_with_last + end) end {:skip, zipper, lifts} diff --git a/test/style/module_directives/alias_lifting_test.exs b/test/style/module_directives/alias_lifting_test.exs index e1793c0d..454654d4 100644 --- a/test/style/module_directives/alias_lifting_test.exs +++ b/test/style/module_directives/alias_lifting_test.exs @@ -202,6 +202,25 @@ defmodule Styler.Style.ModuleDirectives.AliasLiftingTest do ) end + test "replaces known aliases" do + assert_style( + """ + alias A.B.C + + A.B.C.foo() + A.B.C.foo() + A.B.C.foo() + """, + """ + alias A.B.C + + C.foo() + C.foo() + C.foo() + """ + ) + end + describe "comments stay put" do test "comments before alias stanza" do assert_style( From ff7755d64d1bb588dac98edc2952dc07ec1134ee Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 24 Jan 2025 11:26:46 -0500 Subject: [PATCH 51/86] alias lifting: be better about conflicts. Closes #193 --- CHANGELOG.md | 6 +- lib/style/module_directives.ex | 60 ++++++- .../module_directives/alias_lifting_test.exs | 157 +++++++++++++++--- 3 files changed, 196 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2902eae2..09724539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ they can and will change without that change being reflected in Styler's semanti ### Improvements -- alias lifting: styler will now replace an expanded alias with its alias when the user has already defined that alias (#201, h/t me) +This release taught Styler to try just that little bit harder when doing alias lifting. + +- general improvements around conflict detection, lifting in more correct places and fewer incorrect places (#193, h/t @jsw800) +- use knowledge of existing aliases to shorten invocations (#201, h/t me) example: alias A.B.C @@ -23,6 +26,7 @@ they can and will change without that change being reflected in Styler's semanti C.bar() C.baz() + ## 1.3.3 ### Improvements diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index 6d442dd5..bafc0124 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -308,8 +308,12 @@ defmodule Styler.Style.ModuleDirectives do defp find_liftable_aliases(ast, dealiases) do excluded = dealiases |> Map.keys() |> Enum.into(Styler.Config.get(:lifting_excludes)) + firsts = MapSet.new(dealiases, fn {_last, [first | _]} -> first end) + ast |> Zipper.zip() + # we're reducing a datastructure that looks like + # %{last => {aliases, seen_before?} | :some_collision_probelm} |> Zipper.reduce_while(%{}, fn # we don't want to rewrite alias name `defx Aliases ... do` of these three keywords {{defx, _, args}, _} = zipper, lifts when defx in ~w(defmodule defimpl defprotocol)a -> @@ -330,7 +334,7 @@ defmodule Styler.Style.ModuleDirectives do {{:quote, _, _}, _} = zipper, lifts -> {:skip, zipper, lifts} - {{:__aliases__, _, [_, _, _ | _] = aliases}, _} = zipper, lifts -> + {{:__aliases__, _, [first, _, _ | _] = aliases}, _} = zipper, lifts -> last = List.last(aliases) lifts = @@ -342,13 +346,54 @@ defmodule Styler.Style.ModuleDirectives do last in excluded or Enum.any?(aliases, &(not is_atom(&1))) -> lifts - # track how often we see this alias - once we've seen it a second time we'll known + # aliasing this would change the meaning of an existing alias + last > first and last in firsts -> + lifts + + # We've seen this once before, time to mark it for lifting and do some bookkeeping for first-collisions + lifts[last] == {aliases, false} -> + lifts + |> Map.put(last, {aliases, true}) + |> Map.update!(first, fn + {:collision_with_first, claimants, colliders} -> + # release our claim on this collision + claimants = MapSet.delete(claimants, aliases) + + if Enum.empty?(claimants) and Enum.any?(colliders) do + # no more claimants, try to promote a collider to be lifted + + colliders = Enum.to_list(colliders) + # There's no longer a collision because the only claimant is being lifted. + # So, promote a claimant with these criteria + # - required: its first comes _after_ last, so we aren't promoting an alias that changes the meaning of the other alias we're doing + # - preferred: take a collider we know we want to lift (we've seen it multiple times) + Enum.find(colliders, fn {[first | _], seen?} -> seen? and first > last end) || + Enum.find(colliders, fn {[first | _], _} -> first > last end) || + :collision_with_first + else + {:collision_with_first, claimants, colliders} + end + + other -> + other + end) + true -> - Map.update(lifts, last, {aliases, false}, fn - {^aliases, _} -> {aliases, true} - # if we have `Foo.Bar.Baz` and `Foo.Bar.Bop.Baz` both not aliased, we'll create a collision by lifting both - # grouping by last alias lets us detect these collisions - _ -> :collision_with_last + lifts + |> Map.update(last, {aliases, false}, fn + # if something is claiming the atom we want, add ourselves to the list of colliders + {:collision_with_first, claimers, colliders} -> + {:collision_with_first, claimers, Map.update(colliders, aliases, false, fn _ -> true end)} + + other -> + other + end) + |> Map.update(first, {:collision_with_first, MapSet.new([aliases]), %{}}, fn + {:collision_with_first, claimers, colliders} -> + {:collision_with_first, MapSet.put(claimers, aliases), colliders} + + other -> + other end) end @@ -362,6 +407,7 @@ defmodule Styler.Style.ModuleDirectives do # C.foo() # # lifting A.B.C would create a collision with C. + # unlike the collision_with_first tuple book-keeping, there's no recovery here because we won't lift a < 3 length alias {:skip, zipper, Map.put(lifts, first, :collision_with_first)} zipper, lifts -> diff --git a/test/style/module_directives/alias_lifting_test.exs b/test/style/module_directives/alias_lifting_test.exs index 454654d4..b08befe7 100644 --- a/test/style/module_directives/alias_lifting_test.exs +++ b/test/style/module_directives/alias_lifting_test.exs @@ -221,6 +221,109 @@ defmodule Styler.Style.ModuleDirectives.AliasLiftingTest do ) end + test "two modules that seem to conflict but don't!" do + assert_style( + """ + defmodule Foo do + @moduledoc false + + A.B.C.foo(X.Y.A) + A.B.C.bar() + + X.Y.A + end + """, + """ + defmodule Foo do + @moduledoc false + + alias A.B.C + alias X.Y.A + + C.foo(A) + C.bar() + + A + end + """ + ) + end + + test "if multiple lifts collide, lifts only one" do + assert_style( + """ + defmodule Foo do + @moduledoc false + + A.B.C.f() + A.B.C.f() + X.Y.C.f() + end + """, + """ + defmodule Foo do + @moduledoc false + + alias A.B.C + + C.f() + C.f() + X.Y.C.f() + end + """ + ) + + assert_style( + """ + defmodule Foo do + @moduledoc false + + A.B.C.f() + X.Y.C.f() + X.Y.C.f() + A.B.C.f() + end + """, + """ + defmodule Foo do + @moduledoc false + + alias A.B.C + + C.f() + X.Y.C.f() + X.Y.C.f() + C.f() + end + """ + ) + + assert_style( + """ + defmodule Foo do + @moduledoc false + + X.Y.C.f() + A.B.C.f() + X.Y.C.f() + A.B.C.f() + end + """, + """ + defmodule Foo do + @moduledoc false + + alias X.Y.C + + C.f() + A.B.C.f() + C.f() + A.B.C.f() + end + """ + ) + end + describe "comments stay put" do test "comments before alias stanza" do assert_style( @@ -306,44 +409,60 @@ defmodule Styler.Style.ModuleDirectives.AliasLiftingTest do end end - test "collisions with other lifts" do + test "collisions with submodules" do assert_style """ - defmodule NuhUh do + defmodule A do @moduledoc false A.B.C.f() + + defmodule C do + @moduledoc false + A.B.C.f() + end + A.B.C.f() - X.Y.C.f() end """ + end + test "collisions with 3-deep one-off" do assert_style """ - defmodule NuhUh do + defmodule Foo do @moduledoc false - A.B.C.f() - A.B.C.f() - X.Y.C.f() - X.Y.C.f() + X.Y.Z.foo(A.B.X) + + A.B.X end """ end - test "collisions with submodules" do - assert_style """ - defmodule A do - @moduledoc false + test "when new alias being sorted in would change an existing alias" do + assert_style( + """ + defmodule Foo do + @moduledoc false - A.B.C.f() + X.Y.Z.foo(A.B.X) + X.Y.Z.bar() - defmodule C do - @moduledoc false - A.B.C.f() + A.B.X end + """, + """ + defmodule Foo do + @moduledoc false - A.B.C.f() - end - """ + alias X.Y.Z + + Z.foo(A.B.X) + Z.bar() + + A.B.X + end + """ + ) end test "defprotocol, defmodule, or defimpl" do From 3750e0c8cd22b866f530bece05f592c444b548f6 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 24 Jan 2025 10:39:50 -0700 Subject: [PATCH 52/86] improve alias lift collision case --- lib/style/module_directives.ex | 49 ++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index bafc0124..204ecd21 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -352,31 +352,40 @@ defmodule Styler.Style.ModuleDirectives do # We've seen this once before, time to mark it for lifting and do some bookkeeping for first-collisions lifts[last] == {aliases, false} -> - lifts - |> Map.put(last, {aliases, true}) - |> Map.update!(first, fn + lifts = Map.put(lifts, last, {aliases, true}) + + # Here's the bookkeeping for collisions with this alias's first module name... + case lifts[first] do {:collision_with_first, claimants, colliders} -> # release our claim on this collision claimants = MapSet.delete(claimants, aliases) - - if Enum.empty?(claimants) and Enum.any?(colliders) do - # no more claimants, try to promote a collider to be lifted - - colliders = Enum.to_list(colliders) - # There's no longer a collision because the only claimant is being lifted. - # So, promote a claimant with these criteria - # - required: its first comes _after_ last, so we aren't promoting an alias that changes the meaning of the other alias we're doing - # - preferred: take a collider we know we want to lift (we've seen it multiple times) - Enum.find(colliders, fn {[first | _], seen?} -> seen? and first > last end) || - Enum.find(colliders, fn {[first | _], _} -> first > last end) || - :collision_with_first - else - {:collision_with_first, claimants, colliders} + empty? = Enum.empty?(claimants) + + cond do + empty? and Enum.any?(colliders) -> + # no more claimants, try to promote a collider to be lifted + colliders = Enum.to_list(colliders) + # There's no longer a collision because the only claimant is being lifted. + # So, promote a claimant with these criteria + # - required: its first comes _after_ last, so we aren't promoting an alias that changes the meaning of the other alias we're doing + # - preferred: take a collider we know we want to lift (we've seen it multiple times) + lift = + Enum.find(colliders, fn {[first | _], seen?} -> seen? and first > last end) || + Enum.find(colliders, fn {[first | _], _} -> first > last end) || + :collision_with_first + + Map.put(lifts, first, lift) + + empty? -> + Map.delete(lifts, first) + + true -> + Map.put(lifts, first, {:collision_with_first, claimants, colliders}) end - other -> - other - end) + _ -> + lifts + end true -> lifts From fc71aee72bfa2a64122139ff0e472f35bc6c5d14 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Sat, 25 Jan 2025 08:54:54 -0700 Subject: [PATCH 53/86] remove vestigial with rewriting head --- lib/style/blocks.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index 3cb1ed47..ac8e7a51 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -75,10 +75,6 @@ defmodule Styler.Style.Blocks do {:cont, Zipper.replace(zipper, {:if, m, children}), ctx} end - def run({{:with, _, [[{{:__block__, _, [:do]}, body} | _]]}, _} = zipper, ctx) do - {:cont, Zipper.replace(zipper, body), ctx} - end - # Credo.Check.Refactor.WithClauses def run({{:with, _, children}, _} = zipper, ctx) when is_list(children) do do_block? = Enum.any?(children, &Style.do_block?/1) From 9404d5fc39309d7f0579ae51f2e8423ebabc828f Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 13 Feb 2025 10:33:19 -0700 Subject: [PATCH 54/86] pipes: handle pipifying functions whose first arg is itself a pipe. closes #193 --- CHANGELOG.md | 3 +++ lib/style/module_directives.ex | 13 ++++++++++--- lib/style/pipes.ex | 5 ++++- test/style/pipes_test.exs | 4 ++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09724539..45f7c69d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ This release taught Styler to try just that little bit harder when doing alias l C.bar() C.baz() +### Fixes + +- `pipes`: handle pipifying when the first arg is itself a pipe: `c(a |> b, d)` => `a |> b() |> c(d)` (#213, h/t @kybishop) ## 1.3.3 diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index 204ecd21..b88f3c62 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -370,9 +370,16 @@ defmodule Styler.Style.ModuleDirectives do # - required: its first comes _after_ last, so we aren't promoting an alias that changes the meaning of the other alias we're doing # - preferred: take a collider we know we want to lift (we've seen it multiple times) lift = - Enum.find(colliders, fn {[first | _], seen?} -> seen? and first > last end) || - Enum.find(colliders, fn {[first | _], _} -> first > last end) || - :collision_with_first + Enum.reduce_while(colliders, :collision_with_first, fn + {[first | _], true} = liftable, _ when first > last -> + {:halt, liftable} + + {[first | _], _false} = promotable, :collision_with_first when first > last -> + {:cont, promotable} + + _, result -> + {:cont, result} + end) Map.put(lifts, first, lift) diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index cb269a7f..2b43338b 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -168,7 +168,10 @@ defmodule Styler.Style.Pipes do {:cont, zipper, ctx} true -> - {:cont, Zipper.replace(zipper, {:|>, m, [pipe, {f, m, args}]}), ctx} + # Recurse in case the function-looking is a multi pipe + zipper + |> Zipper.replace({:|>, m, [pipe, {f, m, args}]}) + |> run(ctx) end end diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 3feae8ec..ae1dee90 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -934,6 +934,9 @@ defmodule Styler.Style.PipesTest do assert_style ~s<"\#{#{pipe}}"> end + test "pipifying pipes" do + end + test "when it's not actually the first argument!" do assert_style """ a @@ -944,6 +947,7 @@ defmodule Styler.Style.PipesTest do test "pipifying" do assert_style "d(a |> b |> c)", "a |> b() |> c() |> d()" + assert_style("c(a |> b, d)", "a |> b() |> c(d)") assert_style( """ From ff004cab02ed75fd76e225ac4acf991480695071 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 13 Feb 2025 10:35:54 -0700 Subject: [PATCH 55/86] =?UTF-8?q?cleanup=20the=20messes=20left=20in=20the?= =?UTF-8?q?=20previous=20commit=20=F0=9F=99=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/style/pipes.ex | 2 +- test/style/pipes_test.exs | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index 2b43338b..51b4ca92 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -168,7 +168,7 @@ defmodule Styler.Style.Pipes do {:cont, zipper, ctx} true -> - # Recurse in case the function-looking is a multi pipe + # Recurse in case this is a multi pipe zipper |> Zipper.replace({:|>, m, [pipe, {f, m, args}]}) |> run(ctx) diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index ae1dee90..5a4332ec 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -934,9 +934,6 @@ defmodule Styler.Style.PipesTest do assert_style ~s<"\#{#{pipe}}"> end - test "pipifying pipes" do - end - test "when it's not actually the first argument!" do assert_style """ a From 5a23833ce5c0661323b815ef7eceb284450f6d54 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 13 Feb 2025 11:01:04 -0700 Subject: [PATCH 56/86] correct issue number in change log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f7c69d..d6acd664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ This release taught Styler to try just that little bit harder when doing alias l ### Fixes -- `pipes`: handle pipifying when the first arg is itself a pipe: `c(a |> b, d)` => `a |> b() |> c(d)` (#213, h/t @kybishop) +- `pipes`: handle pipifying when the first arg is itself a pipe: `c(a |> b, d)` => `a |> b() |> c(d)` (#214, h/t @kybishop) ## 1.3.3 From 297106ac8c25a2f199824331ee57cae80ea93959 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 10:08:33 -0700 Subject: [PATCH 57/86] test against 1.18 --- .github/workflows/ci.yml | 2 +- .tool-versions | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a51e0fc2..9ab4bb2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: name: Ex${{matrix.elixir}}/OTP${{matrix.otp}} strategy: matrix: - elixir: ['1.15.8', '1.16.3', '1.17.3'] + elixir: ['1.15.8', '1.16.3', '1.17.3', '1.18.2'] otp: ['25.3.2'] steps: - uses: actions/checkout@v4 diff --git a/.tool-versions b/.tool-versions index 16f4970a..a6cd6f91 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ -erlang 26.1.2 -elixir 1.16.0-otp-26 +elixir 1.18.2-otp-27 +erlang 27.2.3 +nodejs 16.11.1 From ee34edd3a2476c8884a67e93bd3c54c9dbbeddd0 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 10:19:29 -0700 Subject: [PATCH 58/86] 1.18 warnings + formatting --- lib/style/deprecations.ex | 4 ++-- lib/style/pipes.ex | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex index f810e5a3..e536cbff 100644 --- a/lib/style/deprecations.ex +++ b/lib/style/deprecations.ex @@ -98,7 +98,7 @@ defmodule Styler.Style.Deprecations do defp style(node), do: node - defp rewrite_range_match({:.., dm, [first, {_, m, _} = last]}), do: {:"..//", dm, [first, last, {:_, m, nil}]} + defp rewrite_range_match({:.., dm, [first, {_, m, _} = last]}), do: {:..//, dm, [first, last, {:_, m, nil}]} defp rewrite_range_match(x), do: x defp add_step_to_date_range?(first, last) do @@ -117,7 +117,7 @@ defmodule Styler.Style.Deprecations do {:ok, stop} <- extract_value_from_range(last), true <- start > stop do step = {:__block__, [token: "1", line: lm[:line]], [1]} - {:"..//", rm, [first, last, step]} + {:..//, rm, [first, last, step]} else _ -> range end diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index 51b4ca92..bffb5e1f 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -95,7 +95,7 @@ defmodule Styler.Style.Pipes do comments = ctx.comments - |> Style.displace_comments(lhs_line..(rhs_line - 1)) + |> Style.displace_comments(lhs_line..(rhs_line - 1)//1) |> Style.shift_comments(rhs_line..rhs_max_line, shift + 1) {:cont, Zipper.replace(single_pipe_zipper, {fun, meta, [lhs | args]}), %{ctx | comments: comments}} From 8d921d9ad33a23575aa3fd9cdd7027041406155e Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 10:31:26 -0700 Subject: [PATCH 59/86] no one saw that right? --- .tool-versions | 1 - 1 file changed, 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index a6cd6f91..0b6770c6 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,2 @@ elixir 1.18.2-otp-27 erlang 27.2.3 -nodejs 16.11.1 From e083b4b9de5dc44c98e51e06494eec4bfb27b80a Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 10:18:35 -0700 Subject: [PATCH 60/86] ex1.17+: replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` Closes #218 --- CHANGELOG.md | 6 +++++ lib/style/deprecations.ex | 7 +++++ test/style/deprecations_test.exs | 46 +++++++++++++++++++++----------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6acd664..351a1afb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ they can and will change without that change being reflected in Styler's semanti ### Improvements +#### Ex1.17+ + +Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` + +#### Alias Lifting + This release taught Styler to try just that little bit harder when doing alias lifting. - general improvements around conflict detection, lifting in more correct places and fewer incorrect places (#193, h/t @jsw800) diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex index e536cbff..4edeaa2b 100644 --- a/lib/style/deprecations.ex +++ b/lib/style/deprecations.ex @@ -58,6 +58,13 @@ defmodule Styler.Style.Deprecations do do: {:|>, m, [lhs, {f, fm, [lob, opts]}]} end + if Version.match?(System.version(), ">= 1.17.0-dev") do + for {erl, ex} <- [hours: :hour, minutes: :minute, seconds: :second] do + defp style({{:., _, [{:__block__, _, [:timer]}, unquote(erl)]}, fm, [x]}), + do: {:to_timeout, fm, [[{{:__block__, [format: :keyword, line: fm[:line]], [unquote(ex)]}, x}]]} + end + end + # For ranges where `start > stop`, you need to explicitly include the step # Enum.slice(enumerable, 1..-2) => Enum.slice(enumerable, 1..-2//1) # String.slice("elixir", 2..-1) => String.slice("elixir", 2..-1//1) diff --git a/test/style/deprecations_test.exs b/test/style/deprecations_test.exs index cb9396fd..8b5211b3 100644 --- a/test/style/deprecations_test.exs +++ b/test/style/deprecations_test.exs @@ -67,22 +67,6 @@ defmodule Styler.Style.DeprecationsTest do assert_style "foo |> List.zip", "Enum.zip(foo)" end - describe "1.16 deprecations" do - @describetag skip: Version.match?(System.version(), "< 1.16.0-dev") - - test "File.stream!(path, modes, line_or_bytes) to File.stream!(path, line_or_bytes, modes)" do - assert_style( - "File.stream!(path, [encoding: :utf8, trim_bom: true], :line)", - "File.stream!(path, :line, encoding: :utf8, trim_bom: true)" - ) - - assert_style( - "f |> File.stream!([encoding: :utf8, trim_bom: true], :line) |> Enum.take(2)", - "f |> File.stream!(:line, encoding: :utf8, trim_bom: true) |> Enum.take(2)" - ) - end - end - test "~R is deprecated in favor of ~r" do assert_style(~s|Regex.match?(~R/foo/, "foo")|, ~s|Regex.match?(~r/foo/, "foo")|) end @@ -131,4 +115,34 @@ defmodule Styler.Style.DeprecationsTest do assert_style("foo |> bar() |> #{mod}.slice(x..y)") end end + + describe "1.16+" do + @describetag skip: Version.match?(System.version(), "< 1.16.0-dev") + + test "File.stream!(path, modes, line_or_bytes) to File.stream!(path, line_or_bytes, modes)" do + assert_style( + "File.stream!(path, [encoding: :utf8, trim_bom: true], :line)", + "File.stream!(path, :line, encoding: :utf8, trim_bom: true)" + ) + + assert_style( + "f |> File.stream!([encoding: :utf8, trim_bom: true], :line) |> Enum.take(2)", + "f |> File.stream!(:line, encoding: :utf8, trim_bom: true) |> Enum.take(2)" + ) + end + end + + describe "1.17+" do + @describetag skip: Version.match?(System.version(), "< 1.17.0-dev") + + test "to_timeout/1 vs :timer.units(x)" do + assert_style ":timer.hours(x)", "to_timeout(hour: x)" + assert_style ":timer.minutes(x)", "to_timeout(minute: x)" + assert_style ":timer.seconds(x)", "to_timeout(second: x)" + + assert_style "a |> x() |> :timer.hours()" + assert_style "a |> x() |> :timer.minutes()" + assert_style "a |> x() |> :timer.seconds()" + end + end end From d0ecf1d1219cd1fca6144d1c5afbbe43e7df9919 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 10:53:23 -0700 Subject: [PATCH 61/86] 1.18+: change struct updates to map updates. Closes #199 --- CHANGELOG.md | 13 +++++++++++++ lib/style/deprecations.ex | 5 +++++ test/style/deprecations_test.exs | 8 ++++++++ test/style/pipes_test.exs | 2 +- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351a1afb..9ebaff73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,19 @@ they can and will change without that change being reflected in Styler's semanti Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` +#### Ex1.18+ + +Delete deprecated struct update syntax in favor of map update syntax. + +```elixir +# This +%Struct{x | y} +# Styles to this +%{x | y} +``` + +**WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.18's type checking features. (#199, h/t @SteffenDE) + #### Alias Lifting This release taught Styler to try just that little bit harder when doing alias lifting. diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex index 4edeaa2b..93baee08 100644 --- a/lib/style/deprecations.ex +++ b/lib/style/deprecations.ex @@ -65,6 +65,11 @@ defmodule Styler.Style.Deprecations do end end + if Version.match?(System.version(), ">= 1.18.0-dev") do + # Struct update syntax was deprecated `%Foo{x | y} => %{x | y}` + defp style({:%, _, [_struct, {:%{}, _, [{:|, _, _}]} = update]}), do: update + end + # For ranges where `start > stop`, you need to explicitly include the step # Enum.slice(enumerable, 1..-2) => Enum.slice(enumerable, 1..-2//1) # String.slice("elixir", 2..-1) => String.slice("elixir", 2..-1//1) diff --git a/test/style/deprecations_test.exs b/test/style/deprecations_test.exs index 8b5211b3..140a87c4 100644 --- a/test/style/deprecations_test.exs +++ b/test/style/deprecations_test.exs @@ -145,4 +145,12 @@ defmodule Styler.Style.DeprecationsTest do assert_style "a |> x() |> :timer.seconds()" end end + + describe "1.18+" do + @describetag skip: Version.match?(System.version(), "< 1.18.0-dev") + + test "struct update" do + assert_style "%Foo{widget | bar: :baz}", "%{widget | bar: :baz}" + end + end end diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index 5a4332ec..c3858c71 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -408,7 +408,7 @@ defmodule Styler.Style.PipesTest do """, """ def halt(exec, halt_message) do - put_halt_message(%__MODULE__{exec | halted: true}, halt_message) + put_halt_message(%{exec | halted: true}, halt_message) end """ ) From 74d6fd25af0adc2830d1210253898db6a20cb21f Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 11:34:16 -0700 Subject: [PATCH 62/86] ensure test works across versions --- test/style/pipes_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index c3858c71..f87b3a36 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -402,7 +402,7 @@ defmodule Styler.Style.PipesTest do assert_style( """ def halt(exec, halt_message) do - %__MODULE__{exec | halted: true} + %{exec | halted: true} |> put_halt_message(halt_message) end """, From fc6fb5d4cb4b9d9b1cd139aee9cad0f628060fc1 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 12:06:17 -0700 Subject: [PATCH 63/86] fix `with` rewrites when keyword with an else stab (#220) Closes #219 fixes for both <1.17 and >=1.17 --- CHANGELOG.md | 4 +++- lib/style/blocks.ex | 25 +++++++++++++++++++++++-- test/style/blocks_test.exs | 16 +++++++++++++++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ebaff73..7468937b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ they can and will change without that change being reflected in Styler's semanti #### Ex1.17+ -Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` +- Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` +- Handle `, else: (_ -> x)` bugs introduced by `(_ -> x)` being termed a literal (#219, h/t @iamhassangm) #### Ex1.18+ @@ -48,6 +49,7 @@ This release taught Styler to try just that little bit harder when doing alias l ### Fixes - `pipes`: handle pipifying when the first arg is itself a pipe: `c(a |> b, d)` => `a |> b() |> c(d)` (#214, h/t @kybishop) +- `with`: correctly handle a stabby `with` `, else: (_ -> :ok)` being rewritten to a case (#219, h/t @iamhassangm) ## 1.3.3 diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index ac8e7a51..fd51be13 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -53,10 +53,31 @@ defmodule Styler.Style.Blocks do # to `case single_statement do success -> body; ...elses end` def run({{:with, m, [{:<-, am, [success, single_statement]}, [body, elses]]}, zm}, ctx) do {{:__block__, do_meta, [:do]}, body} = body - {{:__block__, _else_meta, [:else]}, elses} = elses + {{:__block__, _, [:else]}, elses} = elses + + elses = + case elses do + # unwrap a stab ala `, else: (_ -> :ok)`. these became literals in 1.17 + {:__block__, _, [[{:->, _, _}] = stab]} -> stab + elses -> elses + end + + # drops keyword formatting etc + do_meta = [line: do_meta[:line]] clauses = [{{:__block__, am, [:do]}, [{:->, do_meta, [[success], body]} | elses]}] + end_line = Style.max_line(elses) + 1 + + # fun fact: i added the detailed meta just because i noticed it was missing while debugging something ... + # ... and it fixed the bug 🤷 + case_meta = [ + end_of_expression: [newlines: 1, line: end_line], + do: do_meta, + end: [line: end_line], + line: m[:line] + ] + # recurse in case this new case should be rewritten to a `if`, etc - run({{:case, m, [single_statement, clauses]}, zm}, ctx) + run({{:case, case_meta, [single_statement, clauses]}, zm}, ctx) end # `with true <- x, do: bar` =>`if x, do: bar` diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index 83cbab40..cf2d1644 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -239,7 +239,7 @@ defmodule Styler.Style.BlocksTest do end end - describe "with statements" do + describe "with" do test "replacement due to no (or all removed) arrows" do assert_style( """ @@ -787,6 +787,20 @@ defmodule Styler.Style.BlocksTest do end """ end + + test "elixir1.17+ stab regressions" do + assert_style( + """ + with :ok <- foo, do: :bar, else: (_ -> :baz) + """, + """ + case foo do + :ok -> :bar + _ -> :baz + end + """ + ) + end end test "Credo.Check.Refactor.CondStatements" do From a46c43f3739f1796958c2054eb49879726e4de9b Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 12:21:33 -0700 Subject: [PATCH 64/86] pipify nested function calls with pipe as the first argument. closes #216 --- CHANGELOG.md | 1 + lib/style/pipes.ex | 11 ++++++----- test/style/pipes_test.exs | 5 ++--- test/support/style_case.ex | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7468937b..eb872701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ This release taught Styler to try just that little bit harder when doing alias l ### Fixes - `pipes`: handle pipifying when the first arg is itself a pipe: `c(a |> b, d)` => `a |> b() |> c(d)` (#214, h/t @kybishop) +- `pipes`: handle pipifying nested functions `d(c(a |> b))` => `a |> b |> c() |> d` (#216, h/t @emkguts) - `with`: correctly handle a stabby `with` `, else: (_ -> :ok)` being rewritten to a case (#219, h/t @iamhassangm) ## 1.3.3 diff --git a/lib/style/pipes.ex b/lib/style/pipes.ex index bffb5e1f..dc56ad27 100644 --- a/lib/style/pipes.ex +++ b/lib/style/pipes.ex @@ -132,7 +132,7 @@ defmodule Styler.Style.Pipes do # a(b |> c[, ...args]) # The first argument to a function-looking node is a pipe. - # Maybe pipe the whole thing? + # Maybe pipify the whole thing? def run({{f, m, [{:|>, _, _} = pipe | args]}, _} = zipper, ctx) do parent = case Zipper.up(zipper) do @@ -168,10 +168,11 @@ defmodule Styler.Style.Pipes do {:cont, zipper, ctx} true -> - # Recurse in case this is a multi pipe - zipper - |> Zipper.replace({:|>, m, [pipe, {f, m, args}]}) - |> run(ctx) + zipper = Zipper.replace(zipper, {:|>, m, [pipe, {f, m, args}]}) + # it's possible this is a nested function call `c(b(a |> b))`, so we should walk up the tree for de-nesting + zipper = Zipper.up(zipper) || zipper + # recursion ensures we get those nested function calls and any additional pipes + run(zipper, ctx) end end diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index f87b3a36..c5617952 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -943,9 +943,8 @@ defmodule Styler.Style.PipesTest do end test "pipifying" do - assert_style "d(a |> b |> c)", "a |> b() |> c() |> d()" - assert_style("c(a |> b, d)", "a |> b() |> c(d)") - + assert_style("e(d(a |> b |> c), f)", "a |> b() |> c() |> d() |> e(f)") + assert_style( """ # d diff --git a/test/support/style_case.ex b/test/support/style_case.ex index a7425772..66e34c65 100644 --- a/test/support/style_case.ex +++ b/test/support/style_case.ex @@ -82,6 +82,7 @@ defmodule Styler.StyleCase do _ -> false end + # This isn't enabled in any test, but can be a useful audit if @ordered_siblings do case Zipper.left(zipper) do {{_, prev_meta, _} = prev, _} -> From ceb827abc249c347c05d7132fa5e29443b37c279 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 12:43:48 -0700 Subject: [PATCH 65/86] change struct update deprecation to ex1.19+ --- CHANGELOG.md | 43 ++++++++++++++++++++------------ lib/style/deprecations.ex | 2 +- test/style/deprecations_test.exs | 4 +-- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb872701..86534753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,25 +5,16 @@ they can and will change without that change being reflected in Styler's semanti ## main -### Improvements - -#### Ex1.17+ - -- Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` -- Handle `, else: (_ -> x)` bugs introduced by `(_ -> x)` being termed a literal (#219, h/t @iamhassangm) +## 1.4 -#### Ex1.18+ +- A very nice change in alias lifting means Styler will make sure that your code is _using_ the aliases that it's specified. +- Shoutout to the smartrent folks for finding pipifying recursion issues +- Elixir 1.17 improvements and fixes +- Elixir 1.19-dev: delete struct updates -Delete deprecated struct update syntax in favor of map update syntax. +Read on for details. -```elixir -# This -%Struct{x | y} -# Styles to this -%{x | y} -``` - -**WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.18's type checking features. (#199, h/t @SteffenDE) +### Improvements #### Alias Lifting @@ -46,6 +37,26 @@ This release taught Styler to try just that little bit harder when doing alias l C.bar() C.baz() +#### Ex1.17+ + +- Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` +- Handle `, else: (_ -> x)` bugs introduced by `(_ -> x)` being termed a literal (#219, h/t @iamhassangm) + +#### Ex1.19+ (experimental) + +1.19 deprecates struct update syntax in favor of map update syntax. Styler will do this update for you if you're on Elixir 1.19.0-dev or later. + +```elixir +# This +%Struct{x | y} +# Styles to this +%{x | y} +``` + +**WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.18's type checking features. + +A future version of Styler may be smart enough to do this check for you and perform the appropriate updates to the assignment location; no guarantees though. Track via #199, h/t @SteffenDE + ### Fixes - `pipes`: handle pipifying when the first arg is itself a pipe: `c(a |> b, d)` => `a |> b() |> c(d)` (#214, h/t @kybishop) diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex index 93baee08..2cbdf115 100644 --- a/lib/style/deprecations.ex +++ b/lib/style/deprecations.ex @@ -65,7 +65,7 @@ defmodule Styler.Style.Deprecations do end end - if Version.match?(System.version(), ">= 1.18.0-dev") do + if Version.match?(System.version(), ">= 1.19.0-dev") do # Struct update syntax was deprecated `%Foo{x | y} => %{x | y}` defp style({:%, _, [_struct, {:%{}, _, [{:|, _, _}]} = update]}), do: update end diff --git a/test/style/deprecations_test.exs b/test/style/deprecations_test.exs index 140a87c4..041110d5 100644 --- a/test/style/deprecations_test.exs +++ b/test/style/deprecations_test.exs @@ -146,8 +146,8 @@ defmodule Styler.Style.DeprecationsTest do end end - describe "1.18+" do - @describetag skip: Version.match?(System.version(), "< 1.18.0-dev") + describe "1.19+" do + @describetag skip: Version.match?(System.version(), "< 1.19.0-dev") test "struct update" do assert_style "%Foo{widget | bar: :baz}", "%{widget | bar: :baz}" From d015a990bb9b0018f3c5ef64752ea9e825a1f693 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Sat, 25 Jan 2025 08:52:53 -0700 Subject: [PATCH 66/86] docs docs docs docs docs! --- README.md | 79 ++++------------------------- docs/comment_directives.md | 101 +++++++++++++++++++++++++++++++++++++ docs/deprecations.md | 23 +++++++++ docs/module_directives.md | 14 +++++ mix.exs | 1 + 5 files changed, 149 insertions(+), 69 deletions(-) create mode 100644 docs/comment_directives.md diff --git a/README.md b/README.md index 6042da5c..647f73b3 100644 --- a/README.md +++ b/README.md @@ -11,77 +11,18 @@ You can learn more about the history, purpose and implementation of Styler from ## Features -### AST Rewrites as part of `mix format` +Styler fixes a plethora of elixir style and optimization issues automatically as part of mix format. -[See our Rewrites documentation on hexdocs](https://hexdocs.pm/styler/styles.html) -Styler fixes a plethora of elixir style and optimization issues automatically as part of `mix format`. In addition to automating corrections for [many credo rules](docs/credo.md) (meaning you can turn them off to speed credo up), Styler: +[See Styler's documentation on Hex](https://hexdocs.pm/styler/styles.html) for the comprehensive list of its features. -- [keeps a strict module layout](docs/module_directives.md#directive-organization) - - alphabetizes module directives -- [extracts repeated aliases](docs/module_directives.md#alias-lifting) -- [makes your pipe chains pretty as can be](docs/pipes.md) - - pipes and unpipes function calls based on the number of calls - - optimizes standard library calls (`a |> Enum.map(m) |> Enum.into(Map.new)` => `Map.new(a, m)`) -- replaces strings with sigils when the string has many escaped quotes -- ... and so much more +The fastest way to see what all it can do you for you is to just try it out in your codebase... but here's a list of a few features to help you decide if you're interested in Styler: -### Maintain static list order via `# styler:sort` - -Styler can keep static values sorted for your team as part of its formatting pass. To instruct it to do so, replace any `# Please keep this list sorted!` notes you wrote to your teammates with `# styler:sort`. - -#### Examples - -```elixir -# styler:sort -[:c, :a, :b] - -# styler:sort -~w(a list of words) - -# styler:sort -@country_codes ~w( - en_US - po_PO - fr_CA - ja_JP -) - -# styler:sort -a_var = - [ - Modules, - In, - A, - List - ] -``` - -Would yield: - -```elixir -# styler:sort -[:a, :b, :c] - -# styler:sort -~w(a list of words) - -# styler:sort -@country_codes ~w( - en_US - fr_CA - ja_JP - po_PO -) - -# styler:sort -a_var = - [ - A, - In, - List, - Modules - ] -``` +- sorts and organizes `import`/`alias`/`require` and other [module directives](docs/module_directives.md) +- keeps lists, sigils, and even arbitrary code sorted with the `# styler:sort` [comment directive](docs/comment_directives.md) +- automatically creates aliases for repeatedly referenced modules names ([_"alias lifting"_](docs/module_directives.md#alias-lifting)) +- optimizes pipe chains for [readability and performance](docs/pipes.md) +- rewrites strings as sigils when it results in fewer escapes +- auto-fixes [many credo rules](docs/credo.md), meaning you can spend less time fighting with CI ## Who is Styler for? @@ -101,7 +42,7 @@ Add `:styler` as a dependency to your project's `mix.exs`: ```elixir def deps do [ - {:styler, "~> 1.2", only: [:dev, :test], runtime: false}, + {:styler, "~> 1.4", only: [:dev, :test], runtime: false}, ] end ``` diff --git a/docs/comment_directives.md b/docs/comment_directives.md new file mode 100644 index 00000000..41dc0a63 --- /dev/null +++ b/docs/comment_directives.md @@ -0,0 +1,101 @@ +## Comment Directives + +Comment Directives are a Styler feature that let you instruct Styler to do maintain additional formatting via comments. + +The plural in the name is optimistic as there's currently only one, but who knows + +### `# styler:sort` + +Styler can keep static values sorted for your team as part of its formatting pass. To instruct it to do so, replace any `# Please keep this list sorted!` notes you wrote to your teammates with `# styler:sort` + +Sorting is done via string comparison of the code. + +Styler knows how to sort the following things: + +- lists of elements +- arbitrary code within `do end` blocks (helpful for schema-like macros) +- `~w` sigils elements +- keyword shapes (structs, maps, and keywords) + +Since you can't have comments in arbitrary places when using Elixir's formatter, +Styler will apply those sorts when they're on the righthand side fo the following operators: + +- module directives (eg `@my_dir ~w(a list of things)`) +- assignments (eg `x = ~w(a list again)`) +- `defstruct` + +#### Examples + +**Before** + +```elixir +# styler:sort +[:c, :a, :b] + +# styler:sort +~w(a list of words) + +# styler:sort +@country_codes ~w( + en_US + po_PO + fr_CA + ja_JP +) + +# styler:sort +a_var = + [ + Modules, + In, + A, + List + ] + +# styler:sort +my_macro "some arg" do + another_macro :q + another_macro :w + another_macro :e + another_macro :r + another_macro :t + another_macro :y +end +``` + +**After** + +```elixir +# styler:sort +[:a, :b, :c] + +# styler:sort +~w(a list of words) + +# styler:sort +@country_codes ~w( + en_US + fr_CA + ja_JP + po_PO +) + +# styler:sort +a_var = + [ + A, + In, + List, + Modules + ] + +# styler:sort +my_macro "some arg" do + another_macro :e + another_macro :q + another_macro :r + another_macro :t + another_macro :w + another_macro :y +end +``` diff --git a/docs/deprecations.md b/docs/deprecations.md index bbc7c190..976b9327 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -25,10 +25,33 @@ This is covered by the Elixir Formatter with the `--migrate` flag, but Styler br Rewrite `unless x` to `if !x` +### 1.19 + +#### Change Struct Updates to Map Updates (Experimental) + +1.19 deprecates struct update syntax in favor of map update syntax. Styler will do this update for you if you're on Elixir 1.19.0-dev or later. + +```elixir +# This +%Struct{x | y} +# Styles to this +%{x | y} +``` + +**WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.18's type checking features. + +A future version of Styler may be smart enough to do this check for you and perform the appropriate updates to the assignment location; no guarantees though. Track via #199, h/t @SteffenDE + +### 1.18 + +None? + ### 1.17 [1.17 Deprecations](https://hexdocs.pm/elixir/1.17.0/changelog.html#4-hard-deprecations) +- Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` + #### Range Matching Without Step ```elixir diff --git a/docs/module_directives.md b/docs/module_directives.md index ff3cc8cb..4bfcd590 100644 --- a/docs/module_directives.md +++ b/docs/module_directives.md @@ -162,6 +162,20 @@ C.foo() C.bar() ``` +Styler also notices when you have a module aliased and aren't employing that alias and will do the updates for you. + +```elixir +# Given +alias My.Apps.Widget + +x = Repo.get(My.Apps.Widget, id) + +# Styled +alias My.Apps.Widget + +x = Repo.get(Widget, id) +``` + ### Collisions Styler won't lift aliases that will collide with existing aliases, and likewise won't lift any module whose name would collide with a standard library name. diff --git a/mix.exs b/mix.exs index 8d3df8e5..c8426a74 100644 --- a/mix.exs +++ b/mix.exs @@ -70,6 +70,7 @@ defmodule Styler.MixProject do "docs/control_flow_macros.md": [title: "Control Flow Macros (if, case, ...)"], "docs/mix_configs.md": [title: "Mix Configs (config/*.exs)"], "docs/module_directives.md": [title: "Module Directives (use, alias, ...)"], + "docs/comment_directives.md": [title: "Comment Directives (# styler:sort)"], "docs/credo.md": [title: "Styler & Credo"], "README.md": [title: "Styler"] ] From 6896d97a1820fe66d9d6264d5376f739bb16206c Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 13:28:08 -0700 Subject: [PATCH 67/86] ship struct update to map update changes after all --- CHANGELOG.md | 17 +++++++---------- docs/deprecations.md | 10 +++------- lib/style/deprecations.ex | 7 +++---- test/style/deprecations_test.exs | 12 ++++-------- 4 files changed, 17 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86534753..59b3a788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,14 +37,9 @@ This release taught Styler to try just that little bit harder when doing alias l C.bar() C.baz() -#### Ex1.17+ - -- Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` -- Handle `, else: (_ -> x)` bugs introduced by `(_ -> x)` being termed a literal (#219, h/t @iamhassangm) - -#### Ex1.19+ (experimental) +#### Struct Updates => Map Updates -1.19 deprecates struct update syntax in favor of map update syntax. Styler will do this update for you if you're on Elixir 1.19.0-dev or later. +1.19 deprecates struct update syntax in favor of map update syntax. ```elixir # This @@ -53,15 +48,17 @@ This release taught Styler to try just that little bit harder when doing alias l %{x | y} ``` -**WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.18's type checking features. +**WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.19's type checking features. Apologies to folks who hoped Styler would do this step for you <3 (#199, h/t @SteffenDE) + +#### Ex1.17+ -A future version of Styler may be smart enough to do this check for you and perform the appropriate updates to the assignment location; no guarantees though. Track via #199, h/t @SteffenDE +- Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` (This style is only applied if you're on 1.17+) ### Fixes - `pipes`: handle pipifying when the first arg is itself a pipe: `c(a |> b, d)` => `a |> b() |> c(d)` (#214, h/t @kybishop) - `pipes`: handle pipifying nested functions `d(c(a |> b))` => `a |> b |> c() |> d` (#216, h/t @emkguts) -- `with`: correctly handle a stabby `with` `, else: (_ -> :ok)` being rewritten to a case (#219, h/t @iamhassangm) +- `with`: fix a stabby `with` `, else: (_ -> :ok)` being rewritten to a case (#219, h/t @iamhassangm) ## 1.3.3 diff --git a/docs/deprecations.md b/docs/deprecations.md index 976b9327..45bdbe24 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -25,11 +25,9 @@ This is covered by the Elixir Formatter with the `--migrate` flag, but Styler br Rewrite `unless x` to `if !x` -### 1.19 +### Change Struct Updates to Map Updates -#### Change Struct Updates to Map Updates (Experimental) - -1.19 deprecates struct update syntax in favor of map update syntax. Styler will do this update for you if you're on Elixir 1.19.0-dev or later. +1.19 deprecates struct update syntax in favor of map update syntax. ```elixir # This @@ -38,9 +36,7 @@ Rewrite `unless x` to `if !x` %{x | y} ``` -**WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.18's type checking features. - -A future version of Styler may be smart enough to do this check for you and perform the appropriate updates to the assignment location; no guarantees though. Track via #199, h/t @SteffenDE +**WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.19's type checking features. ### 1.18 diff --git a/lib/style/deprecations.ex b/lib/style/deprecations.ex index 2cbdf115..824a88e6 100644 --- a/lib/style/deprecations.ex +++ b/lib/style/deprecations.ex @@ -65,10 +65,9 @@ defmodule Styler.Style.Deprecations do end end - if Version.match?(System.version(), ">= 1.19.0-dev") do - # Struct update syntax was deprecated `%Foo{x | y} => %{x | y}` - defp style({:%, _, [_struct, {:%{}, _, [{:|, _, _}]} = update]}), do: update - end + # Struct update syntax is deprecated in 1.19 + # `%Foo{x | y} => %{x | y}` + defp style({:%, _, [_struct, {:%{}, _, [{:|, _, _}]} = update]}), do: update # For ranges where `start > stop`, you need to explicitly include the step # Enum.slice(enumerable, 1..-2) => Enum.slice(enumerable, 1..-2//1) diff --git a/test/style/deprecations_test.exs b/test/style/deprecations_test.exs index 041110d5..d0633f91 100644 --- a/test/style/deprecations_test.exs +++ b/test/style/deprecations_test.exs @@ -116,6 +116,10 @@ defmodule Styler.Style.DeprecationsTest do end end + test "struct update, deprecated in 1.19" do + assert_style "%Foo{widget | bar: :baz}", "%{widget | bar: :baz}" + end + describe "1.16+" do @describetag skip: Version.match?(System.version(), "< 1.16.0-dev") @@ -145,12 +149,4 @@ defmodule Styler.Style.DeprecationsTest do assert_style "a |> x() |> :timer.seconds()" end end - - describe "1.19+" do - @describetag skip: Version.match?(System.version(), "< 1.19.0-dev") - - test "struct update" do - assert_style "%Foo{widget | bar: :baz}", "%{widget | bar: :baz}" - end - end end From aaedd0c9e8c11c55b0ee83e31a2eaab97ae94923 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 20 Feb 2025 13:32:46 -0700 Subject: [PATCH 68/86] v1.4.0 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index c8426a74..344cd357 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.3.3" + @version "1.4.0" @url "https://github.com/adobe/elixir-styler" def project do From 3d5a485846b2c5497c2133e4b35ea8b7222945dd Mon Sep 17 00:00:00 2001 From: Fabian Becker Date: Fri, 21 Feb 2025 09:50:28 +0100 Subject: [PATCH 69/86] Add OTP26/27 but only run for 1.17/1.18 --- .github/workflows/ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ab4bb2c..a2391e35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,13 @@ jobs: name: Ex${{matrix.elixir}}/OTP${{matrix.otp}} strategy: matrix: - elixir: ['1.15.8', '1.16.3', '1.17.3', '1.18.2'] - otp: ['25.3.2'] + elixir: ["1.15.8", "1.16.3", "1.17.3", "1.18.2"] + otp: ["25.3.2", "26.2.5", "27.2.4"] + exclude: + - elixir: "1.15.8" + otp: "27.2.4" + - elixir: "1.16.3" + otp: "27.2.4" steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 From 941d06728b5c2ffab19e05ee84b52a47294cf397 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Fri, 21 Feb 2025 08:15:12 -0700 Subject: [PATCH 70/86] fix `with` redundant body + non-arrow behind redundant clause. Closes #221 --- lib/style/blocks.ex | 46 ++++++++++++++++++++------------------ test/style/blocks_test.exs | 2 ++ test/style/pipes_test.exs | 2 +- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index fd51be13..7a74378c 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -187,35 +187,37 @@ defmodule Styler.Style.Blocks do # drop singleton identity else clauses like `else foo -> foo end` elses = - case elses do - [{{_, _, [:else]}, [{:->, _, [[left], right]}]}] -> if nodes_equivalent?(left, right), do: [], else: elses - _ -> elses + with [{{_, _, [:else]}, [{:->, _, [[left], right]}]}] <- elses, + true <- nodes_equivalent?(left, right), + do: [], + else: (_ -> elses) + + # Remove Redundant body + {postroll, reversed_clauses, do_body} = + if Enum.empty?(postroll) and Enum.empty?(elses) and nodes_equivalent?(lhs, do_body) do + # removing redundant RHS can expose more non-arrows behind it, so repeat our postroll process + {postroll, reversed_clauses} = Enum.split_while(rest, &(not left_arrow?(&1))) + {postroll, reversed_clauses, rhs} + else + {postroll, reversed_clauses, do_body} end + # Put the postroll into the body {reversed_clauses, do_body} = - cond do - # Put the postroll into the body - Enum.any?(postroll) -> - {node, do_body_meta, do_children} = do_body - do_children = if node == :__block__, do: do_children, else: [do_body] - do_body = {:__block__, Keyword.take(do_body_meta, [:line]), Enum.reverse(postroll, do_children)} - {reversed_clauses, do_body} - - # Credo.Check.Refactor.RedundantWithClauseResult - Enum.empty?(elses) and nodes_equivalent?(lhs, do_body) -> - {rest, rhs} - - # no change - true -> - {reversed_clauses, do_body} + if Enum.any?(postroll) do + {node, do_body_meta, do_children} = do_body + do_children = if node == :__block__, do: do_children, else: [do_body] + do_body = {:__block__, Keyword.take(do_body_meta, [:line]), Enum.reverse(postroll, do_children)} + {reversed_clauses, do_body} + else + {reversed_clauses, do_body} end - do_line = do_meta[:line] final_clause_line = final_clause_meta[:line] do_line = cond do - do_meta[:format] == :keyword && final_clause_line + 1 >= do_line -> do_line + do_meta[:format] == :keyword && final_clause_line + 1 >= do_meta[:line] -> do_meta[:line] do_meta[:format] == :keyword -> final_clause_line + 1 true -> final_clause_line end @@ -243,12 +245,12 @@ defmodule Styler.Style.Blocks do |> Zipper.prepend_siblings(preroll) |> run(ctx) - # the # of `<-` canged, so we should have another look at this with statement + # the # of `<-` changed, so we should have another look at this with statement Enum.any?(postroll) -> run(zipper, ctx) true -> - # of clauess didn't change, so don't reecurse or we'll loop FOREEEVEERR + # of clauses didn't change, so don't reecurse or we'll loop FOREEEVEERR {:cont, zipper, ctx} end end diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index cf2d1644..e8f4626e 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -727,6 +727,7 @@ defmodule Styler.Style.BlocksTest do assert_style( """ with {:ok, a} <- foo(), + x = y, {:ok, b} <- bar(a) do {:ok, b} else @@ -735,6 +736,7 @@ defmodule Styler.Style.BlocksTest do """, """ with {:ok, a} <- foo() do + x = y bar(a) end """ diff --git a/test/style/pipes_test.exs b/test/style/pipes_test.exs index c5617952..50284537 100644 --- a/test/style/pipes_test.exs +++ b/test/style/pipes_test.exs @@ -944,7 +944,7 @@ defmodule Styler.Style.PipesTest do test "pipifying" do assert_style("e(d(a |> b |> c), f)", "a |> b() |> c() |> d() |> e(f)") - + assert_style( """ # d From 9990eb6682cb5100f73b9ede848ff69d6a6a6c5d Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 24 Feb 2025 13:32:43 -0700 Subject: [PATCH 71/86] link to quokka --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 647f73b3..78a1ed84 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,9 @@ Styler's only current configuration option is `:alias_lifting_exclude`, which ac #### No Credo-Style Enable/Disable -Styler [will not add configuration](https://github.com/adobe/elixir-styler/pull/127#issuecomment-1912242143) for ad-hoc enabling/disabling of rewrites. Sorry! Its implementation simply does not support that approach. There are however many forks out there that have attempted this; please explore the [Github forks tab](https://github.com/adobe/elixir-styler/forks) to see if there's a project that suits your needs or that you can draw inspiration from. +Styler [will not add configuration](https://github.com/adobe/elixir-styler/pull/127#issuecomment-1912242143) for ad-hoc enabling/disabling of rewrites. Sorry! + +However, Smartrent has a fork of Styler named [Quokka](https://github.com/smartrent/quokka) that allows for finegrained control of Styler. Maybe it's what you're looking for. If not, you can always fork it or Styler as a starting point for your own tool! Ultimately Styler is @adobe's internal tool that we're happy to share with the world. We're delighted if you like it as is, and just as excited if it's a starting point for you to make something even better for yourself. From 3b0571e110c8b9e91c3649c901e3e780709ac337 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 3 Mar 2025 11:13:25 -0700 Subject: [PATCH 72/86] rewrite `to_timeout(unit: x * m)` to use larger units in some cases --- CHANGELOG.md | 13 ++++++++++ lib/style/single_node.ex | 43 +++++++++++++++++++++++++++++++++ lib/styler.ex | 2 +- test/style/single_node_test.exs | 30 +++++++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b3a788..25503ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ they can and will change without that change being reflected in Styler's semanti ## main +### Improvements + +- `to_timeout/1` use the next largest unit in some simple instances + + ```elixir + # before + to_timeout(second: 60 * m) + to_timeout(day: 7) + # after + to_timeout(minute: m) + to_timeout(week: 1) + ``` + ## 1.4 - A very nice change in alias lifting means Styler will make sure that your code is _using_ the aliases that it's specified. diff --git a/lib/style/single_node.ex b/lib/style/single_node.ex index 3d159b43..9ccb7329 100644 --- a/lib/style/single_node.ex +++ b/lib/style/single_node.ex @@ -186,6 +186,49 @@ defmodule Styler.Style.SingleNode do defp style({:case, cm, [head, [{do_, arrows}]]}), do: {:case, cm, [head, [{do_, rewrite_arrows(arrows)}]]} defp style({:fn, m, arrows}), do: {:fn, m, rewrite_arrows(arrows)} + defp style({:to_timeout, meta, [[{{:__block__, um, [unit]}, {:*, _, [left, right]}}]]} = node) + when unit in ~w(day hour minute second millisecond)a do + [l, r] = + Enum.map([left, right], fn + {_, _, [x]} -> x + _ -> nil + end) + + {step, next_unit} = + case unit do + :day -> {7, :week} + :hour -> {24, :day} + :minute -> {60, :hour} + :second -> {60, :minute} + :millisecond -> {1000, :second} + end + + if step in [l, r] do + n = if l == step, do: right, else: left + style({:to_timeout, meta, [[{{:__block__, um, [next_unit]}, n}]]}) + else + node + end + end + + defp style({:to_timeout, meta, [[{{:__block__, um, [unit]}, {:__block__, tm, [n]}}]]} = node) do + step_up = + case {unit, n} do + {:day, 7} -> :week + {:hour, 24} -> :day + {:minute, 60} -> :hour + {:second, 60} -> :minute + {:millisecond, 1000} -> :second + _ -> nil + end + + if step_up do + {:to_timeout, meta, [[{{:__block__, um, [step_up]}, {:__block__, [token: "1", line: tm[:line]], [1]}}]]} + else + node + end + end + defp style(node), do: node defp replace_into({:., dm, [{_, am, _} = enum, _]}, collectable, rest) do diff --git a/lib/styler.ex b/lib/styler.ex index dbb3992e..e8c8c749 100644 --- a/lib/styler.ex +++ b/lib/styler.ex @@ -21,10 +21,10 @@ defmodule Styler do @styles [ Styler.Style.ModuleDirectives, Styler.Style.Pipes, + Styler.Style.Deprecations, Styler.Style.SingleNode, Styler.Style.Defs, Styler.Style.Blocks, - Styler.Style.Deprecations, Styler.Style.Configs, Styler.Style.CommentDirectives ] diff --git a/test/style/single_node_test.exs b/test/style/single_node_test.exs index f8e40539..46de5f94 100644 --- a/test/style/single_node_test.exs +++ b/test/style/single_node_test.exs @@ -341,4 +341,34 @@ defmodule Styler.Style.SingleNodeTest do assert_style("Enum.reverse(foo, bar) ++ bar") end end + + describe "to_timeout" do + test "to next unit" do + facts = [ + {1000, :millisecond, :second}, + {60, :second, :minute}, + {60, :minute, :hour}, + {24, :hour, :day}, + {7, :day, :week} + ] + + for {n, unit, next} <- facts do + assert_style "to_timeout(#{unit}: #{n} * m)", "to_timeout(#{next}: m)" + assert_style "to_timeout(#{unit}: m * #{n})", "to_timeout(#{next}: m)" + assert_style "to_timeout(#{unit}: #{n})", "to_timeout(#{next}: 1)" + end + + assert_style "to_timeout(second: 60 * 60)", "to_timeout(hour: 1)" + end + + test "combined with :timer.x deprecation rewrite" do + assert_style ":timer.minutes(60 * 4)", "to_timeout(hour: 4)" + end + + test "doesnt mess with" do + assert_style "to_timeout(hour: n * m)" + assert_style "to_timeout(whatever)" + assert_style "to_timeout(hour: 24 * 1, second: 60 * 4)" + end + end end From 8fe1ca0efbb224de6be9e31318e2c16230c638c4 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 3 Mar 2025 11:14:30 -0700 Subject: [PATCH 73/86] defs test describe formatting --- test/style/defs_test.exs | 408 +++++++++++++++++++-------------------- 1 file changed, 203 insertions(+), 205 deletions(-) diff --git a/test/style/defs_test.exs b/test/style/defs_test.exs index d80f5871..808d80fe 100644 --- a/test/style/defs_test.exs +++ b/test/style/defs_test.exs @@ -11,232 +11,230 @@ defmodule Styler.Style.DefsTest do use Styler.StyleCase, async: true - describe "run" do - test "comments stay put when we can't shrink the head, even with blocks" do - assert_style(""" - def my_function( - so_long_that_this_head_will_not_fit_on_one_lineso_long_that_this_head_will_not_fit_on_one_line, - so_long_that_this_head_will_not_fit_on_one_line - ) do - result = - case foo do - :bar -> :baz - :baz -> :bong - end - - # My comment - Context.process(result) - end - """) - end + test "comments stay put when we can't shrink the head, even with blocks" do + assert_style(""" + def my_function( + so_long_that_this_head_will_not_fit_on_one_lineso_long_that_this_head_will_not_fit_on_one_line, + so_long_that_this_head_will_not_fit_on_one_line + ) do + result = + case foo do + :bar -> :baz + :baz -> :bong + end - test "function with do keyword" do - assert_style( - """ - # Top comment - def save( - # Socket comment - %Socket{assigns: %{user: user, live_action: :new}} = initial_socket, - # Params comment - params - ), - do: :ok - """, - """ - # Top comment - # Socket comment - # Params comment - def save(%Socket{assigns: %{user: user, live_action: :new}} = initial_socket, params), do: :ok - """ - ) + # My comment + Context.process(result) end + """) + end - test "bodyless function with spec" do - assert_style(""" - @spec original_object(atom()) :: atom() - def original_object(object) - """) - end + test "function with do keyword" do + assert_style( + """ + # Top comment + def save( + # Socket comment + %Socket{assigns: %{user: user, live_action: :new}} = initial_socket, + # Params comment + params + ), + do: :ok + """, + """ + # Top comment + # Socket comment + # Params comment + def save(%Socket{assigns: %{user: user, live_action: :new}} = initial_socket, params), do: :ok + """ + ) + end - test "block function body doesn't get newlined" do - assert_style(""" - # Here's a comment - def some_function(%{id: id, type: type, processed_at: processed_at} = file, params, _) - when type == :file and is_nil(processed_at) do - with {:ok, results} <- FileProcessor.process(file) do - # This comment could make sense - {:ok, post_process_the_results_somehow(results)} - end + test "bodyless function with spec" do + assert_style(""" + @spec original_object(atom()) :: atom() + def original_object(object) + """) + end + + test "block function body doesn't get newlined" do + assert_style(""" + # Here's a comment + def some_function(%{id: id, type: type, processed_at: processed_at} = file, params, _) + when type == :file and is_nil(processed_at) do + with {:ok, results} <- FileProcessor.process(file) do + # This comment could make sense + {:ok, post_process_the_results_somehow(results)} end - """) end + """) + end - test "kwl function body doesn't get newlined" do - assert_style(""" - def is_expired_timestamp?(timestamp) when is_integer(timestamp), - do: Timex.from_unix(timestamp, :second) <= Timex.shift(DateTime.utc_now(), minutes: 1) - """) - end + test "kwl function body doesn't get newlined" do + assert_style(""" + def is_expired_timestamp?(timestamp) when is_integer(timestamp), + do: Timex.from_unix(timestamp, :second) <= Timex.shift(DateTime.utc_now(), minutes: 1) + """) + end - test "function with do block" do - assert_style( - """ - def save( - %Socket{assigns: %{user: user, live_action: :new}} = initial_socket, - params # Comments in the darndest places - ) do - :ok - end - """, - """ - # Comments in the darndest places - def save(%Socket{assigns: %{user: user, live_action: :new}} = initial_socket, params) do - :ok - end - """ - ) - end + test "function with do block" do + assert_style( + """ + def save( + %Socket{assigns: %{user: user, live_action: :new}} = initial_socket, + params # Comments in the darndest places + ) do + :ok + end + """, + """ + # Comments in the darndest places + def save(%Socket{assigns: %{user: user, live_action: :new}} = initial_socket, params) do + :ok + end + """ + ) + end - test "no body" do - assert_style "def no_body_nor_parens_yikes!" - - assert_style( - """ - # Top comment - def no_body( - foo, # This is a foo - bar # This is a bar - ) - - # Another comment for this head - def no_body(nil, _), do: nil - """, - """ - # Top comment - # This is a foo - # This is a bar - def no_body(foo, bar) - - # Another comment for this head - def no_body(nil, _), do: nil - """ - ) - end + test "no body" do + assert_style "def no_body_nor_parens_yikes!" - test "when clause w kwl do" do - assert_style( - """ - def foo(%{ - bar: baz - }) - # Self-documenting code! - when baz in [ - :a, # Obviously, this is a - :b # ... and this is b - ], - do: :never_write_code_like_this - """, - """ - # Self-documenting code! - # Obviously, this is a - # ... and this is b - def foo(%{bar: baz}) when baz in [:a, :b], do: :never_write_code_like_this - """ + assert_style( + """ + # Top comment + def no_body( + foo, # This is a foo + bar # This is a bar ) - end - test "keyword do with a list" do - assert_style( - """ - def foo, - do: [ - # Weirdo comment - :never_write_code_like_this - ] - """, - """ - # Weirdo comment - def foo, do: [:never_write_code_like_this] - """ - ) - end + # Another comment for this head + def no_body(nil, _), do: nil + """, + """ + # Top comment + # This is a foo + # This is a bar + def no_body(foo, bar) + + # Another comment for this head + def no_body(nil, _), do: nil + """ + ) + end + + test "when clause w kwl do" do + assert_style( + """ + def foo(%{ + bar: baz + }) + # Self-documenting code! + when baz in [ + :a, # Obviously, this is a + :b # ... and this is b + ], + do: :never_write_code_like_this + """, + """ + # Self-documenting code! + # Obviously, this is a + # ... and this is b + def foo(%{bar: baz}) when baz in [:a, :b], do: :never_write_code_like_this + """ + ) + end - test "rewrites subsequent definitions" do - assert_style( - """ - def foo(), do: :ok + test "keyword do with a list" do + assert_style( + """ + def foo, + do: [ + # Weirdo comment + :never_write_code_like_this + ] + """, + """ + # Weirdo comment + def foo, do: [:never_write_code_like_this] + """ + ) + end - def foo( - too, - # Long long is too long - long - ), do: :ok - """, - """ - def foo, do: :ok + test "rewrites subsequent definitions" do + assert_style( + """ + def foo(), do: :ok + def foo( + too, # Long long is too long - def foo(too, long), do: :ok - """ - ) - end + long + ), do: :ok + """, + """ + def foo, do: :ok + + # Long long is too long + def foo(too, long), do: :ok + """ + ) + end - test "when clause with block do" do - assert_style( - """ - # Foo takes a bar - def foo(%{ - bar: baz - }) - # Baz should be either :a or :b - when baz in [ - :a, - :b - ] - do # Weird place for a comment - # Above the body - :never_write_code_like_this - # Below the body - end - """, - """ - # Foo takes a bar + test "when clause with block do" do + assert_style( + """ + # Foo takes a bar + def foo(%{ + bar: baz + }) # Baz should be either :a or :b - # Weird place for a comment - def foo(%{bar: baz}) when baz in [:a, :b] do - # Above the body - :never_write_code_like_this - # Below the body - end - """ - ) - end + when baz in [ + :a, + :b + ] + do # Weird place for a comment + # Above the body + :never_write_code_like_this + # Below the body + end + """, + """ + # Foo takes a bar + # Baz should be either :a or :b + # Weird place for a comment + def foo(%{bar: baz}) when baz in [:a, :b] do + # Above the body + :never_write_code_like_this + # Below the body + end + """ + ) + end - test "Doesn't move stuff around if it would make the line too long" do - assert_style(""" - @doc "this is a doc" - # And also a comment - def wow_this_function_name_is_super_long(it_also, has_a, ton_of, arguments), - do: "this is going to end up making the line too long if we inline it" - - @doc "this is another function" - # And it also has a comment - def this_one_fits_on_one_line, do: :ok - """) - end + test "Doesn't move stuff around if it would make the line too long" do + assert_style(""" + @doc "this is a doc" + # And also a comment + def wow_this_function_name_is_super_long(it_also, has_a, ton_of, arguments), + do: "this is going to end up making the line too long if we inline it" + + @doc "this is another function" + # And it also has a comment + def this_one_fits_on_one_line, do: :ok + """) + end - test "Doesn't collapse pipe chains in a def do ... end" do - assert_style(""" - def foo(some_list) do - some_list - |> Enum.reject(&is_nil/1) - |> Enum.map(&transform/1) - end - """) + test "Doesn't collapse pipe chains in a def do ... end" do + assert_style(""" + def foo(some_list) do + some_list + |> Enum.reject(&is_nil/1) + |> Enum.map(&transform/1) end + """) + end - test "regression: @def module attribute" do - assert_style("@def ~s(this should be okay)") - end + test "regression: @def module attribute" do + assert_style("@def ~s(this should be okay)") end end From 13320e95d029416b3df646e69e29020f318b1c0f Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Mon, 3 Mar 2025 11:19:28 -0700 Subject: [PATCH 74/86] dont crash on invalid defs --- CHANGELOG.md | 4 ++++ lib/style/defs.ex | 30 +++++++++++------------------- test/style/defs_test.exs | 12 ++++++++++-- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25503ace..063917a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ they can and will change without that change being reflected in Styler's semanti to_timeout(week: 1) ``` +### Fixes + +- fixed styler raising when encountering invalid function definition ast + ## 1.4 - A very nice change in alias lifting means Styler will make sure that your code is _using_ the aliases that it's specified. diff --git a/lib/style/defs.ex b/lib/style/defs.ex index f1670fd5..8a6243f5 100644 --- a/lib/style/defs.ex +++ b/lib/style/defs.ex @@ -62,19 +62,18 @@ defmodule Styler.Style.Defs do end end - # all the other kinds of defs! - # @TODO all paths here skip, which means that `def a .. quote do def b ...` won't style `def b` - def run({{def, def_meta, [head, body]}, _} = zipper, ctx) when def in [:def, :defp] do + def run({{def, def_meta, [head, [{{:__block__, dm, [:do]}, {_, bm, _}} | _] = body]}, _} = zipper, ctx) + when def in [:def, :defp] do def_line = def_meta[:line] + end_line = def_meta[:end][:line] || bm[:closing][:line] || dm[:line] - if do_meta = def_meta[:do] do - # This is a def with a do end block - end_line = def_meta[:end][:line] - - if def_line == end_line do + cond do + def_line == end_line -> {:skip, zipper, ctx} - else - do_line = do_meta[:line] + + # def do end + Keyword.has_key?(def_meta, :do) -> + do_line = dm[:line] delta = def_line - do_line def_meta = @@ -92,19 +91,12 @@ defmodule Styler.Style.Defs do |> Style.shift_comments(do_line..end_line, delta) {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} - end - else - # This is a def with a keyword do - [{{:__block__, do_meta, [:do]}, {_, body_meta, _}}] = body - end_line = body_meta[:closing][:line] || do_meta[:line] - if def_line == end_line do - {:skip, zipper, ctx} - else + # def , do: + true -> node = Style.set_line({def, def_meta, [head, body]}, def_line) comments = Style.displace_comments(ctx.comments, def_line..end_line) {:skip, Zipper.replace(zipper, node), %{ctx | comments: comments}} - end end end diff --git a/test/style/defs_test.exs b/test/style/defs_test.exs index 808d80fe..f7a9fab7 100644 --- a/test/style/defs_test.exs +++ b/test/style/defs_test.exs @@ -234,7 +234,15 @@ defmodule Styler.Style.DefsTest do """) end - test "regression: @def module attribute" do - assert_style("@def ~s(this should be okay)") + describe "no ops" do + test "regression: @def module attribute" do + assert_style("@def ~s(this should be okay)") + end + + test "no explode on invalid def syntax" do + assert_style("def foo, true") + assert_style("def foo(a), true") + assert_raise SyntaxError, fn -> assert_style("def foo(a) true") end + end end end From 1df5f1d5b4e86547ba79244c7216c7cdaebeb743 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Wed, 5 Mar 2025 23:21:42 -0800 Subject: [PATCH 75/86] fix CI for older elixir --- test/style/deprecations_test.exs | 4 ++++ test/style/single_node_test.exs | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/style/deprecations_test.exs b/test/style/deprecations_test.exs index d0633f91..0fbd13b7 100644 --- a/test/style/deprecations_test.exs +++ b/test/style/deprecations_test.exs @@ -148,5 +148,9 @@ defmodule Styler.Style.DeprecationsTest do assert_style "a |> x() |> :timer.minutes()" assert_style "a |> x() |> :timer.seconds()" end + + test "combined with to_timeout improvements" do + assert_style ":timer.minutes(60 * 4)", "to_timeout(hour: 4)" + end end end diff --git a/test/style/single_node_test.exs b/test/style/single_node_test.exs index 46de5f94..762049ca 100644 --- a/test/style/single_node_test.exs +++ b/test/style/single_node_test.exs @@ -361,10 +361,6 @@ defmodule Styler.Style.SingleNodeTest do assert_style "to_timeout(second: 60 * 60)", "to_timeout(hour: 1)" end - test "combined with :timer.x deprecation rewrite" do - assert_style ":timer.minutes(60 * 4)", "to_timeout(hour: 4)" - end - test "doesnt mess with" do assert_style "to_timeout(hour: n * m)" assert_style "to_timeout(whatever)" From be4dceca7e9fc89e804f611ebf1e53bca65c8d8d Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Sun, 16 Mar 2025 20:13:23 -0600 Subject: [PATCH 76/86] v1.4.1 --- CHANGELOG.md | 4 +++- mix.exs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 063917a4..7daa8203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,11 @@ they can and will change without that change being reflected in Styler's semanti ## main +## 1.4.1 + ### Improvements -- `to_timeout/1` use the next largest unit in some simple instances +- `to_timeout/1` rewrites to use the next largest unit in some simple instances ```elixir # before diff --git a/mix.exs b/mix.exs index 344cd357..9891e240 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.4.0" + @version "1.4.1" @url "https://github.com/adobe/elixir-styler" def project do From 6b42462f49eee3d6c60049ec9e3b109ea31ccc69 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 1 Apr 2025 11:23:28 -0600 Subject: [PATCH 77/86] if: drop empty do bodies. Closes #227 --- CHANGELOG.md | 4 ++++ docs/control_flow_macros.md | 28 +++++++++++----------------- lib/style/blocks.ex | 11 ++++++----- test/style/blocks_test.exs | 26 ++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7daa8203..74797496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ they can and will change without that change being reflected in Styler's semanti ## main +### Improvements + +- `if`: drop empty `do` bodies like `if a, do: nil, else: b` => `if !a, do: b` (#227) + ## 1.4.1 ### Improvements diff --git a/docs/control_flow_macros.md b/docs/control_flow_macros.md index 70a85460..4fa6e05d 100644 --- a/docs/control_flow_macros.md +++ b/docs/control_flow_macros.md @@ -21,6 +21,8 @@ We advocate for `case` and `if` as the first tools to be considered for any cont Never! `unless` [is being deprecated](https://github.com/elixir-lang/elixir/pull/13769#issuecomment-2334878315) and so should not be used. +Styler replaces `unless` statements with their `if` equivalent similar to using the `mix format --migrate_unless` flag. + ### use `with` when... > `with` great power comes great responsibility @@ -56,34 +58,26 @@ if a, do: b, else: nil if a, do: b ``` +It also removes `do: nil` when an `else` is present, inverting the head to maintain semantics + +```elixir +if a, do: nil, else: b +# styled: +if !a, do: b +``` + ### Negation Inversion -Styler removes negators in the head of `if` and `unless` statements by "inverting" the statement. +Styler removes negators in the head of `if` statements by "inverting" the statement. The following operators are considered "negators": `!`, `not`, `!=`, `!==` - Examples: ```elixir -# negated `if` statement with no `else` clause are rewritten to `unless` -if not x, do: y -# Styled: -unless x, do: y - # negated `if` statements with an `else` clause have their clauses inverted and negation removed if !x, do: y, else: z # Styled: if x, do: z, else: y - -# negated `unless` statements are rewritten to `if` -unless x != y, do: z -# B styled: -if x == y, do: z - -# `unless` with `else` is verboten; these are always rewritten to `if` statements -unless x, do: y, else: z -# styled: -if x, do: z, else: y ``` Because elixir relies on truthy/falsey values for its `if` statements, boolean casting is unnecessary and so double negation is simply removed. diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index 7a74378c..322bca97 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -31,6 +31,7 @@ defmodule Styler.Style.Blocks do alias Styler.Zipper defguardp is_negator(n) when elem(n, 0) in [:!, :not, :!=, :!==] + defguardp is_empty_body(n) when elem(n, 0) == :__block__ and elem(n, 2) in [[nil], []] # case statement with exactly 2 `->` cases # rewrite to `if` if it's any of 3 trivial cases @@ -139,13 +140,13 @@ defmodule Styler.Style.Blocks do [negator, [{do_, do_body}, {else_, else_body}]] when is_negator(negator) -> zipper |> Zipper.replace({:if, m, [invert(negator), [{do_, else_body}, {else_, do_body}]]}) |> run(ctx) - # drop `else end` - [head, [do_block, {_, {:__block__, _, []}}]] -> + # drop `else end` and `else: nil` + [head, [do_block, {_, else_body}]] when is_empty_body(else_body) -> {:cont, Zipper.replace(zipper, {:if, m, [head, [do_block]]}), ctx} - # drop `else: nil` - [head, [do_block, {_, {:__block__, _, [nil]}}]] -> - {:cont, Zipper.replace(zipper, {:if, m, [head, [do_block]]}), ctx} + # invert and drop `do: nil` + [head, [{do_, do_body}, {_, else_body}]] when is_empty_body(do_body) -> + {:cont, Zipper.replace(zipper, {:if, m, [invert(head), [{do_, else_body}]]}), ctx} [head, [do_, else_]] -> if Style.max_line(do_) > Style.max_line(else_) do diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index e8f4626e..56efd186 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -988,6 +988,32 @@ defmodule Styler.Style.BlocksTest do ) end + test "inverts do nil" do + assert_style("if a, do: b, else: nil", "if a, do: b") + + assert_style("if a do nil else b end", """ + if !a do + b + end + """) + + assert_style( + """ + if a == b do + # comment + else + :ok + end + """, + """ + if a != b do + # comment + :ok + end + """ + ) + end + test "double negator rewrites" do for a <- ~w(not !), block <- ["do: z", "do: z, else: zz"] do assert_style "if #{a} (x != y), #{block}", "if x == y, #{block}" From 5b1c94631bd4748cb427c09562c11af46f452b64 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 29 Apr 2025 18:15:14 +0300 Subject: [PATCH 78/86] Fix large comment block mangling bug when ordering sibling AST (#232) Closes #230 --- CHANGELOG.md | 4 ++ lib/style.ex | 94 ++++++++++++++----------------------- lib/style/configs.ex | 2 +- test/style/configs_test.exs | 43 ++++++++++++++++- 4 files changed, 80 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74797496..4da3d281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ they can and will change without that change being reflected in Styler's semanti - `if`: drop empty `do` bodies like `if a, do: nil, else: b` => `if !a, do: b` (#227) +### Fixes + +- fix bug that mangled large blocks of comments when sorting in configs or with `# styler:sort` (#230, h/t @cschmatzler) + ## 1.4.1 ### Improvements diff --git a/lib/style.ex b/lib/style.ex index f1a2dcf8..03b404fa 100644 --- a/lib/style.ex +++ b/lib/style.ex @@ -173,67 +173,47 @@ defmodule Styler.Style do def max_line([_ | _] = list), do: list |> List.last() |> max_line() def max_line(ast) do - meta = - case ast do - {_, meta, _} -> - meta + meta = meta(ast) - _ -> - [] - end + cond do + line = meta[:end_of_expression][:line] -> + line - if max_line = meta[:closing][:line] do - max_line - else - {_, max_line} = - Macro.prewalk(ast, 0, fn - {_, meta, _} = ast, max -> {ast, max(meta[:line] || max, max)} - ast, max -> {ast, max} - end) + line = meta[:closing][:line] -> + line + + true -> + {_, max_line} = + Macro.prewalk(ast, 0, fn + {_, meta, _} = ast, max -> {ast, max(meta[:line] || max, max)} + ast, max -> {ast, max} + end) - max_line + max_line end end + @doc "Sets the nodes' meta line and comments' line numbers to fit the ordering of the nodes list." + # TODO this doesn't grab comments which are floating as their own paragrpah, unconnected to a node + # they'll just be left floating where they were, then mangled with the re-ordered comments.. def order_line_meta_and_comments(nodes, comments, first_line) do - {nodes, comments, node_comments} = fix_lines(nodes, comments, first_line, [], []) - {nodes, Enum.sort_by(comments ++ node_comments, & &1.line)} - end + {nodes, shifted_comments, comments, _line} = + Enum.reduce(nodes, {[], [], comments, first_line}, fn node, {n_acc, c_acc, comments, move_to_line} -> + meta = meta(node) + line = meta[:line] + last_line = max_line(node) + {mine, comments} = comments_for_lines(comments, line, last_line) - defp fix_lines([], comments, _, node_acc, c_acc), do: {Enum.reverse(node_acc), comments, c_acc} + shift = move_to_line - (List.first(mine)[:line] || line) + 1 + shifted_node = shift_line(node, shift) + shifted_comments = Enum.map(mine, &%{&1 | line: &1.line + shift}) - defp fix_lines([node | nodes], comments, start_line, n_acc, c_acc) do - meta = meta(node) - line = meta[:line] - last_line = meta[:end_of_expression][:line] || max_line(node) + move_to_line = last_line + shift + (meta[:end_of_expression][:newlines] || 0) - {node, node_comments, comments} = - if start_line == line do - {node, [], comments} - else - {mine, comments} = comments_for_lines(comments, line, last_line) - line_with_comments = (List.first(mine)[:line] || line) - (List.first(mine)[:previous_eol_count] || 1) + 1 - - if line_with_comments == start_line do - {node, mine, comments} - else - shift = start_line - line_with_comments - # fix the node's line - node = shift_line(node, shift) - # fix the comment's line - mine = Enum.map(mine, &%{&1 | line: &1.line + shift}) - {node, mine, comments} - end - end + {[shifted_node | n_acc], shifted_comments ++ c_acc, comments, move_to_line} + end) - meta = meta(node) - # @TODO what about comments that were free floating between blocks? i'm just ignoring them and maybe always will... - # kind of just want to shove them to the end though, so that they don't interrupt existing stanzas. - # i think that's accomplishable by doing a final call above that finds all comments in the comments list that weren't moved - # and which are in the range of start..finish and sets their lines to finish! - last_line = meta[:end_of_expression][:line] || max_line(node) - last_line = (meta[:end_of_expression][:newlines] || 1) + last_line - fix_lines(nodes, comments, last_line, [node | n_acc], node_comments ++ c_acc) + {Enum.reverse(nodes), Enum.sort_by(comments ++ shifted_comments, & &1.line)} end # typical node @@ -243,13 +223,9 @@ defmodule Styler.Style do def meta(_), do: nil @doc """ - Returns all comments "for" a node, including on the line before it. - see `comments_for_lines` for more + Returns all comments "for" a node, including on the line before it. see `comments_for_lines` for more """ - def comments_for_node({_, m, _} = node, comments) do - last_line = m[:end_of_expression][:line] || max_line(node) - comments_for_lines(comments, m[:line], last_line) - end + def comments_for_node({_, m, _} = node, comments), do: comments_for_lines(comments, m[:line], max_line(node)) @doc """ Gets all comments in range start_line..last_line, and any comments immediately before start_line.s @@ -268,10 +244,6 @@ defmodule Styler.Style do comments |> Enum.reverse() |> comments_for_lines(start_line, last_line, [], []) end - defp comments_for_lines(reversed_comments, start, last, match, acc) - - defp comments_for_lines([], _, _, match, acc), do: {Enum.reverse(match), acc} - defp comments_for_lines([%{line: line} = comment | rev_comments], start, last, match, acc) do cond do # after our block - no match @@ -285,4 +257,6 @@ defmodule Styler.Style do true -> {match, Enum.reverse(rev_comments, [comment | acc])} end end + + defp comments_for_lines([], _, _, match, acc), do: {match, acc} end diff --git a/lib/style/configs.ex b/lib/style/configs.ex index 75a0d750..d83b4474 100644 --- a/lib/style/configs.ex +++ b/lib/style/configs.ex @@ -86,7 +86,7 @@ defmodule Styler.Style.Configs do # the first node of `rest` is greater than the highest line in configs, assignments # config line is the first line to be used as part of this block {node_comments, _} = Style.comments_for_node(config, comments) - first_line = min(List.last(node_comments)[:line] || cfm[:line], cfm[:line]) + first_line = min(List.first(node_comments)[:line] || cfm[:line], cfm[:line]) Style.order_line_meta_and_comments(nodes, comments, first_line) else {nodes, comments} diff --git a/test/style/configs_test.exs b/test/style/configs_test.exs index 3f3db06f..0bb4966e 100644 --- a/test/style/configs_test.exs +++ b/test/style/configs_test.exs @@ -166,6 +166,7 @@ defmodule Styler.Style.ConfigsTest do config :a, 2 config :a, 3 config :a, 4 + # comment # b comment config :b, 1 @@ -334,9 +335,9 @@ defmodule Styler.Style.ConfigsTest do c: :d, e: :f - config :c, - # some junk after b, idk + # some junk after b, idk + config :c, # ca ca: :ca, # cb 1 @@ -350,5 +351,43 @@ defmodule Styler.Style.ConfigsTest do """ ) end + + test "big block regression #230" do + # The nodes are in reverse order + assert_style( + """ + import Config + + # z-a + # z-b + # z-c + # z-d + # z-e + config :z, z + + # y + config :y, y + + # x + config :x, x + """, + """ + import Config + + # x + config :x, x + + # y + config :y, y + + # z-a + # z-b + # z-c + # z-d + # z-e + config :z, z + """ + ) + end end end From 50ae386e7cde130e53a411f8fd6cb66824005e84 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 29 Apr 2025 09:39:37 -0600 Subject: [PATCH 79/86] if: treat is_nil as a negator --- CHANGELOG.md | 7 +++++++ docs/control_flow_macros.md | 9 +++++++-- lib/style/blocks.ex | 7 ++++--- test/style/blocks_test.exs | 6 ++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4da3d281..bf005eda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ they can and will change without that change being reflected in Styler's semanti ### Improvements - `if`: drop empty `do` bodies like `if a, do: nil, else: b` => `if !a, do: b` (#227) +- `if`: treat `is_nil` as a negator. + + this means `if is_nil(x), do: a, else: b` will be inverted to `if x, do: b, else: a` + + or `if !is_nil(x), do: y` will be rewritten as `if x, do: y` + + This could cause problems where x is `false` =) ### Fixes diff --git a/docs/control_flow_macros.md b/docs/control_flow_macros.md index 4fa6e05d..803fb9e2 100644 --- a/docs/control_flow_macros.md +++ b/docs/control_flow_macros.md @@ -69,7 +69,7 @@ if !a, do: b ### Negation Inversion Styler removes negators in the head of `if` statements by "inverting" the statement. -The following operators are considered "negators": `!`, `not`, `!=`, `!==` +The following operators are considered "negators": `!`, `not`, `!=`, `!==`, `is_nil` Examples: @@ -83,7 +83,12 @@ if x, do: z, else: y Because elixir relies on truthy/falsey values for its `if` statements, boolean casting is unnecessary and so double negation is simply removed. ```elixir -if !!x, do: y +if !x, do: y +# styled: +if x, do: y + +# similarly +if !is_nil(x), do: y # styled: if x, do: y ``` diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index 322bca97..3c1abeb8 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -30,7 +30,7 @@ defmodule Styler.Style.Blocks do alias Styler.Style alias Styler.Zipper - defguardp is_negator(n) when elem(n, 0) in [:!, :not, :!=, :!==] + defguardp is_if_negator(n) when elem(n, 0) in [:!, :not, :!=, :!==, :is_nil] defguardp is_empty_body(n) when elem(n, 0) == :__block__ and elem(n, 2) in [[nil], []] # case statement with exactly 2 `->` cases @@ -132,12 +132,12 @@ defmodule Styler.Style.Blocks do case children do # double negator # if !!x, do: y[, else: ...] => if x, do: y[, else: ...] - [{_, _, [nb]} = na, do_else] when is_negator(na) and is_negator(nb) -> + [{_, _, [nb]} = na, do_else] when is_if_negator(na) and is_if_negator(nb) -> zipper |> Zipper.replace({:if, m, [invert(nb), do_else]}) |> run(ctx) # Credo.Check.Refactor.NegatedConditionsWithElse # if !x, do: y, else: z => if x, do: z, else: y - [negator, [{do_, do_body}, {else_, else_body}]] when is_negator(negator) -> + [negator, [{do_, do_body}, {else_, else_body}]] when is_if_negator(negator) -> zipper |> Zipper.replace({:if, m, [invert(negator), [{do_, else_body}, {else_, do_body}]]}) |> run(ctx) # drop `else end` and `else: nil` @@ -368,5 +368,6 @@ defmodule Styler.Style.Blocks do defp invert({:!, _, [condition]}), do: condition defp invert({:not, _, [condition]}), do: condition defp invert({:in, m, [_, _]} = ast), do: {:not, m, [ast]} + defp invert({:is_nil, _, [a]}), do: a defp invert({_, m, _} = ast), do: {:!, [line: m[:line]], [ast]} end diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index 56efd186..99faf996 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -1012,6 +1012,12 @@ defmodule Styler.Style.BlocksTest do end """ ) + + assert_style "if is_nil(a.b), do: nil, else: a.c", "if a.b, do: a.c" + end + + test "if not is_nil" do + assert_style "if is_nil(a), do: b, else: c", "if a, do: c, else: b" end test "double negator rewrites" do From 17434b65fd41588afd9c649b4a2542042477b40e Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 29 Apr 2025 10:22:31 -0600 Subject: [PATCH 80/86] Revert "if: treat is_nil as a negator" This reverts commit 50ae386e7cde130e53a411f8fd6cb66824005e84. --- CHANGELOG.md | 7 ------- docs/control_flow_macros.md | 9 ++------- lib/style/blocks.ex | 7 +++---- test/style/blocks_test.exs | 6 ------ 4 files changed, 5 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf005eda..4da3d281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,6 @@ they can and will change without that change being reflected in Styler's semanti ### Improvements - `if`: drop empty `do` bodies like `if a, do: nil, else: b` => `if !a, do: b` (#227) -- `if`: treat `is_nil` as a negator. - - this means `if is_nil(x), do: a, else: b` will be inverted to `if x, do: b, else: a` - - or `if !is_nil(x), do: y` will be rewritten as `if x, do: y` - - This could cause problems where x is `false` =) ### Fixes diff --git a/docs/control_flow_macros.md b/docs/control_flow_macros.md index 803fb9e2..4fa6e05d 100644 --- a/docs/control_flow_macros.md +++ b/docs/control_flow_macros.md @@ -69,7 +69,7 @@ if !a, do: b ### Negation Inversion Styler removes negators in the head of `if` statements by "inverting" the statement. -The following operators are considered "negators": `!`, `not`, `!=`, `!==`, `is_nil` +The following operators are considered "negators": `!`, `not`, `!=`, `!==` Examples: @@ -83,12 +83,7 @@ if x, do: z, else: y Because elixir relies on truthy/falsey values for its `if` statements, boolean casting is unnecessary and so double negation is simply removed. ```elixir -if !x, do: y -# styled: -if x, do: y - -# similarly -if !is_nil(x), do: y +if !!x, do: y # styled: if x, do: y ``` diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index 3c1abeb8..322bca97 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -30,7 +30,7 @@ defmodule Styler.Style.Blocks do alias Styler.Style alias Styler.Zipper - defguardp is_if_negator(n) when elem(n, 0) in [:!, :not, :!=, :!==, :is_nil] + defguardp is_negator(n) when elem(n, 0) in [:!, :not, :!=, :!==] defguardp is_empty_body(n) when elem(n, 0) == :__block__ and elem(n, 2) in [[nil], []] # case statement with exactly 2 `->` cases @@ -132,12 +132,12 @@ defmodule Styler.Style.Blocks do case children do # double negator # if !!x, do: y[, else: ...] => if x, do: y[, else: ...] - [{_, _, [nb]} = na, do_else] when is_if_negator(na) and is_if_negator(nb) -> + [{_, _, [nb]} = na, do_else] when is_negator(na) and is_negator(nb) -> zipper |> Zipper.replace({:if, m, [invert(nb), do_else]}) |> run(ctx) # Credo.Check.Refactor.NegatedConditionsWithElse # if !x, do: y, else: z => if x, do: z, else: y - [negator, [{do_, do_body}, {else_, else_body}]] when is_if_negator(negator) -> + [negator, [{do_, do_body}, {else_, else_body}]] when is_negator(negator) -> zipper |> Zipper.replace({:if, m, [invert(negator), [{do_, else_body}, {else_, do_body}]]}) |> run(ctx) # drop `else end` and `else: nil` @@ -368,6 +368,5 @@ defmodule Styler.Style.Blocks do defp invert({:!, _, [condition]}), do: condition defp invert({:not, _, [condition]}), do: condition defp invert({:in, m, [_, _]} = ast), do: {:not, m, [ast]} - defp invert({:is_nil, _, [a]}), do: a defp invert({_, m, _} = ast), do: {:!, [line: m[:line]], [ast]} end diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index 99faf996..56efd186 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -1012,12 +1012,6 @@ defmodule Styler.Style.BlocksTest do end """ ) - - assert_style "if is_nil(a.b), do: nil, else: a.c", "if a.b, do: a.c" - end - - test "if not is_nil" do - assert_style "if is_nil(a), do: b, else: c", "if a, do: c, else: b" end test "double negator rewrites" do From aa3e7ce7157085c637785f3f7ff9d8208845fb6d Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Tue, 29 Apr 2025 10:23:04 -0600 Subject: [PATCH 81/86] Revert "if: drop empty do bodies. Closes #227" This reverts commit 6b42462f49eee3d6c60049ec9e3b109ea31ccc69. --- CHANGELOG.md | 4 ---- docs/control_flow_macros.md | 28 +++++++++++++++++----------- lib/style/blocks.ex | 11 +++++------ test/style/blocks_test.exs | 26 -------------------------- 4 files changed, 22 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4da3d281..552e3945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,6 @@ they can and will change without that change being reflected in Styler's semanti ## main -### Improvements - -- `if`: drop empty `do` bodies like `if a, do: nil, else: b` => `if !a, do: b` (#227) - ### Fixes - fix bug that mangled large blocks of comments when sorting in configs or with `# styler:sort` (#230, h/t @cschmatzler) diff --git a/docs/control_flow_macros.md b/docs/control_flow_macros.md index 4fa6e05d..70a85460 100644 --- a/docs/control_flow_macros.md +++ b/docs/control_flow_macros.md @@ -21,8 +21,6 @@ We advocate for `case` and `if` as the first tools to be considered for any cont Never! `unless` [is being deprecated](https://github.com/elixir-lang/elixir/pull/13769#issuecomment-2334878315) and so should not be used. -Styler replaces `unless` statements with their `if` equivalent similar to using the `mix format --migrate_unless` flag. - ### use `with` when... > `with` great power comes great responsibility @@ -58,26 +56,34 @@ if a, do: b, else: nil if a, do: b ``` -It also removes `do: nil` when an `else` is present, inverting the head to maintain semantics - -```elixir -if a, do: nil, else: b -# styled: -if !a, do: b -``` - ### Negation Inversion -Styler removes negators in the head of `if` statements by "inverting" the statement. +Styler removes negators in the head of `if` and `unless` statements by "inverting" the statement. The following operators are considered "negators": `!`, `not`, `!=`, `!==` + Examples: ```elixir +# negated `if` statement with no `else` clause are rewritten to `unless` +if not x, do: y +# Styled: +unless x, do: y + # negated `if` statements with an `else` clause have their clauses inverted and negation removed if !x, do: y, else: z # Styled: if x, do: z, else: y + +# negated `unless` statements are rewritten to `if` +unless x != y, do: z +# B styled: +if x == y, do: z + +# `unless` with `else` is verboten; these are always rewritten to `if` statements +unless x, do: y, else: z +# styled: +if x, do: z, else: y ``` Because elixir relies on truthy/falsey values for its `if` statements, boolean casting is unnecessary and so double negation is simply removed. diff --git a/lib/style/blocks.ex b/lib/style/blocks.ex index 322bca97..7a74378c 100644 --- a/lib/style/blocks.ex +++ b/lib/style/blocks.ex @@ -31,7 +31,6 @@ defmodule Styler.Style.Blocks do alias Styler.Zipper defguardp is_negator(n) when elem(n, 0) in [:!, :not, :!=, :!==] - defguardp is_empty_body(n) when elem(n, 0) == :__block__ and elem(n, 2) in [[nil], []] # case statement with exactly 2 `->` cases # rewrite to `if` if it's any of 3 trivial cases @@ -140,13 +139,13 @@ defmodule Styler.Style.Blocks do [negator, [{do_, do_body}, {else_, else_body}]] when is_negator(negator) -> zipper |> Zipper.replace({:if, m, [invert(negator), [{do_, else_body}, {else_, do_body}]]}) |> run(ctx) - # drop `else end` and `else: nil` - [head, [do_block, {_, else_body}]] when is_empty_body(else_body) -> + # drop `else end` + [head, [do_block, {_, {:__block__, _, []}}]] -> {:cont, Zipper.replace(zipper, {:if, m, [head, [do_block]]}), ctx} - # invert and drop `do: nil` - [head, [{do_, do_body}, {_, else_body}]] when is_empty_body(do_body) -> - {:cont, Zipper.replace(zipper, {:if, m, [invert(head), [{do_, else_body}]]}), ctx} + # drop `else: nil` + [head, [do_block, {_, {:__block__, _, [nil]}}]] -> + {:cont, Zipper.replace(zipper, {:if, m, [head, [do_block]]}), ctx} [head, [do_, else_]] -> if Style.max_line(do_) > Style.max_line(else_) do diff --git a/test/style/blocks_test.exs b/test/style/blocks_test.exs index 56efd186..e8f4626e 100644 --- a/test/style/blocks_test.exs +++ b/test/style/blocks_test.exs @@ -988,32 +988,6 @@ defmodule Styler.Style.BlocksTest do ) end - test "inverts do nil" do - assert_style("if a, do: b, else: nil", "if a, do: b") - - assert_style("if a do nil else b end", """ - if !a do - b - end - """) - - assert_style( - """ - if a == b do - # comment - else - :ok - end - """, - """ - if a != b do - # comment - :ok - end - """ - ) - end - test "double negator rewrites" do for a <- ~w(not !), block <- ["do: z", "do: z, else: zz"] do assert_style "if #{a} (x != y), #{block}", "if x == y, #{block}" From c511610f9aabebfcb978c42eb2eaa9ed43b74213 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 1 May 2025 15:42:12 +0300 Subject: [PATCH 82/86] v1.4.2 --- CHANGELOG.md | 6 ++++-- mix.exs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 552e3945..997797db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,11 @@ they can and will change without that change being reflected in Styler's semanti ## main +## 1.4.2 + ### Fixes -- fix bug that mangled large blocks of comments when sorting in configs or with `# styler:sort` (#230, h/t @cschmatzler) +- fix comment misplacement for large comment blocks in config files and `# styler:sort` (#230, h/t @cschmatzler) ## 1.4.1 @@ -28,7 +30,7 @@ they can and will change without that change being reflected in Styler's semanti - fixed styler raising when encountering invalid function definition ast -## 1.4 +## 1.4.0 - A very nice change in alias lifting means Styler will make sure that your code is _using_ the aliases that it's specified. - Shoutout to the smartrent folks for finding pipifying recursion issues diff --git a/mix.exs b/mix.exs index 9891e240..305656f2 100644 --- a/mix.exs +++ b/mix.exs @@ -12,7 +12,7 @@ defmodule Styler.MixProject do use Mix.Project # Don't forget to bump the README when doing non-patch version changes - @version "1.4.1" + @version "1.4.2" @url "https://github.com/adobe/elixir-styler" def project do From f66fc54c479ac6065f5ba46e6b9513a1d489b972 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 8 May 2025 08:18:12 +0300 Subject: [PATCH 83/86] changelog: fix GH md formatting issues --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 997797db..4b591991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ This release taught Styler to try just that little bit harder when doing alias l - use knowledge of existing aliases to shorten invocations (#201, h/t me) example: + alias A.B.C A.B.C.foo() @@ -56,6 +57,7 @@ This release taught Styler to try just that little bit harder when doing alias l A.B.C.baz() becomes: + alias A.B.C C.foo() @@ -93,6 +95,7 @@ This release taught Styler to try just that little bit harder when doing alias l - `# styler:sort` will sort arbitrary ast nodes within a `do end` block: Given: + # styler:sort my_macro "some arg" do another_macro :q @@ -104,6 +107,7 @@ This release taught Styler to try just that little bit harder when doing alias l end We get + # styler:sort my_macro "some arg" do another_macro :e @@ -134,8 +138,6 @@ This release taught Styler to try just that little bit harder when doing alias l - `# styler:sort` no longer blows up on keyword lists :X -### Fixes - ## 1.3.0 ### Improvements From e2d4e112dc11a038677501834c0502f148f74b2c Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 22 May 2025 13:24:48 -0600 Subject: [PATCH 84/86] optimize zipper performance --- lib/style/configs.ex | 7 +++--- lib/style/module_directives.ex | 8 +++---- lib/zipper.ex | 39 ++++++++++++++-------------------- test/zipper_test.exs | 12 +++++------ 4 files changed, 30 insertions(+), 36 deletions(-) diff --git a/lib/style/configs.ex b/lib/style/configs.ex index d83b4474..a5379897 100644 --- a/lib/style/configs.ex +++ b/lib/style/configs.ex @@ -48,8 +48,9 @@ defmodule Styler.Style.Configs do end def run({{:config, cfm, [_, _ | _]} = config, zm}, %{mix_config?: true, comments: comments} = ctx) do + {l, p, r} = zm # all of these list are reversed due to the reduce - {configs, assignments, rest} = accumulate(zm.r, [], []) + {configs, assignments, rest} = accumulate(r, [], []) # @TODO # okay so comments between nodes that we moved....... # lets just push them out of the way (???). so @@ -92,9 +93,9 @@ defmodule Styler.Style.Configs do {nodes, comments} end - [config | left_siblings] = Enum.reverse(nodes, zm.l) + [config | left_siblings] = Enum.reverse(nodes, l) - {:skip, {config, %{zm | l: left_siblings, r: rest}}, %{ctx | comments: comments}} + {:skip, {config, {left_siblings, p, rest}}, %{ctx | comments: comments}} end def run(zipper, %{config?: true} = ctx) do diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index b88f3c62..16a372e1 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -107,9 +107,9 @@ defmodule Styler.Style.ModuleDirectives do # puts `@derive` before `defstruct` etc, fixing compiler warnings def run({{:@, _, [{:derive, _, _}]}, _} = zipper, ctx) do case Style.ensure_block_parent(zipper) do - {:ok, {derive, %{l: left_siblings} = z_meta}} -> + {:ok, {derive, {l, p, r}}} -> previous_defstruct = - left_siblings + l |> Stream.with_index() |> Enum.find_value(fn {{struct_def, meta, _}, index} when struct_def in @defstruct -> {meta[:line], index} @@ -119,8 +119,8 @@ defmodule Styler.Style.ModuleDirectives do if previous_defstruct do {defstruct_line, defstruct_index} = previous_defstruct derive = Style.set_line(derive, defstruct_line - 1) - left_siblings = List.insert_at(left_siblings, defstruct_index + 1, derive) - {:skip, Zipper.remove({derive, %{z_meta | l: left_siblings}}), ctx} + left_siblings = List.insert_at(l, defstruct_index + 1, derive) + {:skip, Zipper.remove({derive, {left_siblings, p, r}}), ctx} else {:cont, zipper, ctx} end diff --git a/lib/zipper.ex b/lib/zipper.ex index 9b43d1e1..c7a4487b 100644 --- a/lib/zipper.ex +++ b/lib/zipper.ex @@ -26,14 +26,8 @@ defmodule Styler.Zipper do import Kernel, except: [node: 1] @type tree :: Macro.t() - - @opaque path :: %{ - l: [tree], - ptree: zipper, - r: [tree] - } - @type zipper :: {tree, path | nil} + @type path :: {left :: [tree], parent :: zipper, right :: [tree]} @type t :: zipper @type command :: :cont | :skip | :halt @@ -91,7 +85,7 @@ defmodule Styler.Zipper do def down(zipper) do case children(zipper) do [] -> nil - [first | rest] -> {first, %{ptree: zipper, l: [], r: rest}} + [first | rest] -> {first, {[], zipper, rest}} end end @@ -102,9 +96,8 @@ defmodule Styler.Zipper do @spec up(zipper) :: zipper | nil def up({_, nil}), do: nil - def up({tree, meta}) do - children = Enum.reverse(meta.l, [tree | meta.r]) - {parent, parent_meta} = meta.ptree + def up({tree, {l, {parent, parent_meta}, r}}) do + children = Enum.reverse(l, [tree | r]) {do_replace_children(parent, children), parent_meta} end @@ -112,16 +105,16 @@ defmodule Styler.Zipper do Returns the zipper of the left sibling of the node at this zipper, or nil. """ @spec left(zipper) :: zipper | nil - def left({tree, %{l: [ltree | l], r: r} = meta}), do: {ltree, %{meta | l: l, r: [tree | r]}} + def left({tree, {[ltree | l], p, r}}), do: {ltree, {l, p, [tree | r]}} def left(_), do: nil @doc """ Returns the leftmost sibling of the node at this zipper, or itself. """ @spec leftmost(zipper) :: zipper - def leftmost({tree, %{l: [_ | _] = l} = meta}) do - [leftmost | r] = Enum.reverse(l, [tree | meta.r]) - {leftmost, %{meta | l: [], r: r}} + def leftmost({tree, {[_ | _] = l, p, r}}) do + [leftmost | r] = Enum.reverse(l, [tree | r]) + {leftmost, {[], p, r}} end def leftmost({_, _} = zipper), do: zipper @@ -130,16 +123,16 @@ defmodule Styler.Zipper do Returns the zipper of the right sibling of the node at this zipper, or nil. """ @spec right(zipper) :: zipper | nil - def right({tree, %{r: [rtree | r]} = meta}), do: {rtree, %{meta | r: r, l: [tree | meta.l]}} + def right({tree, {l, p, [rtree | r]}}), do: {rtree, {[tree | l], p, r}} def right(_), do: nil @doc """ Returns the rightmost sibling of the node at this zipper, or itself. """ @spec rightmost(zipper) :: zipper - def rightmost({tree, %{r: [_ | _] = r} = meta}) do - [rightmost | l] = Enum.reverse(r, [tree | meta.l]) - {rightmost, %{meta | l: l, r: []}} + def rightmost({tree, {l, p, [_ | _] = r}}) do + [rightmost | l] = Enum.reverse(r, [tree | l]) + {rightmost, {l, p, []}} end def rightmost({_, _} = zipper), do: zipper @@ -163,8 +156,8 @@ defmodule Styler.Zipper do """ @spec remove(zipper) :: zipper def remove({_, nil}), do: raise(ArgumentError, message: "Cannot remove the top level node.") - def remove({_, %{l: [left | rest]} = meta}), do: prev_down({left, %{meta | l: rest}}) - def remove({_, %{ptree: {parent, parent_meta}, r: children}}), do: {do_replace_children(parent, children), parent_meta} + def remove({_, {[left | rest], p, r}}), do: prev_down({left, {rest, p, r}}) + def remove({_, {_, {parent, parent_meta}, children}}), do: {do_replace_children(parent, children), parent_meta} @doc """ Inserts the item as the left sibling of the node at this zipper, without @@ -184,7 +177,7 @@ defmodule Styler.Zipper do """ @spec prepend_siblings(zipper, [tree]) :: zipper def prepend_siblings({node, nil}, siblings), do: {:__block__, [], siblings ++ [node]} |> zip() |> down() |> rightmost() - def prepend_siblings({tree, meta}, siblings), do: {tree, %{meta | l: Enum.reverse(siblings, meta.l)}} + def prepend_siblings({tree, {l, p, r}}, siblings), do: {tree, {Enum.reverse(siblings, l), p , r}} @doc """ Inserts the item as the right sibling of the node at this zipper, without @@ -204,7 +197,7 @@ defmodule Styler.Zipper do """ @spec insert_siblings(zipper, [tree]) :: zipper def insert_siblings({node, nil}, siblings), do: {:__block__, [], [node | siblings]} |> zip() |> down() - def insert_siblings({tree, meta}, siblings), do: {tree, %{meta | r: siblings ++ meta.r}} + def insert_siblings({tree, {l, p, r}}, siblings), do: {tree, {l, p, siblings ++ r}} @doc """ Inserts the item as the leftmost child of the node at this zipper, diff --git a/test/zipper_test.exs b/test/zipper_test.exs index 2359d481..81dcf6e3 100644 --- a/test/zipper_test.exs +++ b/test/zipper_test.exs @@ -63,14 +63,14 @@ defmodule StylerTest.ZipperTest do describe "down/1" do test "rips and tears the parent node" do - assert [1, 2] |> Zipper.zip() |> Zipper.down() == {1, %{l: [], r: [2], ptree: {[1, 2], nil}}} - assert {1, 2} |> Zipper.zip() |> Zipper.down() == {1, %{l: [], r: [2], ptree: {{1, 2}, nil}}} + assert [1, 2] |> Zipper.zip() |> Zipper.down() == {1, {[], {[1, 2], nil}, [2]}} + assert {1, 2} |> Zipper.zip() |> Zipper.down() == {1, {[], {{1, 2}, nil}, [2]}} assert {:foo, [], [1, 2]} |> Zipper.zip() |> Zipper.down() == - {1, %{l: [], r: [2], ptree: {{:foo, [], [1, 2]}, nil}}} + {1, {[], {{:foo, [], [1, 2]}, nil}, [2]}} assert {{:., [], [:a, :b]}, [], [1, 2]} |> Zipper.zip() |> Zipper.down() == - {{:., [], [:a, :b]}, %{l: [], r: [1, 2], ptree: {{{:., [], [:a, :b]}, [], [1, 2]}, nil}}} + {{:., [], [:a, :b]}, {[],{{{:., [], [:a, :b]}, [], [1, 2]}, nil}, [1, 2]}} end end @@ -471,8 +471,8 @@ defmodule StylerTest.ZipperTest do end test "builds a new root node made of a block" do - assert {42, %{l: [:nope], ptree: {{:__block__, _, _}, nil}}} = 42 |> Zipper.zip() |> Zipper.insert_left(:nope) - assert {42, %{r: [:nope], ptree: {{:__block__, _, _}, nil}}} = 42 |> Zipper.zip() |> Zipper.insert_right(:nope) + assert {42, {[:nope], {{:__block__, _, _}, nil}, []}} = 42 |> Zipper.zip() |> Zipper.insert_left(:nope) + assert {42, {[], {{:__block__, _, _}, nil}, [:nope]}} = 42 |> Zipper.zip() |> Zipper.insert_right(:nope) end end From 52c7e4a802bb033e7f3474cecf7798f666c2ed5a Mon Sep 17 00:00:00 2001 From: Jesse Herrick Date: Fri, 6 Jun 2025 13:48:30 -0400 Subject: [PATCH 85/86] We still don't want moduledoc false in modules without it --- lib/style/module_directives.ex | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/style/module_directives.ex b/lib/style/module_directives.ex index df0fc0f4..59dd153a 100644 --- a/lib/style/module_directives.ex +++ b/lib/style/module_directives.ex @@ -21,7 +21,6 @@ defmodule Styler.Style.ModuleDirectives do * `Credo.Check.Consistency.MultiAliasImportRequireUse` (force expansion) * `Credo.Check.Readability.AliasOrder` (we sort `__MODULE__`, which credo doesn't) - * `Credo.Check.Readability.ModuleDoc` (adds `@moduledoc false` if missing. includes `*.exs` files) * `Credo.Check.Readability.MultiAlias` * `Credo.Check.Readability.StrictModuleLayout` (see section below for details) * `Credo.Check.Readability.UnnecessaryAliasExpansion` @@ -50,7 +49,6 @@ defmodule Styler.Style.ModuleDirectives do @directives ~w(alias import require use)a @attr_directives ~w(moduledoc shortdoc behaviour)a @defstruct ~w(schema embedded_schema defstruct)a - @moduledoc_false {:@, [line: nil], [{:moduledoc, [line: nil], [{:__block__, [line: nil], [false]}]}]} def run({{:defmodule, _, children}, _} = zipper, ctx) do [name, [{{:__block__, do_meta, [:do]}, _body}]] = children @@ -132,14 +130,6 @@ defmodule Styler.Style.ModuleDirectives do def run(zipper, ctx), do: {:cont, zipper, ctx} - defp moduledoc({:__aliases__, m, aliases}) do - name = aliases |> List.last() |> to_string() - # module names ending with these suffixes will not have a default moduledoc appended - if !String.ends_with?(name, ~w(Test Mixfile MixProject Controller Endpoint Repo Router Socket View HTML JSON)) do - Style.set_line(@moduledoc_false, m[:line] + 1) - end - end - # a dynamic module name, like `defmodule my_variable do ... end` defp moduledoc(_), do: nil From cb50d8bf488263719486ee2a54d4386368d719d5 Mon Sep 17 00:00:00 2001 From: Jesse Herrick Date: Fri, 6 Jun 2025 13:56:26 -0400 Subject: [PATCH 86/86] Remove missed conflict --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index d7cfd25e..a9c2ec74 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,7 @@ Add `:styler` as a dependency to your project's `mix.exs`: ```elixir def deps do [ -<<<<<<< HEAD - {:styler, "~> 1.1", only: [:dev, :test], runtime: false}, -======= {:styler, "~> 1.4", only: [:dev, :test], runtime: false}, ->>>>>>> upstream/main ] end ```