rule : RuleConfig -> Review.Rule.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 may be defined in several 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 either a subrecord of a type alias or 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
when encountered in their larger context. By default, these are not considered
canonical records when encountered alone, though this behavior may be turned on
with treatAllSubrecordsAsCanonical
or
treatCustomTypeRecordsAsCanonical
.
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:
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 }
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
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 }
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
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
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 }
}
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 }
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.
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.
You can try this rule out by running the following command:
elm-review --template SiriusStarr/elm-review-no-unsorted/example --rules NoUnsortedRecords
Configuration for this rule. Create a new one with defaults
and use
reportAmbiguousRecordsWithoutFix
, doNotSortUnknownRecords
, etc. to alter it.
defaults : RuleConfig
The default configuration, with the following behavior:
{ foo = 1, bar = 2 }
will match
{ foo : String, bar : String }
if no other records exist with the fields
foo
and bar
. This is to protect against incorrect type inference by this
rule.Use reportUnknownRecordsWithoutFix
, etc. to alter this behavior, e.g.
config =
[ NoUnsortedRecords.rule
(NoUnsortedRecords.defaults
|> NoUnsortedRecords.reportAmbiguousRecordsWithoutFix
)
]
sortGenericFieldsLast : RuleConfig -> RuleConfig
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 }
An ambiguous record is a record that matches more than one known "canonical" record.
doNotSortAmbiguousRecords : RuleConfig -> RuleConfig
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.
reportAmbiguousRecordsWithoutFix : RuleConfig -> RuleConfig
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.
An unknown record is a record that does not match any known "canonical" records.
doNotSortUnknownRecords : RuleConfig -> RuleConfig
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.
reportUnknownRecordsWithoutFix : RuleConfig -> RuleConfig
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!
Subrecords are records that are either within the fields of a type alias or are arguments of a custom type.
treatSubrecordsAsUnknown : RuleConfig -> RuleConfig
By default, anonymous records within known records and within custom type constructors are sorted by their declaration order when encountered in the context of their outer record/constructor. This disables that behavior, treating them the same as any other unknown record.
For example:
type A
= A { foo : Int, bar : Int, baz : Int }
type alias Rec =
{ yi : { foo : Int, bar : Int, baz : Int }, er : Int }
thisWillBeUnknown =
A { bar = 1, baz = 2, foo = 3 }
and =
{ yi =
-- This will also be unknown
{ bar = 1, baz = 2, foo = 3 }
, er = 1
}
treatAllSubrecordsAsCanonical : RuleConfig -> RuleConfig
By default, anonymous records within known records and within custom type constructors are sorted by their declaration order when encountered in the context of their outer record/constructor. This extends that behavior to sort them even when encountered alone (i.e. not in the context of their parent record/constructor. Note that canonical records will always take priority, however.
For example:
type alias Rec =
{ yi : { foo : Int, bar : Int, baz : Int }, er : Int }
thisWillHaveCanonicalOrder =
-- Even though it does not appear in the context of `Rec`
{ foo = 3, bar = 1, baz = 2 }
treatCustomTypeRecordsAsCanonical : RuleConfig -> RuleConfig
By default, anonymous records within known records and within custom type
constructors are sorted by their declaration order when encountered in the
context of their outer record/constructor. This extends that behavior to sort
custom type args even when encountered alone (i.e. not in the context of their
constructor. This was the behavior prior to version 1.1.0
and thus this
setting is provided for compatibility. Note that canonical records will always
take priority, however.
For example:
type A
= A { foo : Int, bar : Int, baz : Int }
thisWillHaveCanonicalOrder =
-- Even though it does not appear in the context of `A`
{ foo = 3, bar = 1, baz = 2 }
typecheckAllRecords : RuleConfig -> RuleConfig
By default, typechecking is only used to disambiguate records. This alters
that behavior to typecheck all records. For instance, this will force
{ foo = 1, bar = 2 }
to be an "unknown" record if
{ foo : String, bar : String }
is known. This should probably be left turned
off, unless you wish to help find examples of incorrect type inference by this
rule.