SiriusStarr/elm-review-no-unsorted/1.0.6

NOTE: the latest version is 1.1.8

NoUnsortedRecords

Review Rule

rule : RuleConfig -> Rule

Reports record fields that are not in the "proper" order.

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

The proper order of record fields is the order in which they are defined in the type alias in your source files. See the "Configuration" section below for more information.

config =
    [ NoUnsortedRecords.rule
        (NoUnsortedRecords.defaults
            |> NoUnsortedRecords.reportAmbiguousRecordsWithoutFix
        )
    ]

"Proper" Order

Proper order may be defined in two ways. Firstly, type aliases define order, e.g.

type alias MyRecord =
    { foo : Int, bar : Int, baz : Int }

creates a record with name MyRecord and the known field order foo, bar, baz.

Secondly, records without a defined type alias that are nevertheless attached to a custom type are considered to be in the order they are defined in the source:

type MyType
    = A Int { foo : Int, bar : Int, baz : Int }
    | B { b : Int, a : Int, c : Int } String

creates two known records, with the field orders foo, bar, baz and b, a, c, respectively. This is true for records within fields of type aliases as well.

Inference/Disambiguation

Since records are not associated with a unique name, it is necessary to infer what type alias a record matches. In the most ambiguous case, all type aliases are checked for matching fields. If none are found, then the rule can't match it to a specific order (though it may still optionally be sorted alphabetically).

If only one matching type alias is found, then the rule will sort by that order.

In the case of multiple matching field sets, several things may happen. If all of the field sets have the same order, then it isn't necessary to unambiguously identify which is being matched, and that one order will be used. Otherwise, the rule is capable of using the following disambiguation rules:

  • Disambiguation by the fact that all fields must be present:
type alias A =
    { foo : Int, bar : Int, baz : Int }

type alias B =
    { bar : Int, foo : Int, baz : Int, extra : Int }

-- Must be type `A` because missing `extra`
a =
    { foo = 1, bar = 2, baz = 3 }
  • Disambiguation by type signature:
type alias A =
    { foo : Int, bar : Int, baz : Int }

type alias B =
    { bar : Int, foo : Int, baz : Int }

a : A
a =
    { foo = 1, bar = 2, baz = 3 }

It should be noted that this works with relatively complex type signatures, e.g.

type alias A =
    { foo : Int, bar : Int, baz : Int }


type alias B =
    { bar : Int, foo : Int, baz : Int }


a : Int -> String -> ( Int, String, List A )
a i s =
    ( i, s, [ { foo = 1, bar = 2, baz = 3 } ] )

This also works with patterns, e.g.

type alias A =
    { foo : Int, bar : Int, baz : Int }


type alias B =
    { bar : Int, foo : Int, baz : Int }


a : Int -> A -> Int -> Bool
a i1 { foo, bar, baz } i2 =
    True
  • Disambiguation by field type. Very rudimentary type inference is performed, but it may frequently be useful to add annotations, as the inference is by no means complete.
type alias A =
    { foo : Int, bar : Int, baz : Int }

type alias B =
    { bar : Int, foo : String, baz : Int }

-- Must be type `A` because `foo` is `Int`
a : { foo : Int, bar : Int, baz : Int }
a =
    { foo = 1, bar = 2, baz = 3 }
  • Disambiguation by the fact that the it is associated with a custom type with a known record argument:
type Custom
    = A { foo : Int, bar : Int, baz : Int }
    | B { bar : Int, foo : Int, baz : Int }

a =
    -- Must be `A`'s record
    A { foo = 1, bar = 2, baz = 3 }

b custom =
    case custom of
        -- Must be `A`'s record
        A { foo, bar } ->
            False

        -- Must be `B`'s record
        B { bar, foo } ->
            True
  • Disambiguation by the fact that the it is associated with a specific index of a custom type with a known record argument:
type Custom
    = A
        Int
        { foo : Int
        , bar : Int
        , baz : Int
        }
        String
        { bar : Int
        , foo : Int
        , baz : Int
        }

a custom =
    case custom of
        A _ { foo, bar } _ { bar, foo } ->
            False
  • Disambiguation by the fact that the it is associated with a specific field of a record alias:
type alias A =
    { a : { foo : Int, bar : Int, baz : Int }
    , b : { bar : Int, foo : Int, baz : Int }
    }

func : A
func =
    { a = { foo = 2, bar = 1, baz = 3 }
    , b = { bar = 2, foo = 1, baz = 3 }
    }
  • Disambiguation by known function argument types (this includes local bindings):
module A exposing (..)

type alias A =
    { foo : Int, bar : Int, baz : Int }

type alias B =
    { bar : Int, foo : Int, baz : Int }

foo : A -> Bool
foo a =
    True

func : Bool
func =
    -- Must be `A`, because `foo` has type `A -> Bool`
    foo { foo = 1, bar = 2, baz = 3 }

Best Practices for Disambiguation

Type annotations are always useful! If all functions have type annotations (with the appropriate aliases), then it's unlikely ambiguous records will ever be encountered. Beyond that, ambiguity can always be avoided by just making the canonical order for possibly-ambiguous records identical.

If you want to ensure that this rule is not encountering ambiguous/unknown records, then you can use reportAmbiguousRecordsWithoutFix and/or reportUnknownRecordsWithoutFix to report them without automatically sorting them alphabetically. Alternately, you can use doNotSortAmbiguousRecords and/or doNotSortUnknownRecords to disable all sorting/error reporting for them.

When (not) to enable this rule

This rule is useful when you want to ensure that your record fields are in a consistent, predictable order, that is consistent with the order in which they were defined.

This rule is not useful when you want to be able to write records in different orders throughout your codebase, e.g. if you want to emphasize what fields are most important at any given point. It may also not be useful if you have many records with the same fields.

Try it out

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

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

Configuration

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

The default configuration, with the following behavior:

  • Unknown records (those that match no known canonical order) are sorted alphabetically
  • Ambiguous records (those that match more than one canonical order) are sorted alphabetically
  • Generic fields of generic records are sorted before the canonical ones.

Use reportUnknownRecordsWithoutFix, etc. to alter this behavior, e.g.

config =
    [ NoUnsortedRecords.rule
        (NoUnsortedRecords.defaults
            |> NoUnsortedRecords.reportAmbiguousRecordsWithoutFix
        )
    ]

By default, generic fields are placed before others; this alters that behavior to place them at the end instead, e.g.

type alias A =
    { z : Int, y : Int, x : Int }


type alias Generic record =
    { record | foo : Int, bar : Int, baz : Int }


rec : Generic A
rec =
    { foo = 1, bar = 2, baz = 3, z = 4, y = 5, x = 6 }

By default, records that match multiple known aliases with different field orders are sorted alphabetically. (If the field orders of the various matches are identical, then it is not ambiguous.) This disables that behavior, leaving them in their base sorting instead.

By default, records that do not match any known aliases or custom types are sorted alphabetically. This disables that behavior, leaving them in their base sorting.

By default, records that match multiple known aliases with different field orders are sorted alphabetically. (If the field orders of the various matches are identical, then it is not ambiguous.) This disables that behavior, reporting them as ambiguous without automatically fixing them. This is useful if you want to catch ambiguous records and e.g. provide type annotations to make them unambiguous.

By default, records that do not match any known aliases or custom types are sorted alphabetically. This disables that behavior, reporting them as unknown without automatically fixing them.

Note that this will effectively forbid the use of ad hoc/anonymous records!