SiriusStarr / elm-review-no-unsorted / NoUnsortedCases

Review Rule

rule : RuleConfig -> Review.Rule.Rule

Reports case patterns that are not in the "proper" order.

🔧 Running with --fix will automatically sort the patterns.

The proper order of custom types is the order in which they are defined in your source files, and the order of other patterns may be specified in the rule configuration. See the Configuration section below for more information.

config =
    [ NoUnsortedCases.rule NoUnsortedCases.defaults
    ]

Fail

type Custom
    = Foo
    | Bar
    | Baz

func1 c =
    case c of
        Bar ->
            "bar"

        Foo ->
            "foo"

        Baz ->
            "baz"

func2 cs =
    case cs of
        [ Bar ] ->
            "bar"

        [ Foo ] ->
            "foo"

        [ Foo, Foo ] ->
            "foofoo"

        [ Baz ] ->
            "baz"

        _ ->
            "other"

func3 c =
    case c of
        Nothing ->
            ""

        Just Bar ->
            "bar"

        Just Foo ->
            "foo"

        Just Baz ->
            "baz"

func4 c1 c2 =
    case ( c1, c2 ) of
        ( Foo, Baz ) ->
            "foo baz"

        ( Foo, Bar ) ->
            "foo bar"

        ( Bar, Foo ) ->
            "bar foo"

        ( Baz, Foo ) ->
            "baz foo"

        _ ->
            "other"

Success

type Custom
    = Foo
    | Bar
    | Baz

func1 c =
    case c of
        Foo ->
            "foo"

        Bar ->
            "bar"

        Baz ->
            "baz"

func2 cs =
    case cs of
        [ Foo ] ->
            "foo"

        [ Foo, Foo ] ->
            "foofoo"

        [ Bar ] ->
            "bar"

        [ Baz ] ->
            "baz"

        _ ->
            "other"

func3 c =
    case c of
        Just Foo ->
            "foo"

        Just Bar ->
            "bar"

        Just Baz ->
            "baz"

        Nothing ->
            ""

func4 c1 c2 =
    case ( c1, c2 ) of
        ( Foo, Bar ) ->
            "foo bar"

        ( Foo, Baz ) ->
            "foo baz"

        ( Bar, Foo ) ->
            "bar foo"

        ( Baz, Foo ) ->
            "baz foo"

        _ ->
            "other"

When (not) to enable this rule

This rule is useful when you want to ensure that you pattern match in a consistent, predictable order, that is consistent with the order in which a type was defined, as well as ensuring (optionally) that literal patterns and the like are sorted.

This rule is not useful when you want to be able to write case patterns in different orders throughout your codebase, e.g. if you want to emphasize what pattern is most important at any given point or glean a tiny bit of performance out of matching the more commonly-expected patterns first.

Try it out

You can try this rule out by running the following command:

elm-review --template SiriusStarr/elm-review-no-unsorted/example --rules NoUnsortedCases

Configuration


type RuleConfig

Configuration for this rule. Create a new one with defaults and use doNotSortLiterals, sortListPatternsByLength, etc. to alter it.

defaults : RuleConfig

The default configuration, with the following behavior:

func x =
    case x of
        T () Bar ->
            1

        T () Baz ->
            2

        T () Foo ->
            3

will be sorted to

func x =
    case x of
        T () Foo ->
            3

        T () Bar ->
            1

        T () Baz ->
            2

Use doNotSortLiterals, sortListPatternsByLength, etc. to alter any of this behavior, e.g.

config =
    [ NoUnsortedCases.defaults
        |> NoUnsortedCases.doNotSortLiterals
        |> NoUnsortedCases.sortListPatternsByLength
        |> NoUnsortedCases.rule
    ]

sortOnlyMatchingTypes : (String -> String -> Basics.Bool) -> RuleConfig -> RuleConfig

Restrict custom type sorting to only those matching a provided predicate. This function takes two strings, the first being the full module name of a type, e.g. "Review.Rule" and the second being the name of a type, e.g. "Rule", and returns a Bool indicating whether the type should be sorted (with True meaning sortable). For example:

Module Foo:

module Foo exposing (Foo(..))

type Foo
    = Foo
    | Bar
    | Baz

Module Main:

module Main exposing (..)

type Msg
    = ButtonPressed
    | ButtonClicked

Module ReviewConfig:

onlyMsg moduleName typeName =
    case ( moduleName, typeName ) of
        ( "Main", "Msg" ) ->
            True

        _ ->
            False

config =
    [ NoUnsortedCases.defaults
        |> NoUnsortedCases.sortOnlyMatchingTypes onlyMsg
        |> NoUnsortedCases.rule
    ]

will sort the following pattern:

case msg of
    ButtonClicked ->
        ( { model | clicked = True }, Cmd.none )

    ButtonPressed ->
        ( { model | pressed = True }, Cmd.none )

but will not sort:

case foo of
    Bar ->
        "bar"

    Baz ->
        "baz"

    Foo ->
        "foo"

doNotSortLiterals : RuleConfig -> RuleConfig

Change the behavior of the rule to not sort literal patterns. If literals are not sorted, case expressions that would require sorting literals cannot be sorted and will thus be ignored by the rule.

doNotSortTypesFromDependencies : RuleConfig -> RuleConfig

Do not sort types from dependencies at all. Note that this will render unsortable any patterns requiring types from dependencies to be sorted.

sortTypesFromDependenciesAlphabetically : RuleConfig -> RuleConfig

Sort custom types imported from dependencies (including Basics types like Maybe and Bool) alphabetically, rather than by their source order in the dependency's source code.

sortListPatternsByLength : RuleConfig -> RuleConfig

List patterns may be sorted in one of two ways:

Note that uncons patterns are considered the length of their matching list, with wildcard patterns considered to have infinite length for the purposes of sorting. This is necessary to ensure that earlier patterns are not erroneously matched by wildcards.

Elementwise

case list of
    [] ->
        ""

    [ 1 ] ->
        "1"

    [ 1, 1 ] ->
        "11"

    [ 1, 1, 1 ] ->
        "111"

    [ 1, 2 ] ->
        "12"

    [ 1, 3 ] ->
        "13"

    [ 2 ] ->
        "2"

    [ 2, 1 ] ->
        "21"

    [ 2, 2 ] ->
        "22"

    [ 2, 3 ] ->
        "23"

    [ 3 ] ->
        "3"

    _ ->
        "Too many..."

Length First

case list of
    [] ->
        ""

    [ 1 ] ->
        "1"

    [ 2 ] ->
        "2"

    [ 3 ] ->
        "3"

    [ 1, 1 ] ->
        "11"

    [ 1, 2 ] ->
        "12"

    [ 1, 3 ] ->
        "13"

    [ 2, 1 ] ->
        "21"

    [ 2, 2 ] ->
        "22"

    [ 2, 3 ] ->
        "23"

    [ 1, 1, 1 ] ->
        "111"

    _ ->
        "Too many..."

doNotLookPastUnsortable : RuleConfig -> RuleConfig

Do not look beyond unsortable patterns, i.e. do not tiebreak cases with an unsortable sub-pattern by the next sub-pattern.

For example, given this:

type X
    = A
    | B () Int

f x =
    case x of
        B () 2 ->
            1

        B () 1 ->
            2

        A ->
            3

By default, this will be sorted to:

case x of
    A ->
        3

    -- v The rule sorted these patterns because even though it can't compare the (), 1 comes before 2
    B () 1 ->
        2

    B () 2 ->
        1

With doNotLookPastUnsortable, however, the two B patterns will be considered unsortable, so it will instead be sorted to this:

case x of
    A ->
        3

    -- v Comparison stopped for these patterns because the rule didn't look beyond the ()
    B () 2 ->
        1

    B () 1 ->
        2

Note that A is sorted above B in both cases because it did not require comparing the unsortable () pattern.

It's not clear why you'd ever want to use this, so it will likely be removed in a future major version. Please let me know if you actually find it useful!