Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp docs #15

Merged
merged 7 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 86 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,17 @@

# Quokka

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 to fit its style rules.

Quokka is a fork of [Styler](https://github.com/adobe/styler) that checks the credo config to determine which rules to rewrite.

## Features

- 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
- [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
- [reorders configuration in config files](docs/configs.md)
- [expands multi-alias/import statements](docs/module_directives.md#directive-expansion)
- [enforces consistent function call parentheses](docs/function_calls.md)
- [ensures consistent spacing around operators](docs/operators.md)
- [formats documentation comments](docs/docs.md)
- [removes unnecessary parentheses](docs/parentheses.md)
- [simplifies boolean expressions](docs/boolean_simplification.md)
- [enforces consistent module attribute usage](docs/module_attributes.md)
- [formats and organizes typespecs](docs/typespecs.md)
- ... and many more style improvements

[See our Rewrites documentation on hexdocs](https://hexdocs.pm/quokka/styles.html)
<img src="docs/assets/quokka.png" alt="A happy quokka with style" width="300"/>

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.

> #### WARNING {: .warning}
> Quokka can change the behavior of your program!
>
> In some cases, this can introduce bugs. It goes without saying, but look over your changes before committing to main :)
>
> We recommend making changes in small chunks until all of the more dangerous
> changes has been safely committed to the codebase

## Installation

Expand All @@ -54,11 +38,13 @@ Then add `Quokka` as a plugin to your `.formatter.exs` file

And that's it! Now when you run `mix format` you'll also get the benefits of Quokka's Stylish Stylings.

**Speed**: Expect the first run to take some time as `Quokka` rewrites violations of styles and bottlenecks on disk I/O. Subsequent formats formats won't take noticeably more time.
**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.

### Configuration

Quokka can be configured in your `.formatter.exs` file
Quokka primarily relies on the configurations of `.formatter.exs` and `Credo` (if available).
However, there are some Quokka specific options that can also be specified
in `.formatter.exs` to fine tune your setup:

```elixir
[
Expand All @@ -74,23 +60,77 @@ Quokka can be configured in your `.formatter.exs` file
]
]
```

Quokka has several configuration options:

- `:files`, which controls which files Quokka will format. This is `%{included: [], excluded: []}` by default.
- `:inefficient_function_rewrites`, which controls whether or not Quokka will rewrite deprecated functions to their new form. This is true by default.
- `:reorder_configs`, which controls whether or not the configs in your `config/*.exs` files are alphabetized. This is true by default.
- `:rewrite_deprecations`, which controls whether or not Quokka will rewrite deprecated functions to their new form. This is true by default.

## WARNING: Quokka 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 :)

Some ways Quokka can change your program:

- [`with` statement rewrites](https://github.com/adobe/elixir-styler/issues/186)
- [config file sorting](https://hexdocs.pm/quokka/mix_configs.html#this-can-break-your-program)
- and likely other ways. stay safe out there!
| Option | Description | Default |
| --- | --- | --- |
| `: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) |
| `:inefficient_function_rewrites` | Rewrite inefficient functions to more efficient form | `true` |
| `:reorder_configs` | Alphabetize `config` by key in `config/*.exs` files | `true` |
| `:rewrite_deprecations` | Rewrite deprecated functions to their new form | `true` |

## Credo inspired rewrites

The power of Quokka comes from utilizing the opinions you've already made with
Credo and going one step further to attempt rewriting them for you.

Below is a general overall of many Credo checks Quokka attempts to handle and
some additional useful details such as links to detailed documentation and if
the check can be configured further for fine tuning.

> #### `:controversial` Credo checks {: .tip}
>
> Quokka allows all `:controversial` Credo checks to be configurable. In many cases,
> a Credo check can also be disabled to prevent rewriting.

<!-- tabs-open -->

### Credo.Check.Consistency

| Credo Check | Rewrite Description | Documentation | Configurable |
|-------------|-------------------|---------------|--------------|
| [`.MultiAliasImportRequireUse`](https://hexdocs.pm/credo/Credo.Check.Consistency.MultiAliasImportRequireUse.html) | Expands multi-alias/import statements | [Directive Expansion](docs/module_directives.md#directive-expansion) | |
| [`.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) | |

### Credo.Check.Design

| Credo Check | Rewrite Description | Documentation | Configurable |
|-------------|-------------------|---------------|--------------|
| [`.AliasUsage`](https://hexdocs.pm/credo/Credo.Check.Design.AliasUsage.html) | Extracts repeated aliases | [Alias Lifting](docs/module_directives.md#alias-lifting) | ✓ |

### Credo.Check.Readability

| Credo Check | Rewrite Description | Documentation | Configurable |
|-------------|-------------------|---------------|--------------|
| [`.AliasOrder`](https://hexdocs.pm/credo/Credo.Check.Readability.AliasOrder.html) | Alphabetizes module directives | [Module Directives](docs/module_directives.md#directive-organization) | ✓ |
| [`.BlockPipe`](https://hexdocs.pm/credo/Credo.Check.Readability.BlockPipe.html) | (En\|dis)ables piping into blocks | [Pipe Chains](docs/pipes.md#pipe-start) | ✓ |
| [`.LargeNumbers`](https://hexdocs.pm/credo/Credo.Check.Readability.LargeNumbers.html) | Formats large numbers with underscores | [Number Formatting](docs/styles.md#large-base-10-numbers) | ✓ |
| [`.MaxLineLength`](https://hexdocs.pm/credo/Credo.Check.Readability.MaxLineLength.html) | Enforces maximum line length | [Line Length](docs/styles.md#line-length) | ✓ |
| [`.MultiAlias`](https://hexdocs.pm/credo/Credo.Check.Readability.MultiAlias.html) | Expands multi-alias statements | [Module Directives](docs/module_directives.md#directive-expansion) | ✓ |
| [`.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) | |
| [`.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) | ✓ |
| [`.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) | |
| [`.PreferImplicitTry`](https://hexdocs.pm/credo/Credo.Check.Readability.PreferImplicitTry.html) | Simplifies try expressions | [Control Flow Macros](docs/styles.md#implicit-try) | |
| [`.SinglePipe`](https://hexdocs.pm/credo/Credo.Check.Readability.SinglePipe.html) | Optimizes pipe chains | [Pipe Chains](docs/pipes.md#unpiping-single-pipes) | ✓ |
| [`.StringSigils`](https://hexdocs.pm/credo/Credo.Check.Readability.StringSigils.html) | Replaces strings with sigils | [Strings to Sigils](docs/styles.md#strings-to-sigils) | |
| [`.StrictModuleLayout`](https://hexdocs.pm/credo/Credo.Check.Readability.StrictModuleLayout.html) | Enforces strict module layout | [Module Directives](docs/module_directives.md#directive-organization) | ✓ |
| [`.UnnecessaryAliasExpansion`](https://hexdocs.pm/credo/Credo.Check.Readability.UnnecessaryAliasExpansion.html) | Removes unnecessary alias expansions | [Module Directives](docs/module_directives.md#directive-expansion) | |
| [`.WithSingleClause`](https://hexdocs.pm/credo/Credo.Check.Readability.WithSingleClause.html) | Simplifies with statements | [Control Flow Macros](docs/control_flow_macros.md#with) | |

### Credo.Check.Refactor

| Credo Check | Rewrite Description | Documentation | Configurable |
|-------------|-------------------|---------------|--------------|
| [`.CondStatements`](https://hexdocs.pm/credo/Credo.Check.Refactor.CondStatements.html) | Simplifies boolean expressions | [Control Flow Macros](docs/control_flow_macros.md#cond) | |
| [`.FilterCount`](https://hexdocs.pm/credo/Credo.Check.Refactor.FilterCount.html) | Optimizes filter + count operations | [Styles](docs/styles.md#filter-count) | |
| [`.MapInto`](https://hexdocs.pm/credo/Credo.Check.Refactor.MapInto.html) | Optimizes map + into operations | [Styles](docs/styles.md#map-into) | |
| [`.MapJoin`](https://hexdocs.pm/credo/Credo.Check.Refactor.MapJoin.html) | Optimizes map + join operations | [Styles](docs/styles.md#map-join) | |
| [`.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) | |
| [`.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) | |
| [`.PipeChainStart`](https://hexdocs.pm/credo/Credo.Check.Refactor.PipeChainStart.html) | Optimizes pipe chain start | [Pipe Chains](docs/pipes.md#pipe-start) | |
| [`.RedundantWithClauseResult`](https://hexdocs.pm/credo/Credo.Check.Refactor.RedundantWithClauseResult.html) | Removes redundant with clause results | [Control Flow Macros](docs/control_flow_macros.md#with) | |
| [`.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) | |
| [`.WithClauses`](https://hexdocs.pm/credo/Credo.Check.Refactor.WithClauses.html) | Optimizes with clauses | [Control Flow Macros](docs/control_flow_macros.md#with) | |

<!-- tabs-close -->

## License

Expand Down
Binary file added docs/assets/quokka.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 17 additions & 84 deletions docs/control_flow_macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,6 @@
Elixir's Kernel documentation refers to these structures as "macros for control-flow".
We often refer to them as "blocks" in our changelog, which is a much worse name, to be sure.

You're likely here just to see what Quokka does, in which case, please [click here to skip](#if-and-unless) the following manifesto on our philosophy regarding the usage of these macros.

## Which Control Flow Macro Should I Use?

The number of "blocks" in Elixir means there are many ways to write semantically equivalent code, often leaving developers [in the dark as to which structure they should use.](https://www.reddit.com/r/elixir/comments/1ctbtcl/i_am_completely_lost_when_it_comes_to_which/)

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 `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`).

### 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...

> `with` great power comes great responsibility
>
> - 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.

Thus, `with` is the control-flow structure of last resort. We advocate that `with` **should only be used when more basic expressions do not suffice or become overly verbose**. As for verbosity, we subscribe to the [Chris Keathley school of thought](https://www.youtube.com/watch?v=l-8ghbdRB1w) that judicious nesting of control flow blocks within a function isn't evil and more-often-than-not is superior to spreading implementation over many small single-use functions. We'd even go so far as to suggest that cyclomatic complexity is an inexact measure of code quality, with more than a few false negatives and many false positives.

`with` is a great way to unnest multiple `case` statements when every failure branch of those statements results in the same error. This is easily and succinctly expressed with `with`'s `else` block: `else (_ -> :error)`. As Keathley says though, [Avoid Else In With Blocks](https://keathley.io/blog/good-and-bad-elixir.html#avoid-else-in-with-blocks). Having multiple else clauses "means that the error conditions matter. Which means that you don’t want `with` at all. You want `case`."

It's acceptable to use one-line `with` statements (eg `with {:ok, _} <- Repo.update(changeset), do: :ok`) to signify that other branches are uninteresting or unmodified by your code, but ultimately that can hide the possible returns of a function from the reader, making it more onerous to debug all possible branches of the code in their mental model of the function. In other words, ideally all function calls in a `with` statement head have obvious error types for the reader, leaving their omission in the code acceptable as the reader feels no need to investigate further. The example at the start of this paragraph with an `Ecto.Repo` call is a good example, as most developers in a codebase using Ecto are expected to be familiar with its basic API.

Using `case` rather than `with` for branches with unusual failure types can help document code as well as save the reader time in tracking down types. For example, replacing the following with a `with` statement that only matched against the `{:ok, _}` tuple would hide from readers that an atypically-shaped 3-tuple is returned when things go wrong.

```elixir
case some_http_call() do
{:ok, _response} -> :ok
{:error, http_error, response} -> {:error, http_error, response}
end
```

## `if` and `unless`

Quokka removes `else: nil` clauses:
Expand All @@ -56,19 +13,26 @@ if a, do: b, else: nil
if a, do: b
```

Quokka removes `unless` since it is being deprecated in Elixir 1.18. This implicitly addresses [`Credo.Check.Refactor.NegatedConditionsInUnless`](https://hexdocs.pm/credo/Credo.Check.Refactor.NegatedConditionsInUnless.html) and [`Credo.Check.Refactor.NegatedConditionsWithElse`](https://hexdocs.pm/credo/Credo.Check.Refactor.NegatedConditionsWithElse.html).

```elixir
# Given:
unless a, do: b
# Styled:
if a, do: b
```

### Negation Inversion

Quokka removes negators in the head of `if` and `unless` statements by "inverting" the statement.
The following operators are considered "negators": `!`, `not`, `!=`, `!==`
This addresses [`Credo.Check.Refactor.NegatedConditionsWithElse`](https://hexdocs.pm/credo/Credo.Check.Refactor.NegatedConditionsWithElse.html). This is not configurable.

Quokka 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
Expand All @@ -94,42 +58,10 @@ if !!x, do: y
if x, do: y
```

## `case`

### "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 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, Quokka leaves the code with better style, trumping obscure exception design :)

```elixir
# Quokka 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

# 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:

```elixir
case foo do
true -> :ok
false -> :error
other -> raise "expected `true` or `false`, got: #{inspect other}"
end
```

## `cond`

This addresses [`Credo.Check.Refactor.CondStatements`](https://hexdocs.pm/credo/Credo.Check.Refactor.CondStatements.html). This is not configurable.

Quokka has only one `cond` statement rewrite: replace 2-clause statements with `if` statements.

```elixir
Expand All @@ -148,7 +80,8 @@ end

## `with`

`with` statements are extremely expressive. Quokka tries to remove any unnecessary complexity from them in the following ways.
This addresses [`Credo.Check.Readability.WithSingleClause`](https://hexdocs.pm/credo/Credo.Check.Readability.WithSingleClause.html), [`Credo.Check.Refactor.RedundantWithClauseResult`](https://hexdocs.pm/credo/Credo.Check.Refactor.RedundantWithClauseResult.html), and [`Credo.Check.Refactor.WithClauses`](https://hexdocs.pm/credo/Credo.Check.Refactor.WithClauses.html). This is not configurable.


### Remove Identity Else Clause

Expand Down
Loading