Skip to content

Commit

Permalink
Revamp docs
Browse files Browse the repository at this point in the history
  • Loading branch information
emkguts committed Feb 7, 2025
1 parent b2b949b commit 983ad19
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 142 deletions.
70 changes: 43 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,47 @@

# 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
<img src="docs/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.

## Rewrites

| Credo Check | Rewrite Description | Documentation | Configurable |
|-------------|-------------------|---------------|--------------|
| [Credo.Check.Consistency.MultiAliasImportRequireUse](https://hexdocs.pm/credo/Credo.Check.Consistency.MultiAliasImportRequireUse.html) | Expands multi-alias/import statements | [Directive Expansion](docs/module_directives.md#directive-expansion) | |
| [Credo.Check.Consistency.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.AliasUsage](https://hexdocs.pm/credo/Credo.Check.Design.AliasUsage.html) | Extracts repeated aliases | [Alias Lifting](docs/module_directives.md#alias-lifting) ||
| [Credo.Check.Readability.AliasOrder](https://hexdocs.pm/credo/Credo.Check.Readability.AliasOrder.html) | Alphabetizes module directives | [Module Directives](docs/module_directives.md#directive-organization) ||
| [Credo.Check.Readability.BlockPipe](https://hexdocs.pm/credo/Credo.Check.Readability.BlockPipe.html) | (En\|dis)ables piping into blocks | [Pipe Chains](docs/pipes.md#pipe-start) ||
| [Credo.Check.Readability.LargeNumbers](https://hexdocs.pm/credo/Credo.Check.Readability.LargeNumbers.html) | Formats large numbers with underscores | [Number Formatting](docs/styles.md#large-base-10-numbers) ||
| [Credo.Check.Readability.MaxLineLength](https://hexdocs.pm/credo/Credo.Check.Readability.MaxLineLength.html) | Enforces maximum line length | [Line Length](docs/styles.md#line-length) ||
| [Credo.Check.Readability.MultiAlias](https://hexdocs.pm/credo/Credo.Check.Readability.MultiAlias.html) | Expands multi-alias statements | [Module Directives](docs/module_directives.md#directive-expansion) ||
| [Credo.Check.Readability.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) | |
| [Credo.Check.Readability.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) ||
| [Credo.Check.Readability.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) | |
| [Credo.Check.Readability.PreferImplicitTry](https://hexdocs.pm/credo/Credo.Check.Readability.PreferImplicitTry.html) | Simplifies try expressions | [Control Flow Macros](docs/styles.md#implicit-try) | |
| [Credo.Check.Readability.SinglePipe](https://hexdocs.pm/credo/Credo.Check.Readability.SinglePipe.html) | Optimizes pipe chains | [Pipe Chains](docs/pipes.md#unpiping-single-pipes) ||
| [Credo.Check.Readability.StringSigils](https://hexdocs.pm/credo/Credo.Check.Readability.StringSigils.html) | Replaces strings with sigils | [Strings to Sigils](docs/styles.md#strings-to-sigils) | |
| [Credo.Check.Readability.StrictModuleLayout](https://hexdocs.pm/credo/Credo.Check.Readability.StrictModuleLayout.html) | Enforces strict module layout | [Module Directives](docs/module_directives.md#directive-organization) ||
| [Credo.Check.Readability.UnnecessaryAliasExpansion](https://hexdocs.pm/credo/Credo.Check.Readability.UnnecessaryAliasExpansion.html) | Removes unnecessary alias expansions | [Module Directives](docs/module_directives.md#directive-expansion) | |
| [Credo.Check.Readability.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.CondStatements](https://hexdocs.pm/credo/Credo.Check.Refactor.CondStatements.html) | Simplifies boolean expressions | [Control Flow Macros](docs/control_flow_macros.md#cond) | |
| [Credo.Check.Refactor.FilterCount](https://hexdocs.pm/credo/Credo.Check.Refactor.FilterCount.html) | Optimizes filter + count operations | [Styles](docs/styles.md#filter-count) | |
| [Credo.Check.Refactor.MapInto](https://hexdocs.pm/credo/Credo.Check.Refactor.MapInto.html) | Optimizes map + into operations | [Styles](docs/styles.md#map-into) | |
| [Credo.Check.Refactor.MapJoin](https://hexdocs.pm/credo/Credo.Check.Refactor.MapJoin.html) | Optimizes map + join operations | [Styles](docs/styles.md#map-join) | |
| [Credo.Check.Refactor.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) | |
| [Credo.Check.Refactor.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) | |
| [Credo.Check.Refactor.PipeChainStart](https://hexdocs.pm/credo/Credo.Check.Refactor.PipeChainStart.html) | Optimizes pipe chain start | [Pipe Chains](docs/pipes.md#pipe-start) | |
| [Credo.Check.Refactor.RedundantWithClauseResult](https://hexdocs.pm/credo/Credo.Check.Refactor.RedundantWithClauseResult.html) | Removes redundant with clause results | [Control Flow Macros](docs/control_flow_macros.md#with) | |
| [Credo.Check.Refactor.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) | |
| [Credo.Check.Refactor.WithClauses](https://hexdocs.pm/credo/Credo.Check.Refactor.WithClauses.html) | Optimizes with clauses | [Control Flow Macros](docs/control_flow_macros.md#with) | |
| - | Alphabetizes configuration in config files | [Config Files](docs/mix_configs.md) ||
| - | Rewrites deprecated functions | [Deprecation Rewrites](docs/styles.md#elixir-deprecation-rewrites) ||
| - | Miscellaneous inefficient function calls | [Inefficient Function Rewrites](docs/styles.md#inefficient-function-rewrites) ||
| - | Miscellaneous case and with rewrites !! CHANGES FUNCTIONALITY !! | [Case Rewrites](docs/control_flow_macros.md#case) ||
| - | Piped function optimizations | [Pipe Chains](docs/pipes.md#piped-function-optimizations) | |


[See our Rewrites documentation on hexdocs](https://hexdocs.pm/quokka/styles.html)

Expand Down Expand Up @@ -82,14 +98,14 @@ Quokka has several configuration options:
- `: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!
## 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 :)

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)
- [config file sorting](https://hexdocs.pm/styler/mix_configs.html#this-can-break-your-program) -- But this can be disabled.
- and likely other ways. stay safe out there!

## License
Expand Down
67 changes: 17 additions & 50 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 Down Expand Up @@ -130,6 +94,8 @@ 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 +114,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
47 changes: 0 additions & 47 deletions docs/credo.md

This file was deleted.

2 changes: 2 additions & 0 deletions docs/mix_configs.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Mix Configs

This can be enabled or disabled by setting the `:reorder_configs` option in your `.formatter.exs` file. See the [README](../README.md#configuration) for more information.

Mix Config files have their config stanzas sorted. Similar to the sorting of aliases, this delivers consistency to an otherwise arbitrary world, and can even help catch bugs like configuring the same key multiple times.

A file is considered a config file if
Expand Down
Loading

0 comments on commit 983ad19

Please sign in to comment.