Current section

27 Versions

Jump to

Compare versions

15 files changed
+955 additions
-461 deletions
  @@ -1 +1,144 @@
1 - # Changelog
\ No newline at end of file
1 + # Changelog
2 +
3 + Quokka follows [Semantic Versioning](https://semver.org) and
4 + [Common Changelog: Guiding Principles](https://common-changelog.org/#12-guiding-principles)
5 +
6 + ## [1.1.0] - 2025-02-14
7 +
8 + ### Improvements
9 +
10 + #### Line length formatting only
11 +
12 + In order to phase this into large codebases, Quokka now supports formatting only the line length, the idea being that it is easier to review a diff where one commit is just compressing vertical code and the following is the substantive rewrites -- aka the rewrites that change the AST. In order to use this feature, use `newline_fixes_only: true | false` in the config.
13 +
14 + ##### `# quokka:sort` Quokka's first comment directive
15 +
16 + Quokka 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. It also works with maps, key-value pairs (sort by key), and `defstruct`, and even arbitrary ast nodes with a `do end` block.
17 +
18 + The intention is to remove comments to humans, like `# Please keep this list sorted!`, in favor of comments to robots: `# quokka:sort`. Personally speaking, Quokka is much better at alphabetical-order than I ever will be.
19 +
20 + To use the new directive, put it on the line before a list or wordlist.
21 +
22 + This example:
23 +
24 + ```elixir
25 + # quokka:sort
26 + [:c, :a, :b]
27 +
28 + # quokka:sort
29 + ~w(a list of words)
30 +
31 + # quokka:sort
32 + @country_codes ~w(
33 + en_US
34 + po_PO
35 + fr_CA
36 + ja_JP
37 + )
38 +
39 + # quokka:sort
40 + a_var =
41 + [
42 + Modules,
43 + In,
44 + A,
45 + List
46 + ]
47 +
48 + # quokka:sort
49 + my_macro "some arg" do
50 + another_macro :q
51 + another_macro :w
52 + another_macro :e
53 + another_macro :r
54 + another_macro :t
55 + another_macro :y
56 + end
57 + ```
58 +
59 + Would yield:
60 +
61 + ```elixir
62 + # quokka:sort
63 + [:a, :b, :c]
64 +
65 + # quokka:sort
66 + ~w(a list of words)
67 +
68 + # quokka:sort
69 + @country_codes ~w(
70 + en_US
71 + fr_CA
72 + ja_JP
73 + po_PO
74 + )
75 +
76 + # quokka:sort
77 + a_var =
78 + [
79 + A,
80 + In,
81 + List,
82 + Modules
83 + ]
84 +
85 + # quokka:sort
86 + my_macro "some arg" do
87 + another_macro :e
88 + another_macro :q
89 + another_macro :r
90 + another_macro :t
91 + another_macro :w
92 + another_macro :y
93 + end
94 + ```
95 + #### Other improvements
96 + - General improvements around conflict detection, lifting in more correct places and fewer incorrect places.
97 + - Use knowledge of existing aliases to shorten invocations.
98 +
99 + example:
100 + alias A.B.C
101 +
102 + A.B.C.foo()
103 + A.B.C.bar()
104 + A.B.C.baz()
105 +
106 + becomes:
107 + alias A.B.C
108 +
109 + C.foo()
110 + C.bar()
111 + C.baz()
112 +
113 + - Config Sorting: improve comment handling when only sorting a few nodes.
114 + - Pipes: pipe-ifies when first arg to a function is a pipe. reach out if this happens in unstylish places in your code.
115 + - Pipes: unpiping assignments will make the assignment one-line when possible
116 + - Deprecations: 1.18 deprecations
117 + - `List.zip` => `Enum.zip`
118 + - `first..last = range` => `first..last//_ = range`
119 +
120 + ### Fixes
121 +
122 + - Support the credo config of the format `checks: %{enabled: [...], disabled: [...]}`, whereas previously it expected `checks: [...]}`
123 + - Pipes: optimizations are less likely to move comments
124 + - Don't pipify when the call is itself in a pipe (aka don't touch a |> b(c |> d() |>e()) |> f())
125 +
126 + ## [1.0.0] - 2025-02-10
127 +
128 + Quokka is inspired by the wonderful [`elixir-styler`](https://github.com/adobe/elixir-styler) :heart:
129 +
130 + It maintains the same directive that consistent coding standards can help teams
131 + iterate quickly, but allows a few more affordances
132 + [via `.credo.exs` configuration](https://hexdocs.pm/credo/config_file.html).
133 + This allows users with an already fine-tuned `.credo.exs` config to enjoy
134 + the automatic rewrites and strong opinions of Quokka
135 +
136 + More details about specific Credo rewrites and their configurability can be
137 + found in [Quokka: Credo inspired rewrites](https://hexdocs.pm/quokka/readme.html#credo-inspired-rewrites).
138 +
139 + Adoption of opinionated code changes can be hard in larger code bases, so
140 + Quokka allows a few configuration options in `.formatter.exs` to help
141 + isolate big sets of potentially controversial or code breaking changes that
142 + may need time for adoption. However, these may be removed in a future release.
143 + See [Quokka: Configuration](https://hexdocs.pm/quokka/readme.html#configuration)
144 + for more details.
  @@ -4,15 +4,16 @@
4 4
5 5 # Quokka
6 6
7 - <img src="docs/assets/quokka.png" alt="A happy quokka with style" width="300"/>
7 + <img src="docs/assets/quokka.png" alt="A happy quokka with style" width="300"/>
8 8
9 - Quokka is an Elixir formatter plugin that's combination of `mix format` and `mix credo`, except instead of telling you what's wrong, it just rewrites the code for you. Quokka is a fork of [Styler](https://github.com/adobe/styler) that checks the Credo config to determine which rules to rewrite. Many common, non-controversial Credo style rules are rewritten automatically, while the controversial Credo style rules are rewritten based on your Credo configuration so you can customize your style.
9 + Quokka is an Elixir formatter plugin that's combination of `mix format` and `mix credo`, except instead of telling you what's wrong, it just rewrites the code for you. Quokka is a fork of [Styler](https://github.com/adobe/elixir-styler) that checks the Credo config to determine which rules to rewrite. Many common, non-controversial Credo style rules are rewritten automatically, while the controversial Credo style rules are rewritten based on your Credo configuration so you can customize your style.
10 10
11 11 > #### WARNING {: .warning}
12 + >
12 13 > Quokka can change the behavior of your program!
13 - >
14 + >
14 15 > In some cases, this can introduce bugs. It goes without saying, but look over your changes before committing to main :)
15 - >
16 + >
16 17 > We recommend making changes in small chunks until all of the more dangerous
17 18 > changes has been safely committed to the codebase
18 19
  @@ -38,6 +39,11 @@ Then add `Quokka` as a plugin to your `.formatter.exs` file
38 39
39 40 And that's it! Now when you run `mix format` you'll also get the benefits of Quokka's Stylish Stylings.
40 41
42 + ### First Run
43 +
44 + You may want to initially run Quokka in "newline fixes only" mode. This will only fix spacing issues, making future PRs _much_ smaller and easier to digest.
45 + See the example in the configuration section if you wish to do this.
46 +
41 47 **Speed**: Expect the first run to take some time as `Quokka` rewrites violations of styles and bottlenecks on disk I/O. Subsequent formats will take noticeably less time.
42 48
43 49 ### Configuration
  @@ -56,16 +62,18 @@ in `.formatter.exs` to fine tune your setup:
56 62 files: %{
57 63 included: ["lib/", ...],
58 64 excluded: ["lib/example.ex", ...]
59 - }
65 + },
66 + newline_fixes_only: true | false
60 67 ]
61 68 ]
62 69 ```
63 - | Option | Description | Default |
64 - | --- | --- | --- |
65 - | `:files` | Quokka gets files from `.formatter.exs[:inputs]`. However, in some cases you may need to selectively exclude/include files you wish to still run in `mix format`, but have different behavior with Quokka. | `%{included: [], excluded: []}` (all files included, none excluded) |
66 - | `:inefficient_function_rewrites` | Rewrite inefficient functions to more efficient form | `true` |
67 - | `:reorder_configs` | Alphabetize `config` by key in `config/*.exs` files | `true` |
68 - | `:rewrite_deprecations` | Rewrite deprecated functions to their new form | `true` |
70 +
71 + | Option | Description | Default |
72 + | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
73 + | `:files` | Quokka gets files from `.formatter.exs[:inputs]`. However, in some cases you may need to selectively exclude/include files you wish to still run in `mix format`, but have different behavior with Quokka. | `%{included: [], excluded: []}` (all files included, none excluded) |
74 + | `:inefficient_function_rewrites` | Rewrite inefficient functions to more efficient form | `true` |
75 + | `:reorder_configs` | Alphabetize `config` by key in `config/*.exs` files | `true` |
76 + | `:rewrite_deprecations` | Rewrite deprecated functions to their new form | `true` |
69 77
70 78 ## Credo inspired rewrites
71 79
  @@ -77,7 +85,7 @@ some additional useful details such as links to detailed documentation and if
77 85 the check can be configured further for fine tuning.
78 86
79 87 > #### `:controversial` Credo checks {: .tip}
80 - >
88 + >
81 89 > Quokka allows all `:controversial` Credo checks to be configurable. In many cases,
82 90 > a Credo check can also be disabled to prevent rewriting.
83 91
  @@ -85,50 +93,50 @@ the check can be configured further for fine tuning.
85 93
86 94 ### Credo.Check.Consistency
87 95
88 - | Credo Check | Rewrite Description | Documentation | Configurable |
89 - |-------------|-------------------|---------------|--------------|
90 - | [`.MultiAliasImportRequireUse`](https://hexdocs.pm/credo/Credo.Check.Consistency.MultiAliasImportRequireUse.html) | Expands multi-alias/import statements | [Directive Expansion](docs/module_directives.md#directive-expansion) | |
91 - | [`.ParameterPatternMatching`](https://hexdocs.pm/credo/Credo.Check.Consistency.ParameterPatternMatching.html) | Enforces consistent parameter pattern matching | [Parameter Pattern Matching](docs/styles.md#parameter-pattern-matching-consistency) | |
96 + | Credo Check | Rewrite Description | Documentation | Configurable |
97 + | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------- | ------------ |
98 + | [`.MultiAliasImportRequireUse`](https://hexdocs.pm/credo/Credo.Check.Consistency.MultiAliasImportRequireUse.html) | Expands multi-alias/import statements | [Directive Expansion](docs/module_directives.md#directive-expansion) | |
99 + | [`.ParameterPatternMatching`](https://hexdocs.pm/credo/Credo.Check.Consistency.ParameterPatternMatching.html) | Enforces consistent parameter pattern matching | [Parameter Pattern Matching](docs/styles.md#parameter-pattern-matching-consistency) | |
92 100
93 101 ### Credo.Check.Design
94 102
95 - | Credo Check | Rewrite Description | Documentation | Configurable |
96 - |-------------|-------------------|---------------|--------------|
97 - | [`.AliasUsage`](https://hexdocs.pm/credo/Credo.Check.Design.AliasUsage.html) | Extracts repeated aliases | [Alias Lifting](docs/module_directives.md#alias-lifting) | âś“ |
103 + | Credo Check | Rewrite Description | Documentation | Configurable |
104 + | ---------------------------------------------------------------------------- | ------------------------- | -------------------------------------------------------- | ------------ |
105 + | [`.AliasUsage`](https://hexdocs.pm/credo/Credo.Check.Design.AliasUsage.html) | Extracts repeated aliases | [Alias Lifting](docs/module_directives.md#alias-lifting) | âś“ |
98 106
99 107 ### Credo.Check.Readability
100 108
101 - | Credo Check | Rewrite Description | Documentation | Configurable |
102 - |-------------|-------------------|---------------|--------------|
103 - | [`.AliasOrder`](https://hexdocs.pm/credo/Credo.Check.Readability.AliasOrder.html) | Alphabetizes module directives | [Module Directives](docs/module_directives.md#directive-organization) | âś“ |
104 - | [`.BlockPipe`](https://hexdocs.pm/credo/Credo.Check.Readability.BlockPipe.html) | (En\|dis)ables piping into blocks | [Pipe Chains](docs/pipes.md#pipe-start) | âś“ |
105 - | [`.LargeNumbers`](https://hexdocs.pm/credo/Credo.Check.Readability.LargeNumbers.html) | Formats large numbers with underscores | [Number Formatting](docs/styles.md#large-base-10-numbers) | âś“ |
106 - | [`.MaxLineLength`](https://hexdocs.pm/credo/Credo.Check.Readability.MaxLineLength.html) | Enforces maximum line length | [Line Length](docs/styles.md#line-length) | âś“ |
107 - | [`.MultiAlias`](https://hexdocs.pm/credo/Credo.Check.Readability.MultiAlias.html) | Expands multi-alias statements | [Module Directives](docs/module_directives.md#directive-expansion) | âś“ |
108 - | [`.OneArityFunctionInPipe`](https://hexdocs.pm/credo/Credo.Check.Readability.OneArityFunctionInPipe.html) | Optimizes pipe chains with single arity functions | [Pipe Chains](docs/pipes.md#add-parenthesis-to-function-calls-in-pipes) | |
109 - | [`.ParenthesesOnZeroArityDefs`](https://hexdocs.pm/credo/Credo.Check.Readability.ParenthesesOnZeroArityDefs.html) | Enforces consistent function call parentheses | [Function Calls](docs/styles.md#add-parenthesis-to-0-arity-functions-and-macro-definitions) | âś“ |
110 - | [`.PipeIntoAnonymousFunctions`](https://hexdocs.pm/credo/Credo.Check.Readability.PipeIntoAnonymousFunctions.html) | Optimizes pipes with anonymous functions | [Pipe Chains](docs/pipes.md#add-then-2-when-defining-and-calling-anonymous-functions-in-pipes) | |
111 - | [`.PreferImplicitTry`](https://hexdocs.pm/credo/Credo.Check.Readability.PreferImplicitTry.html) | Simplifies try expressions | [Control Flow Macros](docs/styles.md#implicit-try) | |
112 - | [`.SinglePipe`](https://hexdocs.pm/credo/Credo.Check.Readability.SinglePipe.html) | Optimizes pipe chains | [Pipe Chains](docs/pipes.md#unpiping-single-pipes) | âś“ |
113 - | [`.StringSigils`](https://hexdocs.pm/credo/Credo.Check.Readability.StringSigils.html) | Replaces strings with sigils | [Strings to Sigils](docs/styles.md#strings-to-sigils) | |
114 - | [`.StrictModuleLayout`](https://hexdocs.pm/credo/Credo.Check.Readability.StrictModuleLayout.html) | Enforces strict module layout | [Module Directives](docs/module_directives.md#directive-organization) | âś“ |
115 - | [`.UnnecessaryAliasExpansion`](https://hexdocs.pm/credo/Credo.Check.Readability.UnnecessaryAliasExpansion.html) | Removes unnecessary alias expansions | [Module Directives](docs/module_directives.md#directive-expansion) | |
116 - | [`.WithSingleClause`](https://hexdocs.pm/credo/Credo.Check.Readability.WithSingleClause.html) | Simplifies with statements | [Control Flow Macros](docs/control_flow_macros.md#with) | |
109 + | Credo Check | Rewrite Description | Documentation | Configurable |
110 + | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------ |
111 + | [`.AliasOrder`](https://hexdocs.pm/credo/Credo.Check.Readability.AliasOrder.html) | Alphabetizes module directives | [Module Directives](docs/module_directives.md#directive-organization) | âś“ |
112 + | [`.BlockPipe`](https://hexdocs.pm/credo/Credo.Check.Readability.BlockPipe.html) | (En\|dis)ables piping into blocks | [Pipe Chains](docs/pipes.md#pipe-start) | âś“ |
113 + | [`.LargeNumbers`](https://hexdocs.pm/credo/Credo.Check.Readability.LargeNumbers.html) | Formats large numbers with underscores | [Number Formatting](docs/styles.md#large-base-10-numbers) | âś“ |
114 + | [`.MaxLineLength`](https://hexdocs.pm/credo/Credo.Check.Readability.MaxLineLength.html) | Enforces maximum line length | [Line Length](docs/styles.md#line-length) | âś“ |
115 + | [`.MultiAlias`](https://hexdocs.pm/credo/Credo.Check.Readability.MultiAlias.html) | Expands multi-alias statements | [Module Directives](docs/module_directives.md#directive-expansion) | âś“ |
116 + | [`.OneArityFunctionInPipe`](https://hexdocs.pm/credo/Credo.Check.Readability.OneArityFunctionInPipe.html) | Optimizes pipe chains with single arity functions | [Pipe Chains](docs/pipes.md#add-parenthesis-to-function-calls-in-pipes) | |
117 + | [`.ParenthesesOnZeroArityDefs`](https://hexdocs.pm/credo/Credo.Check.Readability.ParenthesesOnZeroArityDefs.html) | Enforces consistent function call parentheses | [Function Calls](docs/styles.md#add-parenthesis-to-0-arity-functions-and-macro-definitions) | âś“ |
118 + | [`.PipeIntoAnonymousFunctions`](https://hexdocs.pm/credo/Credo.Check.Readability.PipeIntoAnonymousFunctions.html) | Optimizes pipes with anonymous functions | [Pipe Chains](docs/pipes.md#add-then-2-when-defining-and-calling-anonymous-functions-in-pipes) | |
119 + | [`.PreferImplicitTry`](https://hexdocs.pm/credo/Credo.Check.Readability.PreferImplicitTry.html) | Simplifies try expressions | [Control Flow Macros](docs/styles.md#implicit-try) | |
120 + | [`.SinglePipe`](https://hexdocs.pm/credo/Credo.Check.Readability.SinglePipe.html) | Optimizes pipe chains | [Pipe Chains](docs/pipes.md#unpiping-single-pipes) | âś“ |
121 + | [`.StringSigils`](https://hexdocs.pm/credo/Credo.Check.Readability.StringSigils.html) | Replaces strings with sigils | [Strings to Sigils](docs/styles.md#strings-to-sigils) | |
122 + | [`.StrictModuleLayout`](https://hexdocs.pm/credo/Credo.Check.Readability.StrictModuleLayout.html) | Enforces strict module layout | [Module Directives](docs/module_directives.md#directive-organization) | âś“ |
123 + | [`.UnnecessaryAliasExpansion`](https://hexdocs.pm/credo/Credo.Check.Readability.UnnecessaryAliasExpansion.html) | Removes unnecessary alias expansions | [Module Directives](docs/module_directives.md#directive-expansion) | |
124 + | [`.WithSingleClause`](https://hexdocs.pm/credo/Credo.Check.Readability.WithSingleClause.html) | Simplifies with statements | [Control Flow Macros](docs/control_flow_macros.md#with) | |
117 125
118 126 ### Credo.Check.Refactor
119 127
120 - | Credo Check | Rewrite Description | Documentation | Configurable |
121 - |-------------|-------------------|---------------|--------------|
122 - | [`.CondStatements`](https://hexdocs.pm/credo/Credo.Check.Refactor.CondStatements.html) | Simplifies boolean expressions | [Control Flow Macros](docs/control_flow_macros.md#cond) | |
123 - | [`.FilterCount`](https://hexdocs.pm/credo/Credo.Check.Refactor.FilterCount.html) | Optimizes filter + count operations | [Styles](docs/styles.md#filter-count) | |
124 - | [`.MapInto`](https://hexdocs.pm/credo/Credo.Check.Refactor.MapInto.html) | Optimizes map + into operations | [Styles](docs/styles.md#map-into) | |
125 - | [`.MapJoin`](https://hexdocs.pm/credo/Credo.Check.Refactor.MapJoin.html) | Optimizes map + join operations | [Styles](docs/styles.md#map-join) | |
126 - | [`.NegatedConditionsInUnless`](https://hexdocs.pm/credo/Credo.Check.Refactor.NegatedConditionsInUnless.html) | Simplifies negated conditions in unless | [Control Flow Macros](docs/control_flow_macros.md#if-and-unless) | |
127 - | [`.NegatedConditionsWithElse`](https://hexdocs.pm/credo/Credo.Check.Refactor.NegatedConditionsWithElse.html) | Simplifies negated conditions with else | [Control Flow Macros](docs/control_flow_macros.md#negation-inversion) | |
128 - | [`.PipeChainStart`](https://hexdocs.pm/credo/Credo.Check.Refactor.PipeChainStart.html) | Optimizes pipe chain start | [Pipe Chains](docs/pipes.md#pipe-start) | |
129 - | [`.RedundantWithClauseResult`](https://hexdocs.pm/credo/Credo.Check.Refactor.RedundantWithClauseResult.html) | Removes redundant with clause results | [Control Flow Macros](docs/control_flow_macros.md#with) | |
130 - | [`.UnlessWithElse`](https://hexdocs.pm/credo/Credo.Check.Refactor.UnlessWithElse.html) | Simplifies unless with else | [Control Flow Macros](docs/control_flow_macros.md#if-and-unless) | |
131 - | [`.WithClauses`](https://hexdocs.pm/credo/Credo.Check.Refactor.WithClauses.html) | Optimizes with clauses | [Control Flow Macros](docs/control_flow_macros.md#with) | |
128 + | Credo Check | Rewrite Description | Documentation | Configurable |
129 + | ------------------------------------------------------------------------------------------------------------ | --------------------------------------- | --------------------------------------------------------------------- | ------------ |
130 + | [`.CondStatements`](https://hexdocs.pm/credo/Credo.Check.Refactor.CondStatements.html) | Simplifies boolean expressions | [Control Flow Macros](docs/control_flow_macros.md#cond) | |
131 + | [`.FilterCount`](https://hexdocs.pm/credo/Credo.Check.Refactor.FilterCount.html) | Optimizes filter + count operations | [Styles](docs/styles.md#filter-count) | |
132 + | [`.MapInto`](https://hexdocs.pm/credo/Credo.Check.Refactor.MapInto.html) | Optimizes map + into operations | [Styles](docs/styles.md#map-into) | |
133 + | [`.MapJoin`](https://hexdocs.pm/credo/Credo.Check.Refactor.MapJoin.html) | Optimizes map + join operations | [Styles](docs/styles.md#map-join) | |
134 + | [`.NegatedConditionsInUnless`](https://hexdocs.pm/credo/Credo.Check.Refactor.NegatedConditionsInUnless.html) | Simplifies negated conditions in unless | [Control Flow Macros](docs/control_flow_macros.md#if-and-unless) | |
135 + | [`.NegatedConditionsWithElse`](https://hexdocs.pm/credo/Credo.Check.Refactor.NegatedConditionsWithElse.html) | Simplifies negated conditions with else | [Control Flow Macros](docs/control_flow_macros.md#negation-inversion) | |
136 + | [`.PipeChainStart`](https://hexdocs.pm/credo/Credo.Check.Refactor.PipeChainStart.html) | Optimizes pipe chain start | [Pipe Chains](docs/pipes.md#pipe-start) | |
137 + | [`.RedundantWithClauseResult`](https://hexdocs.pm/credo/Credo.Check.Refactor.RedundantWithClauseResult.html) | Removes redundant with clause results | [Control Flow Macros](docs/control_flow_macros.md#with) | |
138 + | [`.UnlessWithElse`](https://hexdocs.pm/credo/Credo.Check.Refactor.UnlessWithElse.html) | Simplifies unless with else | [Control Flow Macros](docs/control_flow_macros.md#if-and-unless) | |
139 + | [`.WithClauses`](https://hexdocs.pm/credo/Credo.Check.Refactor.WithClauses.html) | Optimizes with clauses | [Control Flow Macros](docs/control_flow_macros.md#with) | |
132 140
133 141 <!-- tabs-close -->
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/smartrent/quokka">>}]}.
2 2 {<<"name">>,<<"quokka">>}.
3 - {<<"version">>,<<"1.0.0">>}.
3 + {<<"version">>,<<"1.1.0">>}.
4 4 {<<"description">>,
5 5 <<"A Credo-configured code-style enforcer that will just fix it for you instead of complaining">>}.
6 6 {<<"elixir">>,<<"~> 1.15">>}.
  @@ -16,8 +16,9 @@
16 16 [<<"lib">>,<<"lib/zipper.ex">>,<<"lib/quokka">>,<<"lib/quokka/config.ex">>,
17 17 <<"lib/style">>,<<"lib/style/single_node.ex">>,<<"lib/style/defs.ex">>,
18 18 <<"lib/style/configs.ex">>,<<"lib/style/blocks.ex">>,
19 - <<"lib/style/module_directives.ex">>,<<"lib/style/deprecations.ex">>,
20 - <<"lib/style/pipes.ex">>,<<"lib/style.ex">>,<<"lib/alias_env.ex">>,
21 - <<"lib/style_error.ex">>,<<"lib/styler.ex">>,<<".formatter.exs">>,
22 - <<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}.
19 + <<"lib/style/module_directives.ex">>,<<"lib/style/comment_directives.ex">>,
20 + <<"lib/style/deprecations.ex">>,<<"lib/style/pipes.ex">>,<<"lib/style.ex">>,
21 + <<"lib/alias_env.ex">>,<<"lib/style_error.ex">>,<<"lib/styler.ex">>,
22 + <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,
23 + <<"CHANGELOG.md">>]}.
23 24 {<<"build_tools">>,[<<"mix">>]}.
  @@ -22,18 +22,26 @@ defmodule Quokka.Config do
22 22 alias Credo.Check.Readability.SinglePipe
23 23 alias Credo.Check.Readability.StrictModuleLayout
24 24 alias Credo.Check.Refactor.PipeChainStart
25 + alias Quokka.Style.Blocks
26 + alias Quokka.Style.CommentDirectives
25 27 alias Quokka.Style.Configs
28 + alias Quokka.Style.Defs
29 + alias Quokka.Style.Deprecations
30 + alias Quokka.Style.ModuleDirectives
31 + alias Quokka.Style.Pipes
32 + alias Quokka.Style.SingleNode
26 33
27 34 @key __MODULE__
28 35
29 36 @styles [
30 - Quokka.Style.ModuleDirectives,
31 - Quokka.Style.Pipes,
32 - Quokka.Style.SingleNode,
33 - Quokka.Style.Defs,
34 - Quokka.Style.Blocks,
35 - Quokka.Style.Deprecations,
36 - Configs
37 + ModuleDirectives,
38 + Pipes,
39 + SingleNode,
40 + Defs,
41 + Blocks,
42 + Deprecations,
43 + Configs,
44 + CommentDirectives
37 45 ]
38 46
39 47 @stdlib ~w(
  @@ -53,15 +61,22 @@ defmodule Quokka.Config do
53 61 def set!(config) do
54 62 credo_opts = extract_configs_from_credo()
55 63
56 - lift_alias_excluded_namespaces = (credo_opts[:lift_alias_excluded_namespaces] || []) |> Enum.map(&Atom.to_string/1)
57 - lift_alias_excluded_lastnames = (credo_opts[:lift_alias_excluded_lastnames] || []) |> Enum.map(&Atom.to_string/1)
64 + lift_alias_excluded_namespaces =
65 + (credo_opts[:lift_alias_excluded_namespaces] || []) |> Enum.map(&Atom.to_string/1)
66 +
67 + lift_alias_excluded_lastnames =
68 + (credo_opts[:lift_alias_excluded_lastnames] || []) |> Enum.map(&Atom.to_string/1)
69 +
70 + inefficient_function_rewrites =
71 + if is_nil(config[:inefficient_function_rewrites]),
72 + do: true,
73 + else: config[:inefficient_function_rewrites]
74 +
75 + newline_fixes_only = if is_nil(config[:newline_fixes_only]), do: false, else: config[:newline_fixes_only]
58 76
59 77 reorder_configs =
60 78 if is_nil(config[:reorder_configs]), do: true, else: config[:reorder_configs]
61 79
62 - inefficient_function_rewrites =
63 - if is_nil(config[:inefficient_function_rewrites]), do: true, else: config[:inefficient_function_rewrites]
64 -
65 80 rewrite_deprecations =
66 81 if is_nil(config[:rewrite_deprecations]), do: true, else: config[:rewrite_deprecations]
67 82
  @@ -69,28 +84,29 @@ defmodule Quokka.Config do
69 84 strict_module_layout_order = credo_opts[:strict_module_layout_order] || default_order
70 85
71 86 :persistent_term.put(@key, %{
72 - block_pipe_flag: credo_opts[:block_pipe_flag] || false,
73 87 block_pipe_exclude: credo_opts[:block_pipe_exclude] || [],
74 - directories_included: Map.get(config[:files] || %{}, :included, []),
88 + block_pipe_flag: credo_opts[:block_pipe_flag] || false,
75 89 directories_excluded: Map.get(config[:files] || %{}, :excluded, []),
90 + directories_included: Map.get(config[:files] || %{}, :included, []),
76 91 inefficient_function_rewrites: inefficient_function_rewrites,
77 92 large_numbers_gt: credo_opts[:large_numbers_gt] || :infinity,
78 - line_length: credo_opts[:line_length] || 98,
79 - pipe_chain_start_flag: credo_opts[:pipe_chain_start_flag] || false,
80 - pipe_chain_start_excluded_functions: credo_opts[:pipe_chain_start_excluded_functions] || [],
81 - pipe_chain_start_excluded_argument_types: credo_opts[:pipe_chain_start_excluded_argument_types] || [],
82 - reorder_configs: reorder_configs,
83 - rewrite_deprecations: rewrite_deprecations,
84 93 lift_alias: credo_opts[:lift_alias] || false,
85 94 lift_alias_depth: credo_opts[:lift_alias_depth] || 0,
86 - lift_alias_excluded_namespaces: MapSet.new(lift_alias_excluded_namespaces ++ @stdlib),
87 95 lift_alias_excluded_lastnames: MapSet.new(lift_alias_excluded_lastnames ++ @stdlib),
96 + lift_alias_excluded_namespaces: MapSet.new(lift_alias_excluded_namespaces ++ @stdlib),
88 97 lift_alias_frequency: credo_opts[:lift_alias_frequency] || 0,
98 + line_length: credo_opts[:line_length] || 98,
99 + newline_fixes_only: newline_fixes_only,
100 + pipe_chain_start_excluded_argument_types: credo_opts[:pipe_chain_start_excluded_argument_types] || [],
101 + pipe_chain_start_excluded_functions: credo_opts[:pipe_chain_start_excluded_functions] || [],
102 + pipe_chain_start_flag: credo_opts[:pipe_chain_start_flag] || false,
103 + reorder_configs: reorder_configs,
104 + rewrite_deprecations: rewrite_deprecations,
89 105 rewrite_multi_alias: credo_opts[:rewrite_multi_alias] || false,
90 106 single_pipe_flag: credo_opts[:single_pipe_flag] || false,
91 107 sort_order: credo_opts[:sort_order] || :alpha,
92 108 strict_module_layout_order: strict_module_layout_order ++ (default_order -- strict_module_layout_order),
93 - zero_arity_parens: credo_opts[:zero_arity_parens] || false,
109 + zero_arity_parens: credo_opts[:zero_arity_parens] || false
94 110 })
95 111 end
96 112
  @@ -106,11 +122,18 @@ defmodule Quokka.Config do
106 122 end
107 123
108 124 def get_styles() do
109 - styles_to_remove =
110 - for {module, flag_name} <- [{Configs, :reorder_configs}, {Quokka.Style.Deprecations, :rewrite_deprecations}],
111 - do: (if get(flag_name), do: nil, else: module)
125 + if get(:newline_fixes_only) do
126 + [Defs]
127 + else
128 + styles_to_remove =
129 + for {module, flag_name} <- [
130 + {Configs, :reorder_configs},
131 + {Deprecations, :rewrite_deprecations}
132 + ],
133 + do: if(get(flag_name), do: nil, else: module)
112 134
113 - @styles -- styles_to_remove
135 + @styles -- styles_to_remove
136 + end
114 137 end
115 138
116 139 def sort_order() do
  @@ -205,7 +228,11 @@ defmodule Quokka.Config do
205 228 end
206 229
207 230 defp extract_configs_from_credo() do
208 - Enum.reduce(read_credo_config().checks, %{}, fn
231 + case read_credo_config().checks do
232 + checks when is_list(checks) -> checks
233 + checks -> Map.get(checks, :enabled, [])
234 + end
235 + |> Enum.reduce(%{}, fn
209 236 {AliasOrder, opts}, acc when is_list(opts) ->
210 237 Map.put(acc, :sort_order, opts[:sort_method])
  @@ -60,7 +60,7 @@ defmodule Quokka.Style do
60 60 @doc "Traverses an ast node, updating all nodes' meta with `meta_fun`"
61 61 def update_all_meta(node, meta_fun), do: Macro.prewalk(node, &Macro.update_meta(&1, meta_fun))
62 62
63 - # useful for comparing AST without meta (line numbers, etc) interfering
63 + @doc "prewalks ast and sets all meta to `nil`. useful for comparing AST without meta (line numbers, etc) interfering"
64 64 def without_meta(ast), do: update_all_meta(ast, fn _ -> nil end)
65 65
66 66 @doc """
  @@ -84,6 +84,9 @@ defmodule Quokka.Style do
84 84 end
85 85 end
86 86
87 + def do_block?([{{:__block__, _, [:do]}, _body} | _]), do: true
88 + def do_block?(_), do: false
89 +
87 90 @doc """
88 91 Returns a zipper focused on the nearest node where additional nodes can be inserted (a "block").
89 92
  @@ -109,7 +112,9 @@ defmodule Quokka.Style do
109 112 # give it a block parent, then step back to the child - we can insert next to it now that it's in a block
110 113 defp wrap_in_block(zipper) do
111 114 zipper
112 - |> Zipper.update(fn {_, meta, _} = node -> {:__block__, Keyword.take(meta, [:line]), [node]} end)
115 + |> Zipper.update(fn {_, meta, _} = node ->
116 + {:__block__, Keyword.take(meta, [:line]), [node]}
117 + end)
113 118 |> Zipper.down()
114 119 end
115 120
  @@ -145,7 +150,7 @@ defmodule Quokka.Style do
145 150 comments
146 151 |> Enum.map(fn comment ->
147 152 if delta = Enum.find_value(shifts, fn {range, delta} -> comment.line in range && delta end) do
148 - %{comment | line: comment.line + delta}
153 + %{comment | line: max(comment.line + delta, 1)}
149 154 else
150 155 comment
151 156 end
  @@ -161,84 +166,134 @@ defmodule Quokka.Style do
161 166 def reset_newlines(nodes), do: reset_newlines(nodes, [])
162 167
163 168 def reset_newlines([node], acc), do: Enum.reverse([set_newlines(node, 2) | acc])
169 +
164 170 def reset_newlines([node | nodes], acc), do: reset_newlines(nodes, [set_newlines(node, 1) | acc])
165 171
166 172 defp set_newlines({directive, meta, children}, newline) do
167 - updated_meta = Keyword.update(meta, :end_of_expression, [newlines: newline], &Keyword.put(&1, :newlines, newline))
173 + updated_meta =
174 + Keyword.update(
175 + meta,
176 + :end_of_expression,
177 + [newlines: newline],
178 + &Keyword.put(&1, :newlines, newline)
179 + )
180 +
168 181 {directive, updated_meta, children}
169 182 end
170 183
171 - @doc """
172 - "Fixes" the line numbers of nodes who have had their orders changed via sorting or other methods.
173 - This "fix" simply ensures that comments don't get wrecked as part of us moving AST nodes willy-nilly.
184 + def max_line([_ | _] = list), do: list |> List.last() |> max_line()
174 185
175 - The fix is rather naive, and simply enforces the following property on the code:
176 - A given node must have a line number less than the following node.
177 - Et voila! Comments behave much better.
186 + def max_line(ast) do
187 + meta =
188 + case ast do
189 + {_, meta, _} ->
190 + meta
178 191
179 - ## In Detail
192 + _ ->
193 + []
194 + end
180 195
181 - For example, given document
196 + if max_line = meta[:closing][:line] do
197 + max_line
198 + else
199 + {_, max_line} =
200 + Macro.prewalk(ast, 0, fn
201 + {_, meta, _} = ast, max -> {ast, max(meta[:line] || max, max)}
202 + ast, max -> {ast, max}
203 + end)
182 204
183 - 1: defmodule ...
184 - 2: alias B
185 - 3: # this is foo
186 - 4: def foo ...
187 - 5: alias A
188 -
189 - Sorting aliases the ast node for would put `alias A` (line 5) before `alias B` (line 2).
190 -
191 - 1: defmodule ...
192 - 5: alias A
193 - 2: alias B
194 - 3: # this is foo
195 - 4: def foo ...
196 -
197 - Elixir's document algebra would then encounter `line: 5` and immediately dump all comments with `line <= 5`,
198 - meaning after running through the formatter we'd end up with
199 -
200 - 1: defmodule
201 - 2: # hi
202 - 3: # this is foo
203 - 4: alias A
204 - 5: alias B
205 - 6:
206 - 7: def foo ...
207 -
208 - This function fixes that by seeing that `alias A` has a higher line number than its following sibling `alias B` and so
209 - updates `alias A`'s line to be preceding `alias B`'s line.
210 -
211 - Running the results of this function through the formatter now no longer dumps the comments prematurely
212 -
213 - 1: defmodule ...
214 - 2: alias A
215 - 3: alias B
216 - 4: # this is foo
217 - 5: def foo ...
218 - """
219 - def fix_line_numbers(nodes, nil), do: fix_line_numbers(nodes, 999_999)
220 - def fix_line_numbers(nodes, {_, meta, _}), do: fix_line_numbers(nodes, meta[:line])
221 - def fix_line_numbers(nodes, max), do: nodes |> Enum.reverse() |> do_fix_lines(max, [])
222 -
223 - defp do_fix_lines([], _, acc), do: acc
224 -
225 - defp do_fix_lines([{_, meta, _} = node | nodes], max, acc) do
226 - line = meta[:line]
227 -
228 - # the -2 is just an ugly hack to leave room for one-liner comments and not hijack them.
229 - if line > max,
230 - do: do_fix_lines(nodes, max, [shift_line(node, max - line - 2) | acc]),
231 - else: do_fix_lines(nodes, line, [node | acc])
205 + max_line
206 + end
232 207 end
233 208
234 - # @TODO can i shortcut and just return end_of_expression[:line] when it's available?
235 - def max_line(ast) do
236 - {_, max_line} =
237 - Macro.prewalk(ast, 0, fn
238 - {_, meta, _} = ast, max -> {ast, max(meta[:line] || max, max)}
239 - ast, max -> {ast, max}
240 - end)
209 + def order_line_meta_and_comments(nodes, comments, first_line) do
210 + {nodes, comments, node_comments} = fix_lines(nodes, comments, first_line, [], [])
211 + {nodes, Enum.sort_by(comments ++ node_comments, & &1.line)}
212 + end
241 213
242 - max_line
214 + defp fix_lines([], comments, _, node_acc, c_acc), do: {Enum.reverse(node_acc), comments, c_acc}
215 +
216 + defp fix_lines([node | nodes], comments, start_line, n_acc, c_acc) do
217 + meta = meta(node)
218 + line = meta[:line]
219 + last_line = meta[:end_of_expression][:line] || max_line(node)
220 +
221 + {node, node_comments, comments} =
222 + if start_line == line do
223 + {node, [], comments}
224 + else
225 + {mine, comments} = comments_for_lines(comments, line, last_line)
226 + line_with_comments = (List.first(mine)[:line] || line) - (List.first(mine)[:previous_eol_count] || 1) + 1
227 +
228 + if line_with_comments == start_line do
229 + {node, mine, comments}
230 + else
231 + shift = start_line - line_with_comments
232 + # fix the node's line
233 + node = shift_line(node, shift)
234 + # fix the comment's line
235 + mine = Enum.map(mine, &%{&1 | line: &1.line + shift})
236 + {node, mine, comments}
237 + end
238 + end
239 +
240 + meta = meta(node)
241 + # @TODO what about comments that were free floating between blocks? i'm just ignoring them and maybe always will...
242 + # kind of just want to shove them to the end though, so that they don't interrupt existing stanzas.
243 + # i think that's accomplishable by doing a final call above that finds all comments in the comments list that weren't moved
244 + # and which are in the range of start..finish and sets their lines to finish!
245 + last_line = meta[:end_of_expression][:line] || max_line(node)
246 + last_line = (meta[:end_of_expression][:newlines] || 1) + last_line
247 + fix_lines(nodes, comments, last_line, [node | n_acc], node_comments ++ c_acc)
248 + end
249 +
250 + # typical node
251 + def meta({_, meta, _}), do: meta
252 + # kwl tuple ala a: :b
253 + def meta({{_, meta, _}, _}), do: meta
254 + def meta(_), do: nil
255 +
256 + @doc """
257 + Returns all comments "for" a node, including on the line before it.
258 + see `comments_for_lines` for more
259 + """
260 + def comments_for_node({_, m, _} = node, comments) do
261 + last_line = m[:end_of_expression][:line] || max_line(node)
262 + comments_for_lines(comments, m[:line], last_line)
263 + end
264 +
265 + @doc """
266 + Gets all comments in range start_line..last_line, and any comments immediately before start_line.s
267 +
268 + 1. code
269 + 2. # a
270 + 3. # b
271 + 4. code # c
272 + 5. # d
273 + 6. code
274 + 7. # e
275 +
276 + here, comments_for_lines(comments, 4, 6) is "a", "b", "c", "d"
277 + """
278 + def comments_for_lines(comments, start_line, last_line) do
279 + comments |> Enum.reverse() |> comments_for_lines(start_line, last_line, [], [])
280 + end
281 +
282 + defp comments_for_lines(reversed_comments, start, last, match, acc)
283 +
284 + defp comments_for_lines([], _, _, match, acc), do: {Enum.reverse(match), acc}
285 +
286 + defp comments_for_lines([%{line: line} = comment | rev_comments], start, last, match, acc) do
287 + cond do
288 + # after our block - no match
289 + line > last -> comments_for_lines(rev_comments, start, last, match, [comment | acc])
290 + # after start, before last -- it's a match!
291 + line >= start -> comments_for_lines(rev_comments, start, last, [comment | match], acc)
292 + # this is a comment immediately before start, which means it's modifying this block...
293 + # we count that as a match, and look above it to see if it's a multiline comment
294 + line == start - 1 -> comments_for_lines(rev_comments, start - 1, last, [comment | match], acc)
295 + # comment before start - we've thus iterated through all comments which could be in our range
296 + true -> {match, Enum.reverse(rev_comments, [comment | acc])}
297 + end
243 298 end
244 299 end
Loading more files…