SiriusStarr / elm-review-no-single-pattern-case / NoSinglePatternCase

Notes

It is recommended that you also include NoUnused.Patterns in your config, as the fixes for this rule can sometimes generate nested as patterns in very complex cases (e.g. when nested single-pattern cases are simplified). These cases cannot be resolved automatically, as it is unclear which name should be preferred.

Review Rule

rule : Config fixBy -> Review.Rule.Rule

Reports single-pattern case expressions, which may be written more concisely or removed entirely.

config =
    [ NoSinglePatternCase.rule NoSinglePatternCase.fixInArgument ]

See Config for configuration details.

Fails

Single-pattern case expressions for destructuring are not allowed, as:

type Opaque
    = Opaque Int

unpack : Opaque -> Int
unpack o =
    case o of
        Opaque i ->
            i

may instead be written more concisely, for example as

unpack : Opaque -> Int
unpack (Opaque i) =
    i

Similarly, single-pattern case expressions that ase not used for destructuring are not allowed, as:

type AOrB
    = A
    | B

pointless : AOrB -> Bool
pointless aOrB =
    case aOrB of
        _ ->
            True

may instead be written more concisely, for example as

pointless : AOrB -> Bool
pointless _ =
    True

or

pointless : AOrB -> Bool
pointless =
    always True

Success

Single patterns with constructors that do not match their type name, e.g. type Msg = ButtonClicked, are allowed by default, unless they are imported from dependencies (as those types are not expected to be iterated on). This behavior can be changed with reportAllCustomTypes.

Any case expression with more than one pattern match will not be reported. Consider using jfmengels/elm-review-simplify to detect unnecessary multi-pattern cases.

When (not) to enable this rule

This rule is useful if you prefer destructuring in function/lambda arguments or let bindings, rather than in a single-pattern case.

This rule is not useful if you prefer the more verbose style.

Try it out

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

elm-review --template SiriusStarr/elm-review-no-single-pattern-case/example/fix-in-argument --rules NoSinglePatternCase

Config


type Config fixBy

Configure the rule, determining how automatic fixes are generated.

The default Configs fixInArgument and fixInLet should be used as reasonable defaults, with more customization detailed in those sections.

The behavior of the rule with constructors that don't match their type name can be configured via reportAllCustomTypes. By default, only constructors with an identical name to their type are reported.

The behavior of the rule in the context of useless single pattern cases can also be configured via replaceUnusedBindings. A single pattern case is considered to be useless if its pattern does not bind any name that is actually used in the expression.

fixInArgument : Config FixInArgument

Always fix by destructuring in the argument. This will use as patterns if necessary. If the argument cannot be destructured in, no fix will be generated.

For example:

f1 o =
    case o of
        Opaque i ->
            i

f2 o =
    let
        x =
            someFunc o
    in
    case o of
        Opaque i ->
            i + x

f3 { recordField } =
    case recordField of
        Opaque i ->
            i

will be fixed to:

f1 (Opaque i) =
    i

f2 ((Opaque i) as o) =
    let
        x =
            someFunc o
    in
    i + x

f3 { recordField } =
    case recordField of
        Opaque i ->
            i

Use ifAsPatternRequired and ifCannotDestructureAtArgument to customize the behavior in either of these cases.

fixInLet : Config FixInLet

Always fix by destructuring in a let block, creating a new one if none exists. For example:

f1 o =
    case o of
        Opaque i ->
            i

f2 o =
    let
        x =
            someFunc o
    in
    case o of
        Opaque i ->
            i + x

will be fixed to:

f1 o =
    let
        (Opaque i) =
            o
    in
    i

f2 ((Opaque i) as o) =
    let
        x =
            someFunc o

        (Opaque i) =
            o
    in
    i + x

Use ifNoLetExists to customize the behavior in the case where no let block exists within the scope the pattern is valid in. To clarify, the following counts as no let existing:

unpack : Opaque -> Int
unpack o =
    let
        foo =
            bar
    in
    (\a ->
        case a of
            Opaque i ->
                i
    )
        o

as the name a is not in scope in the extant let block.

ifNoLetExists also handles the case where the closest let block would result in a name clash. To clarify, the following counts as no let existing:

unpack : Opaque -> Int
unpack o =
    let
        foo =
            (\i -> i + 1) 0
    in
    case o of
        Opaque i ->
            i

as i cannot be unpacked in the let block, as doing so would cause a name clash with the i in foo.

Customizing Config Behavior

reportAllCustomTypes : Config fixBy -> Config fixBy

By default, only constructors whose names are identical to their type are reported, along with types imported from dependencies (since those aren't expected to be iterated on). This setting changes behavior back to that of version 2.0.2 and earlier, where absolutely all single pattern cases are flagged by the rule, regardless of the types.

-- import the constructor `OutsideConstructor` from some other package


import SomeOutsidePackage exposing (OutsideType(..))

type Date
    = -- Constructor has same name as type
      Date Int

type Msg
    = -- Constructor has different name than type
      ThingieClicked

update1 : Date -> Int -> Int
update1 date i =
    case date of
        -- THIS CASE IS ALWAYS FLAGGED
        Date j ->
            i + j

update2 : Msg -> Int -> Int
update2 msg i =
    case msg of
        -- THIS CASE IS NOT FLAGGED BY DEFAULT, IS FLAGGED WITH `reportAllCustomTypes`
        ThingieClicked ->
            i + 1

update3 : OutsideType -> Int -> Int
update3 oType i =
    case oType of
        -- THIS CASE IS ALWAYS FLAGGED
        OutsideConstructor j ->
            i + j

replaceUnusedBindings : Config fixBy -> Config fixBy

A single pattern case is considered to be useless if its pattern does not bind any name that is actually used in the expression, e.g.

case x of
    _ ->
        True

case x of
    () ->
        True

case x of
    A ({ field1, field2 } as record) ->
        List.map foo bar
            |> List.sum
            |> baz

The rule will always provide fixes for such cases but by default will not replace the binding used in the case...of expression. This option configures the rule to replace such bindings where possible with the most specific option. For example:

f unusedArg =
    case unusedArg of
        _ ->
            True

will be fixed to

f _ =
    True

and

f x =
    case x of
        A ({ field1, field2 } as record) ->
            List.map foo bar
                |> List.sum
                |> baz

will be fixed to

f (A _) =
    List.map foo bar
        |> List.sum
        |> baz

This provides a more clear indication that the binding is unused. The binding will of course not be replaced if it used anywhere but in case...of.

replaceUnusedBindingsWithWildcard : Config fixBy -> Config fixBy

This setting changes behavior back to that of version 2.0.2 and earlier, where useless bindings are replaced by _ (or ()), not by the constructor name.

For example,

f x =
    case x of
        A ({ field1, field2 } as record) ->
            List.map foo bar
                |> List.sum
                |> baz

will be fixed to

f _ =
    List.map foo bar
        |> List.sum
        |> baz

This is useful if you are reliant on an IDE that doesn't recognize

pointless CreateNewLet =
    foo

as valid Elm syntax. The downside is that you will not receive compiler warnings if the argument changes (e.g. if you add a new constructor to the type).

ifAsPatternRequired : UseAsPatternOrLetsOrFail -> Config FixInArgument -> Config FixInArgument

Specify what to do if an as pattern would be required to destructure in the argument, e.g.

f o =
    let
        x =
            someFunc o
    in
    case o of
        Opaque i ->
            i + x

Available options are useAsPattern (this is the default), fixInLetInstead or fail, e.g.

c1 =
    fixInArgument
        |> ifAsPatternRequired fail

c2 =
    fixInArgument
        |> ifAsPatternRequired useAsPattern

c3 =
    fixInArgument
        |> ifAsPatternRequired
            (fixInLetInstead
                |> andIfNoLetExists createNewLet
            )

c4 =
    fixInArgument
        |> ifAsPatternRequired
            (fixInLetInstead
                |> andIfNoLetExists useAsPattern
            )

ifCannotDestructureAtArgument : UseLetOrFail -> Config FixInArgument -> Config FixInArgument

Specify what to do if the argument cannot be destructured at, either due to it being a record field, e.g.

f { recordField } =
    case recordField of
        Opaque i ->
            i

or a more complex case expression, e.g.

f a =
    case foo <| bar a of
        Opaque i ->
            i

or due to a name clash that would be caused by the increase in scope, e.g.

unpack : Opaque -> Int
unpack o =
    let
        foo =
            (\i -> i + 1) 0
    in
    case o of
        Opaque i ->
            i

Available options are fixInLetInstead or fail (this is the default).

c1 =
    fixInArgument
        |> ifCannotDestructureAtArgument fail

c2 =
    fixInArgument
        |> ifCannotDestructureAtArgument
            (fixInLetInstead
                |> andIfNoLetExists fail
            )

c3 =
    fixInArgument
        |> ifCannotDestructureAtArgument
            (fixInLetInstead
                |> andIfNoLetExists createNewLet
            )

ifNoLetExists : UseArgOrCreateNewLetOrFail -> Config FixInLet -> Config FixInLet

Specify what to do it no let block exists in scope, instead of creating a new one.

Available options are fixInArgumentInstead, createNewLet (this is the default), or fail. Note that andIfAsPatternRequired and andIfCannotDestructureAtArgument must appear in that order after fixInArgumentInstead.

c1 =
    fixInLet
        |> ifNoLetExists fail

c2 =
    fixInLet
        -- This is the default
        |> ifNoLetExists createNewLet

c3 =
    fixInLet
        |> ifNoLetExists
            (fixInArgumentInstead
                |> andIfAsPatternRequired useAsPattern
                |> andIfCannotDestructureAtArgument fail
            )

Config Behavior Options

These functions are simply used by ifAsPatternRequired, ifCannotDestructureAtArgument, and ifNoLetExists to customize behavior of the default configs. Look at the examples in those to understand how to use them.

fail : Either or Fail

Choose to fail at generating a fix.

createNewLet : CreateNewLetOr or

Choose to create a let block when none exists.

useAsPattern : UseAsPatternOrFailOr or

Choose to use an as pattern to destructure in the argument if necessary.

fixInArgumentInstead : UseAsPatternOrFailOr CreateNewLet -> CreateNewLetOr Fail -> UseArgOrCreateNewLetOrFail

Fallback to destructuring in the argument instead of a let block.

Note that andIfAsPatternRequired and andIfCannotDestructureAtArgument must appear in that order, e.g.

c =
    fixInLet
        |> ifNoLetExists
            (fixInArgumentInstead
                |> andIfAsPatternRequired useAsPattern
                |> andIfCannotDestructureAtArgument fail
            )

andIfAsPatternRequired : UseAsPatternOrFailOr CreateNewLet -> (UseAsPatternOrFailOr CreateNewLet -> CreateNewLetOr Fail -> UseArgOrCreateNewLetOrFail) -> CreateNewLetOr Fail -> UseArgOrCreateNewLetOrFail

Specify what to do if an as pattern would be necessary.

Available options are useAsPattern, createNewLet or fail

c1 =
    fixInLet
        |> ifNoLetExists
            (fixInArgumentInstead
                |> andIfAsPatternRequired useAsPattern
                |> andIfCannotDestructureAtArgument fail
            )

c2 =
    fixInLet
        |> ifNoLetExists
            (fixInArgumentInstead
                |> andIfAsPatternRequired createNewLet
                |> andIfCannotDestructureAtArgument fail
            )

c3 =
    fixInLet
        |> ifNoLetExists
            (fixInArgumentInstead
                |> andIfAsPatternRequired fail
                |> andIfCannotDestructureAtArgument fail
            )

andIfCannotDestructureAtArgument : CreateNewLetOr Fail -> (CreateNewLetOr Fail -> UseArgOrCreateNewLetOrFail) -> UseArgOrCreateNewLetOrFail

Specify what to do if the argument cannot be destructured at, e.g.

f { recordField } =
    case recordField of
        Opaque i ->
            i

Available options are createNewLet or fail

c1 =
    fixInLet
        |> ifNoLetExists
            (fixInArgumentInstead
                |> andIfAsPatternRequired fail
                |> andIfCannotDestructureAtArgument createNewLet
            )

c2 =
    fixInLet
        |> ifNoLetExists
            (fixInArgumentInstead
                |> andIfAsPatternRequired fail
                |> andIfCannotDestructureAtArgument fail
            )

fixInLetInstead : a -> UseLetOr a b

Fallback to destructuring in a let block instead of the argument.

andIfNoLetExists : a -> (a -> UseLetOr a b) -> UseLetOr a b

If no let block exists to destructure in, choose some other behavior instead.

Types

You shouldn't need to worry about these types; they are exported solely for the sake of annotation, should it be necessary.


type FixInArgument

Phantom type for Config fixBy.


type FixInLet

Phantom type for Config fixBy.


type UseArgInstead

Fallback to destructuring in the argument, if choosing not to create a let block.


type UseLetInstead noValidLetOptions

Fallback to using a let block if destructuring in the argument has failed (or we've chosen not to).


type CreateNewLet

Choose to create a new let block to destructure in.


type Fail

Choose to fail at generating a fix.


type UseAsPattern

Choose to use an as pattern.


type alias UseAsPatternOrFailOr or =
Either (Either or UseAsPattern) Fail

At this point, an as pattern could be used or some other option.


type alias CreateNewLetOr or =
Either (Either CreateNewLet or) Fail

At this point, a new let could be created or some other option.


type alias UseArgOrCreateNewLetOrFail =
Either (Either CreateNewLet UseArgInstead) Fail

At this point, the argument could be used or a new let block created. block.


type alias UseLetOr or1 or2 =
Either (Either (UseLetInstead or1) or2) Fail

At this point, a let could be used or either of two other options.


type alias UseLetOrFail =
UseLetOr (CreateNewLetOr Fail) Fail

At this point, the only option is to use a let block.


type alias UseAsPatternOrLetsOrFail =
UseAsPatternOrFailOr (UseLetInstead (UseAsPatternOrFailOr CreateNewLet))

At this point, an as pattern could be used, or we could use a let block.


type alias Either a b =
Util.Either a b

Offer a choice between two options.