mirror of
https://github.com/jfmengels/elm-review.git
synced 2024-08-17 12:00:29 +03:00
9192 lines
345 KiB
Elm
9192 lines
345 KiB
Elm
module Simplify exposing
|
|
( rule
|
|
, Configuration, defaults, expectNaN, ignoreCaseOfForTypes
|
|
)
|
|
|
|
{-| Reports when an expression can be simplified.
|
|
|
|
🔧 Running with `--fix` will automatically remove all the reported errors.
|
|
|
|
config =
|
|
[ Simplify.rule Simplify.defaults
|
|
]
|
|
|
|
@docs rule
|
|
@docs Configuration, defaults, expectNaN, ignoreCaseOfForTypes
|
|
|
|
|
|
## Try it out
|
|
|
|
You can try this rule out by running the following command:
|
|
|
|
```bash
|
|
elm-review --template jfmengels/elm-review-simplify/example --rules Simplify
|
|
```
|
|
|
|
|
|
## Simplifications
|
|
|
|
Below is the list of all kinds of simplifications this rule applies.
|
|
|
|
|
|
### Booleans
|
|
|
|
x || True
|
|
--> True
|
|
|
|
x || False
|
|
--> x
|
|
|
|
x && True
|
|
--> x
|
|
|
|
x && False
|
|
--> False
|
|
|
|
not True
|
|
--> False
|
|
|
|
not (not x)
|
|
--> x
|
|
|
|
-- for `<`, `>`, `<=`, `>=`, `==` and `/=`
|
|
not (a < b)
|
|
--> a >= b
|
|
|
|
|
|
### Comparisons
|
|
|
|
x == True
|
|
--> x
|
|
|
|
x /= False
|
|
--> x
|
|
|
|
not x == not y
|
|
--> x == y
|
|
|
|
anything == anything
|
|
--> True
|
|
|
|
anything /= anything
|
|
--> False
|
|
|
|
{ r | a = 1 } == { r | a = 2 }
|
|
--> False
|
|
|
|
|
|
### If expressions
|
|
|
|
if True then x else y
|
|
--> x
|
|
|
|
if False then x else y
|
|
--> y
|
|
|
|
if condition then x else x
|
|
--> x
|
|
|
|
if condition then True else False
|
|
--> condition
|
|
|
|
if condition then False else True
|
|
--> not condition
|
|
|
|
a =
|
|
if condition then
|
|
if not condition then
|
|
1
|
|
else
|
|
2
|
|
else
|
|
3
|
|
--> if condition then 2 else 3
|
|
|
|
|
|
### Case expressions
|
|
|
|
case condition of
|
|
True -> x
|
|
False -> y
|
|
--> if condition then x else y
|
|
|
|
case condition of
|
|
False -> y
|
|
True -> x
|
|
--> if not condition then x else y
|
|
|
|
-- only when no variables are introduced in the pattern
|
|
-- and no custom types defined in the project are referenced
|
|
case value of
|
|
Just _ -> x
|
|
Nothing -> x
|
|
--> x
|
|
|
|
Destructuring using case expressions
|
|
|
|
case value of
|
|
( x, y ) ->
|
|
x + y
|
|
|
|
-->
|
|
let
|
|
( x, y ) =
|
|
value
|
|
in
|
|
x + y
|
|
|
|
|
|
### Let expressions
|
|
|
|
let
|
|
a =
|
|
1
|
|
in
|
|
let
|
|
b =
|
|
1
|
|
in
|
|
a + b
|
|
|
|
-->
|
|
let
|
|
a =
|
|
1
|
|
|
|
b =
|
|
1
|
|
in
|
|
a + b
|
|
|
|
|
|
### Record updates
|
|
|
|
{ a | b = a.b }
|
|
--> a
|
|
|
|
{ a | b = a.b, c = 1 }
|
|
--> { a | c = 1 }
|
|
|
|
|
|
### Field access
|
|
|
|
{ a = b }.a
|
|
--> b
|
|
|
|
{ a | b = c }.b
|
|
--> c
|
|
|
|
{ a | b = c }.d
|
|
--> a.d
|
|
|
|
(let a = b in c).d
|
|
--> let a = b in c.d
|
|
|
|
|
|
### Basics functions
|
|
|
|
identity x
|
|
--> x
|
|
|
|
f >> identity
|
|
--> f
|
|
|
|
always x y
|
|
--> x
|
|
|
|
f >> always x
|
|
--> always x
|
|
|
|
|
|
### Lambdas
|
|
|
|
(\_ -> x) data
|
|
--> x
|
|
|
|
(\() y -> x) ()
|
|
--> (\y -> x)
|
|
|
|
(\_ y -> x) data
|
|
--> (\y -> x)
|
|
|
|
(\x y -> x + y) n m
|
|
-- Reported because simplifiable but not autofixed
|
|
|
|
|
|
### Operators
|
|
|
|
(++) a b
|
|
--> a ++ b
|
|
|
|
a |> f >> g
|
|
--> a |> f |> g
|
|
|
|
|
|
### Numbers
|
|
|
|
n + 0
|
|
--> n
|
|
|
|
n - 0
|
|
--> n
|
|
|
|
0 - n
|
|
--> -n
|
|
|
|
n * 1
|
|
--> n
|
|
|
|
n / 1
|
|
--> n
|
|
|
|
0 / n
|
|
--> 0
|
|
|
|
-(-n)
|
|
--> n
|
|
|
|
negate (negate n)
|
|
--> n
|
|
|
|
n - n
|
|
--> 0
|
|
|
|
|
|
### Strings
|
|
|
|
"a" ++ ""
|
|
--> "a"
|
|
|
|
String.fromList []
|
|
--> ""
|
|
|
|
String.fromList [ a ]
|
|
--> String.fromChar a
|
|
|
|
String.isEmpty ""
|
|
--> True
|
|
|
|
String.isEmpty "a"
|
|
--> False
|
|
|
|
String.concat []
|
|
--> ""
|
|
|
|
String.join str []
|
|
--> ""
|
|
|
|
String.join "" list
|
|
--> String.concat list
|
|
|
|
String.length "abc"
|
|
--> 3
|
|
|
|
String.repeat n ""
|
|
--> ""
|
|
|
|
String.repeat 0 str
|
|
--> ""
|
|
|
|
String.repeat 1 str
|
|
--> str
|
|
|
|
String.replace x y ""
|
|
--> ""
|
|
|
|
String.replace x x z
|
|
--> z
|
|
|
|
String.replace "x" "y" "z"
|
|
--> "z" -- only when resulting string is unchanged
|
|
|
|
String.words ""
|
|
--> []
|
|
|
|
String.lines ""
|
|
--> []
|
|
|
|
String.reverse ""
|
|
--> ""
|
|
|
|
String.reverse (String.reverse str)
|
|
--> str
|
|
|
|
String.slice n n str
|
|
--> ""
|
|
|
|
String.slice n 0 str
|
|
--> ""
|
|
|
|
String.slice a z ""
|
|
--> ""
|
|
|
|
String.left 0 str
|
|
--> ""
|
|
|
|
String.left -1 str
|
|
--> ""
|
|
|
|
String.left n ""
|
|
--> ""
|
|
|
|
String.right 0 str
|
|
--> ""
|
|
|
|
String.right -1 str
|
|
--> ""
|
|
|
|
String.right n ""
|
|
--> ""
|
|
|
|
String.slice 2 1 str
|
|
--> ""
|
|
|
|
String.slice -1 -2 str
|
|
--> ""
|
|
|
|
|
|
### Maybes
|
|
|
|
Maybe.map identity x
|
|
--> x
|
|
|
|
Maybe.map f Nothing
|
|
--> Nothing
|
|
|
|
Maybe.map f (Just x)
|
|
--> Just (f x)
|
|
|
|
Maybe.andThen f Nothing
|
|
--> Nothing
|
|
|
|
Maybe.andThen (always Nothing) x
|
|
--> Nothing
|
|
|
|
Maybe.andThen (\a -> Just b) x
|
|
--> Maybe.map (\a -> b) x
|
|
|
|
Maybe.andThen (\a -> if condition a then Just b else Just c) x
|
|
--> Maybe.map (\a -> if condition a then b else c) x
|
|
|
|
Maybe.andThen f (Just x)
|
|
--> f x
|
|
|
|
Maybe.withDefault x Nothing
|
|
--> x
|
|
|
|
Maybe.withDefault x (Just y)
|
|
--> y
|
|
|
|
|
|
### Results
|
|
|
|
Result.map identity x
|
|
--> x
|
|
|
|
Result.map f (Err x)
|
|
--> Err x
|
|
|
|
Result.map f (Ok x)
|
|
--> Ok (f x)
|
|
|
|
Result.mapError identity x
|
|
--> x
|
|
|
|
Result.mapError f (Ok x)
|
|
--> Ok x
|
|
|
|
Result.mapError f (Err x)
|
|
--> Err (f x)
|
|
|
|
Result.andThen f (Err x)
|
|
--> Err x
|
|
|
|
Result.andThen f (Ok x)
|
|
--> f x
|
|
|
|
Result.andThen (\a -> Ok b) x
|
|
--> Result.map (\a -> b) x
|
|
|
|
Result.withDefault x (Err y)
|
|
--> x
|
|
|
|
Result.withDefault x (Ok y)
|
|
--> y
|
|
|
|
Result.toMaybe (Ok x)
|
|
--> Just x
|
|
|
|
Result.toMaybe (Err e)
|
|
--> Nothing
|
|
|
|
|
|
### Lists
|
|
|
|
a :: []
|
|
--> [ a ]
|
|
|
|
a :: [ b ]
|
|
--> [ a, b ]
|
|
|
|
[ a ] ++ list
|
|
--> a :: list
|
|
|
|
[] ++ list
|
|
--> list
|
|
|
|
[ a, b ] ++ [ c ]
|
|
--> [ a, b, c ]
|
|
|
|
List.append [] ys
|
|
--> ys
|
|
|
|
List.append [ a, b ] [ c ]
|
|
--> [ a, b, c ]
|
|
|
|
List.head []
|
|
--> Nothing
|
|
|
|
List.head (a :: bToZ)
|
|
--> Just a
|
|
|
|
List.tail []
|
|
--> Nothing
|
|
|
|
List.tail (a :: bToZ)
|
|
--> Just bToZ
|
|
|
|
List.member a []
|
|
--> False
|
|
|
|
List.member a [ a, b, c ]
|
|
--> True
|
|
|
|
List.member a [ b ]
|
|
--> a == b
|
|
|
|
List.map f [] -- same for most List functions like List.filter, List.filterMap, ...
|
|
--> []
|
|
|
|
List.map identity list
|
|
--> list
|
|
|
|
List.filter (always True) list
|
|
--> list
|
|
|
|
List.filter (always False) list
|
|
--> []
|
|
|
|
List.filterMap Just list
|
|
--> list
|
|
|
|
List.filterMap (\a -> if condition a then Just b else Just c) list
|
|
--> List.map (\a -> if condition a then b else c) list
|
|
|
|
List.filterMap (always Nothing) list
|
|
--> []
|
|
|
|
List.filterMap identity (List.map f list)
|
|
--> List.filterMap f list
|
|
|
|
List.filterMap identity [ Just x, Just y ]
|
|
--> [ x, y ]
|
|
|
|
List.concat [ [ a, b ], [ c ] ]
|
|
--> [ a, b, c ]
|
|
|
|
List.concat [ a, [ 1 ], [ 2 ] ]
|
|
--> List.concat [ a, [ 1, 2 ] ]
|
|
|
|
List.concat [ a, [], b ]
|
|
--> List.concat [ a, b ]
|
|
|
|
List.concatMap identity list
|
|
--> List.concat list
|
|
|
|
List.concatMap (\a -> [ b ]) list
|
|
--> List.map (\a -> b) list
|
|
|
|
List.concatMap f [ x ]
|
|
--> f x
|
|
|
|
List.concatMap (always []) list
|
|
--> []
|
|
|
|
List.concat (List.map f list)
|
|
--> List.concatMap f list
|
|
|
|
List.indexedMap (\_ value -> f value) list
|
|
--> List.map (\value -> f value) list
|
|
|
|
List.isEmpty []
|
|
--> True
|
|
|
|
List.isEmpty [ a ]
|
|
--> False
|
|
|
|
List.isEmpty (x :: xs)
|
|
--> False
|
|
|
|
List.sum []
|
|
--> 0
|
|
|
|
List.sum [ a ]
|
|
--> a
|
|
|
|
List.product []
|
|
--> 1
|
|
|
|
List.product [ a ]
|
|
--> a
|
|
|
|
List.minimum []
|
|
--> Nothing
|
|
|
|
List.minimum [ a ]
|
|
--> Just a
|
|
|
|
List.maximum []
|
|
--> Nothing
|
|
|
|
List.maximum [ a ]
|
|
--> Just a
|
|
|
|
-- The following simplifications for List.foldl also work for List.foldr
|
|
List.foldl f x []
|
|
--> x
|
|
|
|
List.foldl (\_ soFar -> soFar) x list
|
|
--> x
|
|
|
|
List.foldl (+) 0 list
|
|
--> List.sum list
|
|
|
|
List.foldl (+) initial list
|
|
--> initial + List.sum list
|
|
|
|
List.foldl (*) 1 list
|
|
--> List.product list
|
|
|
|
List.foldl (*) 0 list
|
|
--> 0
|
|
|
|
List.foldl (*) initial list
|
|
--> initial * List.product list
|
|
|
|
List.foldl (&&) True list
|
|
--> List.all identity list
|
|
|
|
List.foldl (&&) False list
|
|
--> False
|
|
|
|
List.foldl (||) False list
|
|
--> List.any identity list
|
|
|
|
List.foldl (||) True list
|
|
--> True
|
|
|
|
List.all f []
|
|
--> True
|
|
|
|
List.all (always True) list
|
|
--> True
|
|
|
|
List.any f []
|
|
--> True
|
|
|
|
List.any (always False) list
|
|
--> False
|
|
|
|
List.any ((==) x) list
|
|
--> List.member x list
|
|
|
|
List.range 6 3
|
|
--> []
|
|
|
|
List.length [ a, b, c ]
|
|
--> 3
|
|
|
|
List.repeat 0 x
|
|
--> []
|
|
|
|
List.partition f []
|
|
--> ( [], [] )
|
|
|
|
List.partition (always True) list
|
|
--> ( list, [] )
|
|
|
|
List.partition (always False) list
|
|
--> ( [], list )
|
|
|
|
List.take 0 list
|
|
--> []
|
|
|
|
List.drop 0 list
|
|
--> list
|
|
|
|
List.reverse (List.reverse list)
|
|
--> list
|
|
|
|
List.sortBy (always a) list
|
|
--> list
|
|
|
|
List.sortBy identity list
|
|
--> List.sort list
|
|
|
|
List.sortWith (\_ _ -> LT) list
|
|
--> List.reverse list
|
|
|
|
List.sortWith (\_ _ -> EQ) list
|
|
--> list
|
|
|
|
List.sortWith (\_ _ -> GT) list
|
|
--> list
|
|
|
|
-- The following simplifications for List.sort also work for List.sortBy f and List.sortWith f
|
|
List.sort []
|
|
--> []
|
|
|
|
List.sort [ a ]
|
|
--> [ a ]
|
|
|
|
-- same for up to List.map5 when any list is empty
|
|
List.map2 f xs []
|
|
--> []
|
|
|
|
List.map2 f [] ys
|
|
--> []
|
|
|
|
List.unzip []
|
|
--> ( [], [] )
|
|
|
|
|
|
### Sets
|
|
|
|
Set.map f Set.empty -- same for Set.filter, Set.remove...
|
|
--> Set.empty
|
|
|
|
Set.map identity set
|
|
--> set
|
|
|
|
Set.isEmpty Set.empty
|
|
--> True
|
|
|
|
Set.member x Set.empty
|
|
--> False
|
|
|
|
Set.fromList []
|
|
--> Set.empty
|
|
|
|
Set.fromList [ a ]
|
|
--> Set.singleton a
|
|
|
|
Set.toList Set.empty
|
|
--> []
|
|
|
|
Set.length Set.empty
|
|
--> 0
|
|
|
|
Set.intersect Set.empty set
|
|
--> Set.empty
|
|
|
|
Set.diff Set.empty set
|
|
--> Set.empty
|
|
|
|
Set.diff set Set.empty
|
|
--> set
|
|
|
|
Set.union set Set.empty
|
|
--> set
|
|
|
|
Set.insert x Set.empty
|
|
--> Set.singleton x
|
|
|
|
-- same for foldr
|
|
List.foldl f x (Set.toList set)
|
|
--> Set.foldl f x set
|
|
|
|
Set.partition f Set.empty
|
|
--> ( Set.empty, Set.empty )
|
|
|
|
Set.partition (always True) set
|
|
--> ( set, Set.empty )
|
|
|
|
|
|
### Dict
|
|
|
|
Dict.isEmpty Dict.empty
|
|
--> True
|
|
|
|
Dict.fromList []
|
|
--> Dict.empty
|
|
|
|
Dict.toList Dict.empty
|
|
--> []
|
|
|
|
Dict.size Dict.empty
|
|
--> 0
|
|
|
|
Dict.member x Dict.empty
|
|
--> False
|
|
|
|
Dict.partition f Dict.empty
|
|
--> ( Dict.empty, Dict.empty )
|
|
|
|
Dict.partition (always True) dict
|
|
--> ( dict, Dict.empty )
|
|
|
|
Dict.partition (always False) dict
|
|
--> ( Dict.empty, dict )
|
|
|
|
List.map Tuple.first (Dict.toList dict)
|
|
--> Dict.keys dict
|
|
|
|
List.map Tuple.second (Dict.toList dict)
|
|
--> Dict.values dict
|
|
|
|
|
|
### Cmd / Sub
|
|
|
|
All of these also apply for `Sub`.
|
|
|
|
Cmd.batch []
|
|
--> Cmd.none
|
|
|
|
Cmd.batch [ a ]
|
|
--> a
|
|
|
|
Cmd.batch [ a, Cmd.none, b ]
|
|
--> Cmd.batch [ a, b ]
|
|
|
|
Cmd.map identity cmd
|
|
--> cmd
|
|
|
|
Cmd.map f Cmd.none
|
|
--> Cmd.none
|
|
|
|
|
|
### Html.Attributes
|
|
|
|
Html.Attributes.classList [ x, y, ( z, False ) ]
|
|
--> Html.Attributes.classList [ x, y ]
|
|
|
|
Html.Attributes.classList [ ( onlyOneThing, True ) ]
|
|
--> Html.Attributes.class onlyOneThing
|
|
|
|
|
|
### Json.Decode
|
|
|
|
Json.Decode.oneOf [ a ]
|
|
--> a
|
|
|
|
|
|
### Parser
|
|
|
|
Parser.oneOf [ a ]
|
|
--> a
|
|
|
|
|
|
### Random
|
|
|
|
Random.uniform a []
|
|
--> Random.constant a
|
|
|
|
Random.weighted ( weight, a ) []
|
|
--> Random.constant a
|
|
|
|
Random.weighted tuple []
|
|
--> Random.constant (Tuple.first tuple)
|
|
|
|
Random.list 0 generator
|
|
--> Random.constant []
|
|
|
|
Random.list 1 generator
|
|
--> Random.map List.singleton generator
|
|
|
|
Random.list n (Random.constant el)
|
|
--> Random.constant (List.repeat n el)
|
|
|
|
Random.map identity generator
|
|
--> generator
|
|
|
|
Random.map (always a) generator
|
|
--> Random.constant a
|
|
|
|
Random.map f (Random.constant x)
|
|
--> Random.constant (f x)
|
|
|
|
-}
|
|
|
|
import Dict exposing (Dict)
|
|
import Elm.Docs
|
|
import Elm.Project exposing (Exposed)
|
|
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
|
|
import Elm.Syntax.Exposing as Exposing
|
|
import Elm.Syntax.Expression as Expression exposing (Expression)
|
|
import Elm.Syntax.Import exposing (Import)
|
|
import Elm.Syntax.Module
|
|
import Elm.Syntax.ModuleName exposing (ModuleName)
|
|
import Elm.Syntax.Node as Node exposing (Node(..))
|
|
import Elm.Syntax.Pattern as Pattern exposing (Pattern)
|
|
import Elm.Syntax.Range as Range exposing (Location, Range)
|
|
import Review.Fix as Fix exposing (Fix)
|
|
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
|
|
import Review.Project.Dependency as Dependency exposing (Dependency)
|
|
import Review.Rule as Rule exposing (Error, Rule)
|
|
import Set exposing (Set)
|
|
import Simplify.AstHelpers as AstHelpers exposing (emptyStringAsString, qualifiedToString)
|
|
import Simplify.Evaluate as Evaluate
|
|
import Simplify.Infer as Infer
|
|
import Simplify.Match as Match exposing (Match(..))
|
|
import Simplify.Normalize as Normalize
|
|
import Simplify.RangeDict as RangeDict exposing (RangeDict)
|
|
|
|
|
|
{-| Rule to simplify Elm code.
|
|
-}
|
|
rule : Configuration -> Rule
|
|
rule (Configuration config) =
|
|
Rule.newProjectRuleSchema "Simplify" initialContext
|
|
|> Rule.withDirectDependenciesProjectVisitor (dependenciesVisitor (Set.fromList config.ignoreConstructors))
|
|
|> Rule.withModuleVisitor (moduleVisitor config)
|
|
|> Rule.withContextFromImportedModules
|
|
|> Rule.withModuleContextUsingContextCreator
|
|
{ fromProjectToModule = fromProjectToModule
|
|
, fromModuleToProject = fromModuleToProject
|
|
, foldProjectContexts = foldProjectContexts
|
|
}
|
|
|> Rule.providesFixesForProjectRule
|
|
|> Rule.fromProjectRuleSchema
|
|
|
|
|
|
moduleVisitor : { config | expectNaN : Bool } -> Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
|
|
moduleVisitor config schema =
|
|
schema
|
|
|> Rule.withCommentsVisitor (\comments context -> ( [], commentsVisitor comments context ))
|
|
|> Rule.withDeclarationListVisitor (\decls context -> ( [], declarationListVisitor decls context ))
|
|
|> Rule.withDeclarationEnterVisitor (\node context -> ( [], declarationVisitor node context ))
|
|
|> Rule.withExpressionEnterVisitor (\expressionNode context -> expressionVisitor expressionNode config context)
|
|
|> Rule.withExpressionExitVisitor (\node context -> ( [], expressionExitVisitor node context ))
|
|
|
|
|
|
|
|
-- CONFIGURATION
|
|
|
|
|
|
{-| Configuration for this rule. Create a new one with [`defaults`](#defaults) and use [`ignoreCaseOfForTypes`](#ignoreCaseOfForTypes) to alter it.
|
|
-}
|
|
type Configuration
|
|
= Configuration
|
|
{ ignoreConstructors : List String
|
|
, expectNaN : Bool
|
|
}
|
|
|
|
|
|
{-| Default configuration for this rule.
|
|
|
|
The rule aims tries to improve the code through simplifications that don't impact the behavior. An exception to this are
|
|
when the presence of `NaN` values
|
|
|
|
Use [`expectNaN`](#expectNaN) if you want to opt out of changes that can impact the behaviour of your code if you expect to work with `NaN` values.
|
|
|
|
Use [`ignoreCaseOfForTypes`](#ignoreCaseOfForTypes) if you want to prevent simplifying case expressions that work on custom types defined in dependencies.
|
|
|
|
config =
|
|
[ Simplify.rule Simplify.defaults
|
|
]
|
|
|
|
-- or
|
|
config =
|
|
[ Simplify.defaults
|
|
|> Simplify.expectNaN
|
|
|> Simplify.ignoreCaseOfForTypes [ "Module.Name.Type" ]
|
|
|> Simplify.rule
|
|
]
|
|
|
|
-}
|
|
defaults : Configuration
|
|
defaults =
|
|
Configuration
|
|
{ ignoreConstructors = []
|
|
, expectNaN = False
|
|
}
|
|
|
|
|
|
{-| Ignore some reports about types from dependencies used in case expressions.
|
|
|
|
This rule simplifies the following construct:
|
|
|
|
module Module.Name exposing (..)
|
|
|
|
case value of
|
|
Just _ -> x
|
|
Nothing -> x
|
|
--> x
|
|
|
|
(Since `v2.0.19`) it will not try to simplify the case expression when some of the patterns references custom types constructors
|
|
defined in the project. It will only do so for custom types that are defined in dependencies (including `elm/core`).
|
|
|
|
If you do happen to want to disable this simplification for a type `Module.Name.Type`, you can configure the rule like this:
|
|
|
|
config =
|
|
[ Simplify.defaults
|
|
|> Simplify.ignoreCaseOfForTypes [ "Module.Name.Type" ]
|
|
|> Simplify.rule
|
|
]
|
|
|
|
I personally don't recommend to use this function too much, because this could be a sign of premature abstraction, and because
|
|
I think that often [You Aren't Gonna Need this code](https://jfmengels.net/safe-dead-code-removal/#yagni-you-arent-gonna-need-it).
|
|
|
|
Please let me know by opening an issue if you do use this function, I am very curious to know;
|
|
|
|
-}
|
|
ignoreCaseOfForTypes : List String -> Configuration -> Configuration
|
|
ignoreCaseOfForTypes ignoreConstructors (Configuration config) =
|
|
Configuration { ignoreConstructors = ignoreConstructors ++ config.ignoreConstructors, expectNaN = config.expectNaN }
|
|
|
|
|
|
{-| Usually, `elm-review-simplify` will only suggest simplifications that are safe to apply without risk of changing the original behavior.
|
|
However, when encountering [`NaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN)
|
|
values, some simplifications can actually impact behavior.
|
|
|
|
For instance, the following expression will evaluate to `True`:
|
|
|
|
x == x
|
|
--> True
|
|
|
|
However, if `x` is `NaN` or a value containing `NaN` then the expression will evaluate to `False`:
|
|
|
|
-- given x = NaN
|
|
x == x
|
|
--> False
|
|
|
|
-- given x = { a = ( NaN, 0 ) }
|
|
x == x
|
|
--> False
|
|
|
|
Given the potential presence of `NaN`, some simplifications become unsafe to apply:
|
|
|
|
- `x == x` to `True`
|
|
- `List.member x [ x ]` to `True`
|
|
- `n * 0` to `0`
|
|
|
|
This special value is hard to recreate in Elm code both intentionally and unintentionally,
|
|
and it's therefore unlikely to be found in your application,
|
|
which is why the rule applies these simplifications by defaults.
|
|
|
|
If you somehow expect to create and encounter `NaN` values in your codebase, then you can use this function to disable these simplifications altogether.
|
|
|
|
config =
|
|
[ Simplify.defaults
|
|
|> Simplify.expectNaN
|
|
|> Simplify.rule
|
|
]
|
|
|
|
-}
|
|
expectNaN : Configuration -> Configuration
|
|
expectNaN (Configuration config) =
|
|
Configuration { ignoreConstructors = config.ignoreConstructors, expectNaN = True }
|
|
|
|
|
|
|
|
-- CONTEXT
|
|
|
|
|
|
type alias ProjectContext =
|
|
{ customTypesToReportInCases : Set ( ModuleName, ConstructorName )
|
|
, exposedVariants : Dict ModuleName (Set String)
|
|
}
|
|
|
|
|
|
type alias ModuleContext =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, moduleName : ModuleName
|
|
, exposedVariantTypes : Exposed
|
|
, commentRanges : List Range
|
|
, moduleBindings : Set String
|
|
, localBindings : RangeDict (Set String)
|
|
, branchLocalBindings : RangeDict (Set String)
|
|
, rangesToIgnore : RangeDict ()
|
|
, rightSidesOfPlusPlus : RangeDict ()
|
|
, customTypesToReportInCases : Set ( ModuleName, ConstructorName )
|
|
, localIgnoredCustomTypes : List Constructor
|
|
, constructorsToIgnore : Set ( ModuleName, String )
|
|
, inferredConstantsDict : RangeDict Infer.Inferred
|
|
, inferredConstants : ( Infer.Inferred, List Infer.Inferred )
|
|
, extractSourceCode : Range -> String
|
|
, exposedVariants : Set String
|
|
, importLookup : ImportLookup
|
|
}
|
|
|
|
|
|
type alias ImportLookup =
|
|
Dict
|
|
ModuleName
|
|
{ alias : Maybe ModuleName
|
|
, exposed : Exposed -- includes names of found variants
|
|
}
|
|
|
|
|
|
type alias QualifyResources a =
|
|
{ a
|
|
| importLookup : ImportLookup
|
|
, moduleBindings : Set String
|
|
, localBindings : RangeDict (Set String)
|
|
}
|
|
|
|
|
|
type Exposed
|
|
= ExposedAll
|
|
| ExposedSome (Set String)
|
|
|
|
|
|
isExposedFrom : Exposed -> String -> Bool
|
|
isExposedFrom exposed name =
|
|
case exposed of
|
|
ExposedAll ->
|
|
True
|
|
|
|
ExposedSome some ->
|
|
Set.member name some
|
|
|
|
|
|
type alias ConstructorName =
|
|
String
|
|
|
|
|
|
type alias Constructor =
|
|
{ moduleName : ModuleName
|
|
, name : String
|
|
, constructors : List String
|
|
}
|
|
|
|
|
|
initialContext : ProjectContext
|
|
initialContext =
|
|
{ customTypesToReportInCases = Set.empty
|
|
, exposedVariants = Dict.empty
|
|
}
|
|
|
|
|
|
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
|
|
fromModuleToProject =
|
|
Rule.initContextCreator
|
|
(\moduleContext ->
|
|
{ customTypesToReportInCases = Set.empty
|
|
, exposedVariants =
|
|
Dict.singleton moduleContext.moduleName
|
|
moduleContext.exposedVariants
|
|
}
|
|
)
|
|
|
|
|
|
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
|
|
fromProjectToModule =
|
|
Rule.initContextCreator
|
|
(\lookupTable metadata extractSourceCode fullAst projectContext ->
|
|
let
|
|
moduleExposedVariantTypes : Exposed
|
|
moduleExposedVariantTypes =
|
|
moduleExposingContext fullAst.moduleDefinition
|
|
|
|
imports : ImportLookup
|
|
imports =
|
|
List.foldl
|
|
(\import_ importLookup ->
|
|
let
|
|
importInfo : { moduleName : ModuleName, exposed : Exposed, alias : Maybe ModuleName }
|
|
importInfo =
|
|
importContext import_
|
|
in
|
|
insertImport importInfo.moduleName { alias = importInfo.alias, exposed = importInfo.exposed } importLookup
|
|
)
|
|
implicitImports
|
|
fullAst.imports
|
|
in
|
|
{ lookupTable = lookupTable
|
|
, moduleName = Rule.moduleNameFromMetadata metadata
|
|
, exposedVariantTypes = moduleExposedVariantTypes
|
|
, importLookup =
|
|
createImportLookup
|
|
{ imports = imports
|
|
, importExposedVariants = projectContext.exposedVariants
|
|
}
|
|
, commentRanges = []
|
|
, moduleBindings = Set.empty
|
|
, localBindings = RangeDict.empty
|
|
, branchLocalBindings = RangeDict.empty
|
|
, rangesToIgnore = RangeDict.empty
|
|
, rightSidesOfPlusPlus = RangeDict.empty
|
|
, localIgnoredCustomTypes = []
|
|
, customTypesToReportInCases = projectContext.customTypesToReportInCases
|
|
, constructorsToIgnore = Set.empty
|
|
, inferredConstantsDict = RangeDict.empty
|
|
, inferredConstants = ( Infer.empty, [] )
|
|
, extractSourceCode = extractSourceCode
|
|
, exposedVariants = Set.empty
|
|
}
|
|
)
|
|
|> Rule.withModuleNameLookupTable
|
|
|> Rule.withMetadata
|
|
|> Rule.withSourceCodeExtractor
|
|
|> Rule.withFullAst
|
|
|
|
|
|
importContext : Node Import -> { moduleName : ModuleName, exposed : Exposed, alias : Maybe ModuleName }
|
|
importContext importNode =
|
|
let
|
|
import_ : Import
|
|
import_ =
|
|
Node.value importNode
|
|
in
|
|
{ moduleName = import_.moduleName |> Node.value
|
|
, alias =
|
|
import_.moduleAlias |> Maybe.map Node.value
|
|
, exposed =
|
|
case import_.exposingList of
|
|
Nothing ->
|
|
ExposedSome Set.empty
|
|
|
|
Just (Node _ existingExposing) ->
|
|
case existingExposing of
|
|
Exposing.All _ ->
|
|
ExposedAll
|
|
|
|
Exposing.Explicit exposes ->
|
|
ExposedSome
|
|
(Set.fromList
|
|
(List.map
|
|
(\(Node _ expose) -> AstHelpers.nameOfExpose expose)
|
|
exposes
|
|
)
|
|
)
|
|
}
|
|
|
|
|
|
createImportLookup :
|
|
{ imports : Dict ModuleName { alias : Maybe ModuleName, exposed : Exposed }
|
|
, importExposedVariants : Dict ModuleName (Set String)
|
|
}
|
|
-> ImportLookup
|
|
createImportLookup context =
|
|
context.imports
|
|
|> Dict.map
|
|
(\moduleName import_ ->
|
|
case import_.exposed of
|
|
ExposedAll ->
|
|
import_
|
|
|
|
ExposedSome some ->
|
|
case Dict.get moduleName context.importExposedVariants of
|
|
Nothing ->
|
|
import_
|
|
|
|
Just importExposedVariants ->
|
|
{ import_
|
|
| exposed =
|
|
ExposedSome
|
|
(Set.union some importExposedVariants)
|
|
}
|
|
)
|
|
|
|
|
|
moduleExposingContext : Node Elm.Syntax.Module.Module -> Exposed
|
|
moduleExposingContext moduleHeader =
|
|
case Elm.Syntax.Module.exposingList (Node.value moduleHeader) of
|
|
Exposing.All _ ->
|
|
ExposedAll
|
|
|
|
Exposing.Explicit some ->
|
|
ExposedSome
|
|
(List.foldl
|
|
(\(Node _ expose) acc ->
|
|
case AstHelpers.getTypeExposeIncludingVariants expose of
|
|
Just name ->
|
|
Set.insert name acc
|
|
|
|
Nothing ->
|
|
acc
|
|
)
|
|
Set.empty
|
|
some
|
|
)
|
|
|
|
|
|
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
|
|
foldProjectContexts newContext previousContext =
|
|
{ customTypesToReportInCases = Set.empty
|
|
, exposedVariants = Dict.union newContext.exposedVariants previousContext.exposedVariants
|
|
}
|
|
|
|
|
|
|
|
-- DEPENDENCIES VISITOR
|
|
|
|
|
|
dependenciesVisitor : Set String -> Dict String Dependency -> ProjectContext -> ( List (Error scope), ProjectContext )
|
|
dependenciesVisitor typeNamesAsStrings dict context =
|
|
let
|
|
modules : List Elm.Docs.Module
|
|
modules =
|
|
dict
|
|
|> Dict.values
|
|
|> List.concatMap Dependency.modules
|
|
|
|
unions : Set String
|
|
unions =
|
|
List.concatMap (\module_ -> List.map (\union -> module_.name ++ "." ++ union.name) module_.unions) modules
|
|
|> Set.fromList
|
|
|
|
unknownTypesToIgnore : List String
|
|
unknownTypesToIgnore =
|
|
Set.diff typeNamesAsStrings unions
|
|
|> Set.toList
|
|
|
|
customTypesToReportInCases : Set ( ModuleName, String )
|
|
customTypesToReportInCases =
|
|
modules
|
|
|> List.concatMap
|
|
(\mod ->
|
|
let
|
|
moduleName : ModuleName
|
|
moduleName =
|
|
AstHelpers.moduleNameFromString mod.name
|
|
in
|
|
mod.unions
|
|
|> List.filter (\union -> not (Set.member (mod.name ++ "." ++ union.name) typeNamesAsStrings))
|
|
|> List.concatMap (\union -> union.tags)
|
|
|> List.map (\( tagName, _ ) -> ( moduleName, tagName ))
|
|
)
|
|
|> Set.fromList
|
|
|
|
dependencyExposedVariants : Dict ModuleName (Set String)
|
|
dependencyExposedVariants =
|
|
List.foldl
|
|
(\moduleDoc acc ->
|
|
Dict.insert
|
|
(AstHelpers.moduleNameFromString moduleDoc.name)
|
|
(moduleDoc.unions
|
|
|> List.concatMap
|
|
(\union ->
|
|
union.tags
|
|
|> List.map (\( variantName, _ ) -> variantName)
|
|
)
|
|
|> Set.fromList
|
|
)
|
|
acc
|
|
)
|
|
context.exposedVariants
|
|
modules
|
|
in
|
|
( if List.isEmpty unknownTypesToIgnore then
|
|
[]
|
|
|
|
else
|
|
[ errorForUnknownIgnoredConstructor unknownTypesToIgnore ]
|
|
, { customTypesToReportInCases = customTypesToReportInCases
|
|
, exposedVariants = dependencyExposedVariants
|
|
}
|
|
)
|
|
|
|
|
|
errorForUnknownIgnoredConstructor : List String -> Error scope
|
|
errorForUnknownIgnoredConstructor list =
|
|
Rule.globalError
|
|
{ message = "Could not find type names: " ++ (String.join ", " <| List.map wrapInBackticks list)
|
|
, details =
|
|
[ "I expected to find these custom types in the dependencies, but I could not find them."
|
|
, "Please check whether these types and have not been removed, and if so, remove them from the configuration of this rule."
|
|
, "If you find that these types have been moved or renamed, please update your configuration."
|
|
, "Note that I may have provided fixes for things you didn't wish to be fixed, so you might want to undo the changes I have applied."
|
|
, "Also note that the configuration for this rule changed in v2.0.19: types that are custom to your project are ignored by default, so this configuration setting can only be used to avoid simplifying case expressions that use custom types defined in dependencies."
|
|
]
|
|
}
|
|
|
|
|
|
|
|
-- COMMENTS VISITOR
|
|
|
|
|
|
commentsVisitor : List (Node String) -> ModuleContext -> ModuleContext
|
|
commentsVisitor comments context =
|
|
{ context | commentRanges = List.map Node.range comments }
|
|
|
|
|
|
|
|
-- DECLARATION LIST VISITOR
|
|
|
|
|
|
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ModuleContext
|
|
declarationListVisitor declarationList context =
|
|
{ context
|
|
| moduleBindings = AstHelpers.declarationListBindings declarationList
|
|
}
|
|
|
|
|
|
|
|
-- DECLARATION VISITOR
|
|
|
|
|
|
declarationVisitor : Node Declaration -> ModuleContext -> ModuleContext
|
|
declarationVisitor declarationNode context =
|
|
case Node.value declarationNode of
|
|
Declaration.CustomTypeDeclaration variantType ->
|
|
let
|
|
variantTypeName : String
|
|
variantTypeName =
|
|
Node.value variantType.name
|
|
in
|
|
if isExposedFrom context.exposedVariantTypes variantTypeName then
|
|
let
|
|
exposedVariants : Set String
|
|
exposedVariants =
|
|
List.foldl
|
|
(\(Node _ variant) acc -> Set.insert (Node.value variant.name) acc)
|
|
context.exposedVariants
|
|
variantType.constructors
|
|
in
|
|
{ context | exposedVariants = exposedVariants }
|
|
|
|
else
|
|
context
|
|
|
|
Declaration.FunctionDeclaration functionDeclaration ->
|
|
{ context
|
|
| rangesToIgnore = RangeDict.empty
|
|
, rightSidesOfPlusPlus = RangeDict.empty
|
|
, inferredConstantsDict = RangeDict.empty
|
|
, localBindings =
|
|
RangeDict.singleton
|
|
(Node.range functionDeclaration.declaration)
|
|
(AstHelpers.patternListBindings (Node.value functionDeclaration.declaration).arguments)
|
|
}
|
|
|
|
_ ->
|
|
context
|
|
|
|
|
|
|
|
-- EXPRESSION VISITOR
|
|
|
|
|
|
expressionVisitor : Node Expression -> { config | expectNaN : Bool } -> ModuleContext -> ( List (Error {}), ModuleContext )
|
|
expressionVisitor node config context =
|
|
let
|
|
expressionRange : Range
|
|
expressionRange =
|
|
Node.range node
|
|
|
|
contextWithInferredConstants : ModuleContext
|
|
contextWithInferredConstants =
|
|
case RangeDict.get expressionRange context.inferredConstantsDict of
|
|
Nothing ->
|
|
context
|
|
|
|
Just inferredConstants ->
|
|
let
|
|
( previous, previousStack ) =
|
|
context.inferredConstants
|
|
in
|
|
{ context
|
|
| inferredConstants = ( inferredConstants, previous :: previousStack )
|
|
}
|
|
in
|
|
if RangeDict.member expressionRange context.rangesToIgnore then
|
|
( [], contextWithInferredConstants )
|
|
|
|
else
|
|
let
|
|
expression : Expression
|
|
expression =
|
|
Node.value node
|
|
|
|
withExpressionSurfaceBindings : RangeDict (Set String)
|
|
withExpressionSurfaceBindings =
|
|
RangeDict.insert expressionRange (expressionSurfaceBindings expression) context.localBindings
|
|
|
|
withNewBranchLocalBindings : RangeDict (Set String)
|
|
withNewBranchLocalBindings =
|
|
RangeDict.union (expressionBranchLocalBindings expression)
|
|
context.branchLocalBindings
|
|
|
|
contextWithInferredConstantsAndLocalBindings : ModuleContext
|
|
contextWithInferredConstantsAndLocalBindings =
|
|
case RangeDict.get expressionRange context.branchLocalBindings of
|
|
Nothing ->
|
|
{ contextWithInferredConstants
|
|
| localBindings = withExpressionSurfaceBindings
|
|
, branchLocalBindings =
|
|
withNewBranchLocalBindings
|
|
}
|
|
|
|
Just currentBranchLocalBindings ->
|
|
{ contextWithInferredConstants
|
|
| localBindings =
|
|
RangeDict.insert expressionRange currentBranchLocalBindings withExpressionSurfaceBindings
|
|
, branchLocalBindings =
|
|
RangeDict.remove expressionRange withNewBranchLocalBindings
|
|
}
|
|
|
|
expressionChecked : { errors : List (Error {}), rangesToIgnore : RangeDict (), rightSidesOfPlusPlus : RangeDict (), inferredConstants : List ( Range, Infer.Inferred ) }
|
|
expressionChecked =
|
|
expressionVisitorHelp node config contextWithInferredConstantsAndLocalBindings
|
|
in
|
|
( expressionChecked.errors
|
|
, { contextWithInferredConstantsAndLocalBindings
|
|
| rangesToIgnore = RangeDict.union expressionChecked.rangesToIgnore context.rangesToIgnore
|
|
, rightSidesOfPlusPlus = RangeDict.union expressionChecked.rightSidesOfPlusPlus context.rightSidesOfPlusPlus
|
|
, inferredConstantsDict =
|
|
List.foldl (\( range, constants ) acc -> RangeDict.insert range constants acc)
|
|
contextWithInferredConstants.inferredConstantsDict
|
|
expressionChecked.inferredConstants
|
|
}
|
|
)
|
|
|
|
|
|
{-| From the `elm/core` readme:
|
|
|
|
>
|
|
> ### Default Imports
|
|
|
|
> The modules in this package are so common, that some of them are imported by default in all Elm files. So it is as if every Elm file starts with these imports:
|
|
>
|
|
> import Basics exposing (..)
|
|
> import List exposing (List, (::))
|
|
> import Maybe exposing (Maybe(..))
|
|
> import Result exposing (Result(..))
|
|
> import String exposing (String)
|
|
> import Char exposing (Char)
|
|
> import Tuple
|
|
> import Debug
|
|
> import Platform exposing (Program)
|
|
> import Platform.Cmd as Cmd exposing (Cmd)
|
|
> import Platform.Sub as Sub exposing (Sub)
|
|
|
|
-}
|
|
implicitImports : ImportLookup
|
|
implicitImports =
|
|
[ ( [ "Basics" ], { alias = Nothing, exposed = ExposedAll } )
|
|
, ( [ "List" ], { alias = Nothing, exposed = ExposedSome (Set.fromList [ "List", "(::)" ]) } )
|
|
, ( [ "Maybe" ], { alias = Nothing, exposed = ExposedSome (Set.fromList [ "Maybe", "Just", "Nothing" ]) } )
|
|
, ( [ "Result" ], { alias = Nothing, exposed = ExposedSome (Set.fromList [ "Result", "Ok", "Err" ]) } )
|
|
, ( [ "String" ], { alias = Nothing, exposed = ExposedSome (Set.singleton "String") } )
|
|
, ( [ "Char" ], { alias = Nothing, exposed = ExposedSome (Set.singleton "Char") } )
|
|
, ( [ "Tuple" ], { alias = Nothing, exposed = ExposedSome Set.empty } )
|
|
, ( [ "Debug" ], { alias = Nothing, exposed = ExposedSome Set.empty } )
|
|
, ( [ "Platform" ], { alias = Nothing, exposed = ExposedSome (Set.singleton "Program") } )
|
|
, ( [ "Platform", "Cmd" ], { alias = Just [ "Cmd" ], exposed = ExposedSome (Set.singleton "Cmd") } )
|
|
, ( [ "Platform", "Sub" ], { alias = Just [ "Sub" ], exposed = ExposedSome (Set.singleton "Sub") } )
|
|
]
|
|
|> Dict.fromList
|
|
|
|
|
|
{-| Merge a given new import with an existing import lookup.
|
|
This is strongly preferred over Dict.insert since the implicit default imports can be overridden
|
|
-}
|
|
insertImport : ModuleName -> { alias : Maybe ModuleName, exposed : Exposed } -> ImportLookup -> ImportLookup
|
|
insertImport moduleName importInfoToAdd importLookup =
|
|
Dict.update moduleName
|
|
(\existingImport ->
|
|
let
|
|
newImportInfo : { alias : Maybe ModuleName, exposed : Exposed }
|
|
newImportInfo =
|
|
case existingImport of
|
|
Nothing ->
|
|
importInfoToAdd
|
|
|
|
Just import_ ->
|
|
{ alias = findMap .alias [ import_, importInfoToAdd ]
|
|
, exposed = exposedMerge ( import_.exposed, importInfoToAdd.exposed )
|
|
}
|
|
in
|
|
Just newImportInfo
|
|
)
|
|
importLookup
|
|
|
|
|
|
exposedMerge : ( Exposed, Exposed ) -> Exposed
|
|
exposedMerge exposedTuple =
|
|
case exposedTuple of
|
|
( ExposedAll, _ ) ->
|
|
ExposedAll
|
|
|
|
( ExposedSome _, ExposedAll ) ->
|
|
ExposedAll
|
|
|
|
( ExposedSome aSet, ExposedSome bSet ) ->
|
|
ExposedSome (Set.union aSet bSet)
|
|
|
|
|
|
qualify : ( ModuleName, String ) -> QualifyResources a -> ( ModuleName, String )
|
|
qualify ( moduleName, name ) qualifyResources =
|
|
let
|
|
qualification : ModuleName
|
|
qualification =
|
|
case qualifyResources.importLookup |> Dict.get moduleName of
|
|
Nothing ->
|
|
moduleName
|
|
|
|
Just import_ ->
|
|
let
|
|
moduleImportedName : ModuleName
|
|
moduleImportedName =
|
|
import_.alias |> Maybe.withDefault moduleName
|
|
in
|
|
if not (isExposedFrom import_.exposed name) then
|
|
moduleImportedName
|
|
|
|
else
|
|
let
|
|
isShadowed : Bool
|
|
isShadowed =
|
|
isBindingInScope qualifyResources name
|
|
in
|
|
if isShadowed then
|
|
moduleImportedName
|
|
|
|
else
|
|
[]
|
|
in
|
|
( qualification, name )
|
|
|
|
|
|
isBindingInScope :
|
|
{ a
|
|
| moduleBindings : Set String
|
|
, localBindings : RangeDict (Set String)
|
|
}
|
|
-> String
|
|
-> Bool
|
|
isBindingInScope resources name =
|
|
Set.member name resources.moduleBindings
|
|
|| RangeDict.any (\bindings -> Set.member name bindings) resources.localBindings
|
|
|
|
|
|
{-| Whenever you add ranges on expression enter, the same ranges should be removed on expression exit.
|
|
Having one function finding unique ranges and a function for extracting bindings there ensures said consistency.
|
|
|
|
An alternative approach would be to use some kind of tree structure
|
|
with parent and sub ranges and bindings as leaves (maybe a "trie", tho I've not seen one as an elm package).
|
|
|
|
Removing all bindings for an expression's range on leave would then be trivial
|
|
|
|
-}
|
|
expressionSurfaceBindings : Expression -> Set String
|
|
expressionSurfaceBindings expression =
|
|
case expression of
|
|
Expression.LambdaExpression lambda ->
|
|
AstHelpers.patternListBindings lambda.args
|
|
|
|
Expression.LetExpression letBlock ->
|
|
AstHelpers.letDeclarationListBindings letBlock.declarations
|
|
|
|
_ ->
|
|
Set.empty
|
|
|
|
|
|
expressionBranchLocalBindings : Expression -> RangeDict (Set String)
|
|
expressionBranchLocalBindings expression =
|
|
case expression of
|
|
Expression.CaseExpression caseBlock ->
|
|
RangeDict.mapFromList
|
|
(\( Node _ pattern, Node resultRange _ ) ->
|
|
( resultRange
|
|
, AstHelpers.patternBindings pattern
|
|
)
|
|
)
|
|
caseBlock.cases
|
|
|
|
Expression.LetExpression letBlock ->
|
|
List.foldl
|
|
(\(Node _ letDeclaration) acc ->
|
|
case letDeclaration of
|
|
Expression.LetFunction letFunctionOrValueDeclaration ->
|
|
RangeDict.insert
|
|
(Node.range (Node.value letFunctionOrValueDeclaration.declaration).expression)
|
|
(AstHelpers.patternListBindings
|
|
(Node.value letFunctionOrValueDeclaration.declaration).arguments
|
|
)
|
|
acc
|
|
|
|
_ ->
|
|
acc
|
|
)
|
|
RangeDict.empty
|
|
letBlock.declarations
|
|
|
|
_ ->
|
|
RangeDict.empty
|
|
|
|
|
|
expressionExitVisitor : Node Expression -> ModuleContext -> ModuleContext
|
|
expressionExitVisitor (Node expressionRange _) context =
|
|
let
|
|
contextWithUpdatedLocalBindings : ModuleContext
|
|
contextWithUpdatedLocalBindings =
|
|
if RangeDict.member expressionRange context.rangesToIgnore then
|
|
context
|
|
|
|
else
|
|
{ context
|
|
| localBindings =
|
|
RangeDict.remove expressionRange context.localBindings
|
|
}
|
|
in
|
|
if RangeDict.member expressionRange context.inferredConstantsDict then
|
|
case Tuple.second context.inferredConstants of
|
|
topOfStack :: restOfStack ->
|
|
{ contextWithUpdatedLocalBindings | inferredConstants = ( topOfStack, restOfStack ) }
|
|
|
|
[] ->
|
|
-- should never be empty
|
|
contextWithUpdatedLocalBindings
|
|
|
|
else
|
|
contextWithUpdatedLocalBindings
|
|
|
|
|
|
errorsAndRangesToIgnore : List (Error {}) -> RangeDict () -> { errors : List (Error {}), rangesToIgnore : RangeDict (), rightSidesOfPlusPlus : RangeDict (), inferredConstants : List ( Range, Infer.Inferred ) }
|
|
errorsAndRangesToIgnore errors rangesToIgnore =
|
|
{ errors = errors
|
|
, rangesToIgnore = rangesToIgnore
|
|
, rightSidesOfPlusPlus = RangeDict.empty
|
|
, inferredConstants = []
|
|
}
|
|
|
|
|
|
onlyErrors : List (Error {}) -> { errors : List (Error {}), rangesToIgnore : RangeDict (), rightSidesOfPlusPlus : RangeDict (), inferredConstants : List ( Range, Infer.Inferred ) }
|
|
onlyErrors errors =
|
|
{ errors = errors
|
|
, rangesToIgnore = RangeDict.empty
|
|
, rightSidesOfPlusPlus = RangeDict.empty
|
|
, inferredConstants = []
|
|
}
|
|
|
|
|
|
firstThatReportsError : List (a -> List (Error {})) -> a -> List (Error {})
|
|
firstThatReportsError remainingChecks data =
|
|
findMap
|
|
(\checkFn ->
|
|
case checkFn data of
|
|
[] ->
|
|
Nothing
|
|
|
|
firstError :: afterFirstError ->
|
|
Just (firstError :: afterFirstError)
|
|
)
|
|
remainingChecks
|
|
|> Maybe.withDefault []
|
|
|
|
|
|
expressionVisitorHelp : Node Expression -> { config | expectNaN : Bool } -> ModuleContext -> { errors : List (Error {}), rangesToIgnore : RangeDict (), rightSidesOfPlusPlus : RangeDict (), inferredConstants : List ( Range, Infer.Inferred ) }
|
|
expressionVisitorHelp (Node expressionRange expression) config context =
|
|
let
|
|
toCheckInfo :
|
|
{ fnRange : Range
|
|
, firstArg : Node Expression
|
|
, argsAfterFirst : List (Node Expression)
|
|
, usingRightPizza : Bool
|
|
}
|
|
-> CheckInfo
|
|
toCheckInfo checkInfo =
|
|
{ lookupTable = context.lookupTable
|
|
, expectNaN = config.expectNaN
|
|
, extractSourceCode = context.extractSourceCode
|
|
, importLookup = context.importLookup
|
|
, commentRanges = context.commentRanges
|
|
, moduleBindings = context.moduleBindings
|
|
, localBindings = context.localBindings
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = expressionRange
|
|
, fnRange = checkInfo.fnRange
|
|
, firstArg = checkInfo.firstArg
|
|
, argsAfterFirst = checkInfo.argsAfterFirst
|
|
, secondArg = List.head checkInfo.argsAfterFirst
|
|
, thirdArg = List.head (List.drop 1 checkInfo.argsAfterFirst)
|
|
, usingRightPizza = checkInfo.usingRightPizza
|
|
}
|
|
|
|
toCompositionCheckInfo :
|
|
{ direction : LeftOrRightDirection
|
|
, earlier : Node Expression
|
|
, later : Node Expression
|
|
, parentRange : Range
|
|
}
|
|
-> CompositionCheckInfo
|
|
toCompositionCheckInfo compositionSpecific =
|
|
{ lookupTable = context.lookupTable
|
|
, importLookup = context.importLookup
|
|
, moduleBindings = context.moduleBindings
|
|
, localBindings = context.localBindings
|
|
, direction = compositionSpecific.direction
|
|
, parentRange = compositionSpecific.parentRange
|
|
, earlier = compositionSpecific.earlier
|
|
, later = compositionSpecific.later
|
|
}
|
|
in
|
|
case expression of
|
|
-----------------
|
|
-- APPLICATION --
|
|
-----------------
|
|
Expression.Application (applied :: firstArg :: argsAfterFirst) ->
|
|
onlyErrors
|
|
(case applied of
|
|
Node fnRange (Expression.FunctionOrValue _ fnName) ->
|
|
case ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange of
|
|
Just moduleName ->
|
|
case Dict.get ( moduleName, fnName ) functionCallChecks of
|
|
Just checkFn ->
|
|
checkFn
|
|
(toCheckInfo
|
|
{ fnRange = fnRange
|
|
, firstArg = firstArg
|
|
, argsAfterFirst = argsAfterFirst
|
|
, usingRightPizza = False
|
|
}
|
|
)
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Node _ (Expression.ParenthesizedExpression (Node lambdaRange (Expression.LambdaExpression lambda))) ->
|
|
appliedLambdaChecks
|
|
{ nodeRange = expressionRange
|
|
, lambdaRange = lambdaRange
|
|
, lambda = lambda
|
|
}
|
|
|
|
Node operatorRange (Expression.PrefixOperator operator) ->
|
|
case argsAfterFirst of
|
|
right :: [] ->
|
|
fullyAppliedPrefixOperatorChecks
|
|
{ operator = operator
|
|
, operatorRange = operatorRange
|
|
, left = firstArg
|
|
, right = right
|
|
}
|
|
|
|
_ ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
)
|
|
|
|
----------
|
|
-- (<|) --
|
|
----------
|
|
Expression.OperatorApplication "<|" _ pipedInto lastArg ->
|
|
case pipedInto of
|
|
Node fnRange (Expression.FunctionOrValue _ fnName) ->
|
|
onlyErrors
|
|
(case ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange of
|
|
Just moduleName ->
|
|
case Dict.get ( moduleName, fnName ) functionCallChecks of
|
|
Just checkFn ->
|
|
checkFn
|
|
(toCheckInfo
|
|
{ fnRange = fnRange
|
|
, firstArg = lastArg
|
|
, argsAfterFirst = []
|
|
, usingRightPizza = False
|
|
}
|
|
)
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
)
|
|
|
|
Node applicationRange (Expression.Application ((Node fnRange (Expression.FunctionOrValue _ fnName)) :: firstArg :: argsBetweenFirstAndLast)) ->
|
|
case ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange of
|
|
Just moduleName ->
|
|
case Dict.get ( moduleName, fnName ) functionCallChecks of
|
|
Just checkFn ->
|
|
errorsAndRangesToIgnore
|
|
(checkFn
|
|
(toCheckInfo
|
|
{ fnRange = fnRange
|
|
, firstArg = firstArg
|
|
, argsAfterFirst = argsBetweenFirstAndLast ++ [ lastArg ]
|
|
, usingRightPizza = False
|
|
}
|
|
)
|
|
)
|
|
(RangeDict.singleton applicationRange ())
|
|
|
|
Nothing ->
|
|
onlyErrors []
|
|
|
|
Nothing ->
|
|
onlyErrors []
|
|
|
|
pipedIntoOther ->
|
|
onlyErrors
|
|
(pipelineChecks
|
|
{ commentRanges = context.commentRanges
|
|
, extractSourceCode = context.extractSourceCode
|
|
, direction = RightToLeft
|
|
, nodeRange = expressionRange
|
|
, pipedInto = pipedIntoOther
|
|
, arg = lastArg
|
|
}
|
|
)
|
|
|
|
----------
|
|
-- (|>) --
|
|
----------
|
|
Expression.OperatorApplication "|>" _ lastArg pipedInto ->
|
|
case pipedInto of
|
|
Node fnRange (Expression.FunctionOrValue _ fnName) ->
|
|
onlyErrors
|
|
(case ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange of
|
|
Just moduleName ->
|
|
case Dict.get ( moduleName, fnName ) functionCallChecks of
|
|
Just checks ->
|
|
checks
|
|
(toCheckInfo
|
|
{ fnRange = fnRange
|
|
, firstArg = lastArg
|
|
, argsAfterFirst = []
|
|
, usingRightPizza = True
|
|
}
|
|
)
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
)
|
|
|
|
Node applicationRange (Expression.Application ((Node fnRange (Expression.FunctionOrValue _ fnName)) :: firstArg :: argsBetweenFirstAndLast)) ->
|
|
case ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange of
|
|
Just moduleName ->
|
|
case Dict.get ( moduleName, fnName ) functionCallChecks of
|
|
Just checks ->
|
|
errorsAndRangesToIgnore
|
|
(checks
|
|
(toCheckInfo
|
|
{ fnRange = fnRange
|
|
, firstArg = firstArg
|
|
, argsAfterFirst = argsBetweenFirstAndLast ++ [ lastArg ]
|
|
, usingRightPizza = True
|
|
}
|
|
)
|
|
)
|
|
(RangeDict.singleton applicationRange ())
|
|
|
|
Nothing ->
|
|
onlyErrors []
|
|
|
|
Nothing ->
|
|
onlyErrors []
|
|
|
|
pipedIntoOther ->
|
|
onlyErrors
|
|
(pipelineChecks
|
|
{ commentRanges = context.commentRanges
|
|
, extractSourceCode = context.extractSourceCode
|
|
, direction = LeftToRight
|
|
, nodeRange = expressionRange
|
|
, pipedInto = pipedIntoOther
|
|
, arg = lastArg
|
|
}
|
|
)
|
|
|
|
----------
|
|
-- (>>) --
|
|
----------
|
|
Expression.OperatorApplication ">>" _ earlier composedLater ->
|
|
let
|
|
( later, parentRange ) =
|
|
case composedLater of
|
|
Node _ (Expression.OperatorApplication ">>" _ later_ _) ->
|
|
( later_, { start = (Node.range earlier).start, end = (Node.range later_).end } )
|
|
|
|
endLater ->
|
|
( endLater, expressionRange )
|
|
in
|
|
onlyErrors
|
|
(compositionChecks
|
|
(toCompositionCheckInfo
|
|
{ direction = LeftToRight
|
|
, parentRange = parentRange
|
|
, earlier = earlier
|
|
, later = later
|
|
}
|
|
)
|
|
)
|
|
|
|
----------
|
|
-- (<<) --
|
|
----------
|
|
Expression.OperatorApplication "<<" _ composedLater earlier ->
|
|
let
|
|
( later, parentRange ) =
|
|
case composedLater of
|
|
Node _ (Expression.OperatorApplication "<<" _ _ later_) ->
|
|
( later_, { start = (Node.range later_).start, end = (Node.range earlier).end } )
|
|
|
|
endLater ->
|
|
( endLater, expressionRange )
|
|
in
|
|
onlyErrors
|
|
(compositionChecks
|
|
(toCompositionCheckInfo
|
|
{ direction = RightToLeft
|
|
, parentRange = parentRange
|
|
, earlier = earlier
|
|
, later = later
|
|
}
|
|
)
|
|
)
|
|
|
|
---------------------
|
|
-- OTHER OPERATION --
|
|
---------------------
|
|
Expression.OperatorApplication operator _ left right ->
|
|
case Dict.get operator operatorChecks of
|
|
Just checkFn ->
|
|
{ errors =
|
|
let
|
|
leftRange : Range
|
|
leftRange =
|
|
Node.range left
|
|
|
|
rightRange : Range
|
|
rightRange =
|
|
Node.range right
|
|
in
|
|
checkFn
|
|
{ lookupTable = context.lookupTable
|
|
, expectNaN = config.expectNaN
|
|
, importLookup = context.importLookup
|
|
, moduleBindings = context.moduleBindings
|
|
, localBindings = context.localBindings
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = expressionRange
|
|
, operator = operator
|
|
, operatorRange =
|
|
findOperatorRange
|
|
{ operator = operator
|
|
, commentRanges = context.commentRanges
|
|
, extractSourceCode = context.extractSourceCode
|
|
, leftRange = leftRange
|
|
, rightRange = rightRange
|
|
}
|
|
, left = left
|
|
, leftRange = leftRange
|
|
, right = right
|
|
, rightRange = rightRange
|
|
, isOnTheRightSideOfPlusPlus = RangeDict.member expressionRange context.rightSidesOfPlusPlus
|
|
}
|
|
, rangesToIgnore = RangeDict.empty
|
|
, rightSidesOfPlusPlus =
|
|
case operator of
|
|
"++" ->
|
|
RangeDict.singleton (Node.range (AstHelpers.removeParens right)) ()
|
|
|
|
_ ->
|
|
RangeDict.empty
|
|
, inferredConstants = []
|
|
}
|
|
|
|
Nothing ->
|
|
onlyErrors []
|
|
|
|
--------------
|
|
-- NEGATION --
|
|
--------------
|
|
Expression.Negation negatedExpression ->
|
|
onlyErrors
|
|
(negationChecks { parentRange = expressionRange, negatedExpression = negatedExpression })
|
|
|
|
-------------------
|
|
-- RECORD ACCESS --
|
|
-------------------
|
|
Expression.RecordAccess record field ->
|
|
case Node.value (AstHelpers.removeParens record) of
|
|
Expression.RecordExpr setters ->
|
|
onlyErrors (recordAccessChecks expressionRange Nothing (Node.value field) setters)
|
|
|
|
Expression.RecordUpdateExpression (Node recordNameRange _) setters ->
|
|
onlyErrors (recordAccessChecks expressionRange (Just recordNameRange) (Node.value field) setters)
|
|
|
|
Expression.LetExpression letIn ->
|
|
onlyErrors [ injectRecordAccessIntoLetExpression (Node.range record) letIn.expression field ]
|
|
|
|
Expression.IfBlock _ thenBranch elseBranch ->
|
|
onlyErrors (distributeFieldAccess "an if/then/else" (Node.range record) [ thenBranch, elseBranch ] field)
|
|
|
|
Expression.CaseExpression caseOf ->
|
|
onlyErrors (distributeFieldAccess "a case/of" (Node.range record) (List.map Tuple.second caseOf.cases) field)
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
--------
|
|
-- IF --
|
|
--------
|
|
Expression.IfBlock condition trueBranch falseBranch ->
|
|
let
|
|
ifCheckInfo : IfCheckInfo
|
|
ifCheckInfo =
|
|
{ nodeRange = expressionRange
|
|
, condition = condition
|
|
, trueBranch = trueBranch
|
|
, falseBranch = falseBranch
|
|
, lookupTable = context.lookupTable
|
|
, inferredConstants = context.inferredConstants
|
|
, importLookup = context.importLookup
|
|
, moduleBindings = context.moduleBindings
|
|
, localBindings = context.localBindings
|
|
}
|
|
in
|
|
case ifChecks ifCheckInfo of
|
|
Just ifErrors ->
|
|
errorsAndRangesToIgnore ifErrors.errors ifErrors.rangesToIgnore
|
|
|
|
Nothing ->
|
|
{ errors = []
|
|
, rangesToIgnore = RangeDict.empty
|
|
, rightSidesOfPlusPlus = RangeDict.empty
|
|
, inferredConstants =
|
|
Infer.inferForIfCondition
|
|
(Node.value (Normalize.normalize context condition))
|
|
{ trueBranchRange = Node.range trueBranch
|
|
, falseBranchRange = Node.range falseBranch
|
|
}
|
|
(Tuple.first context.inferredConstants)
|
|
}
|
|
|
|
-------------
|
|
-- CASE OF --
|
|
-------------
|
|
Expression.CaseExpression caseBlock ->
|
|
onlyErrors
|
|
(firstThatReportsError caseOfChecks
|
|
{ lookupTable = context.lookupTable
|
|
, extractSourceCode = context.extractSourceCode
|
|
, customTypesToReportInCases = context.customTypesToReportInCases
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = expressionRange
|
|
, caseOf = caseBlock
|
|
}
|
|
)
|
|
|
|
------------
|
|
-- LET IN --
|
|
------------
|
|
Expression.LetExpression caseBlock ->
|
|
onlyErrors (letInChecks caseBlock)
|
|
|
|
-------------------
|
|
-- RECORD UPDATE --
|
|
-------------------
|
|
Expression.RecordUpdateExpression variable fields ->
|
|
onlyErrors (removeRecordFields expressionRange variable fields)
|
|
|
|
--------------------
|
|
-- NOT SIMPLIFIED --
|
|
--------------------
|
|
Expression.UnitExpr ->
|
|
onlyErrors []
|
|
|
|
Expression.CharLiteral _ ->
|
|
onlyErrors []
|
|
|
|
Expression.Integer _ ->
|
|
onlyErrors []
|
|
|
|
Expression.Hex _ ->
|
|
onlyErrors []
|
|
|
|
Expression.Floatable _ ->
|
|
onlyErrors []
|
|
|
|
Expression.Literal _ ->
|
|
onlyErrors []
|
|
|
|
Expression.GLSLExpression _ ->
|
|
onlyErrors []
|
|
|
|
Expression.PrefixOperator _ ->
|
|
onlyErrors []
|
|
|
|
Expression.RecordAccessFunction _ ->
|
|
onlyErrors []
|
|
|
|
Expression.FunctionOrValue _ _ ->
|
|
onlyErrors []
|
|
|
|
Expression.ParenthesizedExpression _ ->
|
|
onlyErrors []
|
|
|
|
Expression.TupledExpression _ ->
|
|
onlyErrors []
|
|
|
|
Expression.ListExpr _ ->
|
|
onlyErrors []
|
|
|
|
Expression.RecordExpr _ ->
|
|
onlyErrors []
|
|
|
|
Expression.LambdaExpression _ ->
|
|
onlyErrors []
|
|
|
|
----------------------
|
|
-- IMPOSSIBLE CASES --
|
|
----------------------
|
|
Expression.Operator _ ->
|
|
onlyErrors []
|
|
|
|
Expression.Application [] ->
|
|
onlyErrors []
|
|
|
|
Expression.Application (_ :: []) ->
|
|
onlyErrors []
|
|
|
|
|
|
type alias CheckInfo =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, expectNaN : Bool
|
|
, importLookup : ImportLookup
|
|
, extractSourceCode : Range -> String
|
|
, commentRanges : List Range
|
|
, moduleBindings : Set String
|
|
, localBindings : RangeDict (Set String)
|
|
, inferredConstants : ( Infer.Inferred, List Infer.Inferred )
|
|
, parentRange : Range
|
|
, fnRange : Range
|
|
, usingRightPizza : Bool
|
|
, firstArg : Node Expression
|
|
, argsAfterFirst : List (Node Expression)
|
|
|
|
-- stored for quick access since usage is very common
|
|
-- prefer using secondArg and thirdArg functions
|
|
-- because the optimization could change in the future
|
|
, secondArg : Maybe (Node Expression)
|
|
, thirdArg : Maybe (Node Expression)
|
|
}
|
|
|
|
|
|
secondArg : CheckInfo -> Maybe (Node Expression)
|
|
secondArg checkInfo =
|
|
checkInfo.secondArg
|
|
|
|
|
|
thirdArg : CheckInfo -> Maybe (Node Expression)
|
|
thirdArg checkInfo =
|
|
checkInfo.thirdArg
|
|
|
|
|
|
functionCallChecks : Dict ( ModuleName, String ) (CheckInfo -> List (Error {}))
|
|
functionCallChecks =
|
|
Dict.fromList
|
|
[ ( ( [ "Basics" ], "identity" ), basicsIdentityChecks )
|
|
, ( ( [ "Basics" ], "always" ), basicsAlwaysChecks )
|
|
, ( ( [ "Basics" ], "not" ), basicsNotChecks )
|
|
, ( ( [ "Basics" ], "negate" ), basicsNegateChecks )
|
|
, ( ( [ "Maybe" ], "map" ), maybeMapChecks )
|
|
, ( ( [ "Maybe" ], "andThen" ), maybeAndThenChecks )
|
|
, ( ( [ "Maybe" ], "withDefault" ), maybeWithDefaultChecks )
|
|
, ( ( [ "Result" ], "map" ), resultMapChecks )
|
|
, ( ( [ "Result" ], "mapError" ), resultMapErrorChecks )
|
|
, ( ( [ "Result" ], "andThen" ), resultAndThenChecks )
|
|
, ( ( [ "Result" ], "withDefault" ), resultWithDefaultChecks )
|
|
, ( ( [ "Result" ], "toMaybe" ), resultToMaybeChecks )
|
|
, ( ( [ "List" ], "append" ), listAppendChecks )
|
|
, ( ( [ "List" ], "head" ), listHeadChecks )
|
|
, ( ( [ "List" ], "tail" ), listTailChecks )
|
|
, ( ( [ "List" ], "member" ), listMemberChecks )
|
|
, ( ( [ "List" ], "map" ), listMapChecks )
|
|
, ( ( [ "List" ], "filter" ), collectionFilterChecks listCollection )
|
|
, reportEmptyListSecondArgument ( ( [ "List" ], "filterMap" ), listFilterMapChecks )
|
|
, reportEmptyListFirstArgument ( ( [ "List" ], "concat" ), listConcatChecks )
|
|
, reportEmptyListSecondArgument ( ( [ "List" ], "concatMap" ), listConcatMapChecks )
|
|
, reportEmptyListSecondArgument ( ( [ "List" ], "indexedMap" ), listIndexedMapChecks )
|
|
, reportEmptyListSecondArgument ( ( [ "List" ], "intersperse" ), \_ -> [] )
|
|
, ( ( [ "List" ], "sum" ), listSumChecks )
|
|
, ( ( [ "List" ], "product" ), listProductChecks )
|
|
, ( ( [ "List" ], "minimum" ), listMinimumChecks )
|
|
, ( ( [ "List" ], "maximum" ), listMaximumChecks )
|
|
, ( ( [ "List" ], "foldl" ), listFoldlChecks )
|
|
, ( ( [ "List" ], "foldr" ), listFoldrChecks )
|
|
, ( ( [ "List" ], "all" ), listAllChecks )
|
|
, ( ( [ "List" ], "any" ), listAnyChecks )
|
|
, ( ( [ "List" ], "range" ), listRangeChecks )
|
|
, ( ( [ "List" ], "length" ), collectionSizeChecks listCollection )
|
|
, ( ( [ "List" ], "repeat" ), listRepeatChecks )
|
|
, ( ( [ "List" ], "isEmpty" ), collectionIsEmptyChecks listCollection )
|
|
, ( ( [ "List" ], "partition" ), collectionPartitionChecks listCollection )
|
|
, ( ( [ "List" ], "reverse" ), listReverseChecks )
|
|
, ( ( [ "List" ], "sort" ), listSortChecks )
|
|
, ( ( [ "List" ], "sortBy" ), listSortByChecks )
|
|
, ( ( [ "List" ], "sortWith" ), listSortWithChecks )
|
|
, ( ( [ "List" ], "take" ), listTakeChecks )
|
|
, ( ( [ "List" ], "drop" ), listDropChecks )
|
|
, ( ( [ "List" ], "map2" ), listMapNChecks { n = 2 } )
|
|
, ( ( [ "List" ], "map3" ), listMapNChecks { n = 3 } )
|
|
, ( ( [ "List" ], "map4" ), listMapNChecks { n = 4 } )
|
|
, ( ( [ "List" ], "map5" ), listMapNChecks { n = 5 } )
|
|
, ( ( [ "List" ], "unzip" ), listUnzipChecks )
|
|
, ( ( [ "Set" ], "map" ), collectionMapChecks setCollection )
|
|
, ( ( [ "Set" ], "filter" ), collectionFilterChecks setCollection )
|
|
, ( ( [ "Set" ], "remove" ), collectionRemoveChecks setCollection )
|
|
, ( ( [ "Set" ], "isEmpty" ), collectionIsEmptyChecks setCollection )
|
|
, ( ( [ "Set" ], "size" ), collectionSizeChecks setCollection )
|
|
, ( ( [ "Set" ], "member" ), collectionMemberChecks setCollection )
|
|
, ( ( [ "Set" ], "fromList" ), setFromListChecks )
|
|
, ( ( [ "Set" ], "toList" ), collectionToListChecks setCollection )
|
|
, ( ( [ "Set" ], "partition" ), collectionPartitionChecks setCollection )
|
|
, ( ( [ "Set" ], "intersect" ), collectionIntersectChecks setCollection )
|
|
, ( ( [ "Set" ], "diff" ), collectionDiffChecks setCollection )
|
|
, ( ( [ "Set" ], "union" ), collectionUnionChecks setCollection )
|
|
, ( ( [ "Set" ], "insert" ), collectionInsertChecks setCollection )
|
|
, ( ( [ "Dict" ], "isEmpty" ), collectionIsEmptyChecks dictCollection )
|
|
, ( ( [ "Dict" ], "fromList" ), collectionFromListChecks dictCollection )
|
|
, ( ( [ "Dict" ], "toList" ), collectionToListChecks dictCollection )
|
|
, ( ( [ "Dict" ], "size" ), collectionSizeChecks dictCollection )
|
|
, ( ( [ "Dict" ], "member" ), collectionMemberChecks dictCollection )
|
|
, ( ( [ "Dict" ], "partition" ), collectionPartitionChecks dictCollection )
|
|
, ( ( [ "String" ], "fromList" ), stringFromListChecks )
|
|
, ( ( [ "String" ], "isEmpty" ), stringIsEmptyChecks )
|
|
, ( ( [ "String" ], "concat" ), stringConcatChecks )
|
|
, ( ( [ "String" ], "join" ), stringJoinChecks )
|
|
, ( ( [ "String" ], "length" ), stringLengthChecks )
|
|
, ( ( [ "String" ], "repeat" ), stringRepeatChecks )
|
|
, ( ( [ "String" ], "replace" ), stringReplaceChecks )
|
|
, ( ( [ "String" ], "words" ), stringWordsChecks )
|
|
, ( ( [ "String" ], "lines" ), stringLinesChecks )
|
|
, ( ( [ "String" ], "reverse" ), stringReverseChecks )
|
|
, ( ( [ "String" ], "slice" ), stringSliceChecks )
|
|
, ( ( [ "String" ], "left" ), stringLeftChecks )
|
|
, ( ( [ "String" ], "right" ), stringRightChecks )
|
|
, ( ( [ "Platform", "Cmd" ], "batch" ), subAndCmdBatchChecks "Cmd" )
|
|
, ( ( [ "Platform", "Cmd" ], "map" ), collectionMapChecks cmdCollection )
|
|
, ( ( [ "Platform", "Sub" ], "batch" ), subAndCmdBatchChecks "Sub" )
|
|
, ( ( [ "Platform", "Sub" ], "map" ), collectionMapChecks subCollection )
|
|
, ( ( [ "Json", "Decode" ], "oneOf" ), oneOfChecks )
|
|
, ( ( [ "Html", "Attributes" ], "classList" ), htmlAttributesClassListChecks )
|
|
, ( ( [ "Parser" ], "oneOf" ), oneOfChecks )
|
|
, ( ( [ "Parser", "Advanced" ], "oneOf" ), oneOfChecks )
|
|
, ( ( [ "Random" ], "uniform" ), randomUniformChecks )
|
|
, ( ( [ "Random" ], "weighted" ), randomWeightedChecks )
|
|
, ( ( [ "Random" ], "list" ), randomListChecks )
|
|
, ( ( [ "Random" ], "map" ), randomMapChecks )
|
|
]
|
|
|
|
|
|
type alias OperatorCheckInfo =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, expectNaN : Bool
|
|
, importLookup : ImportLookup
|
|
, moduleBindings : Set String
|
|
, localBindings : RangeDict (Set String)
|
|
, inferredConstants : ( Infer.Inferred, List Infer.Inferred )
|
|
, parentRange : Range
|
|
, operator : String
|
|
, operatorRange : Range
|
|
, left : Node Expression
|
|
, leftRange : Range
|
|
, right : Node Expression
|
|
, rightRange : Range
|
|
, isOnTheRightSideOfPlusPlus : Bool
|
|
}
|
|
|
|
|
|
operatorChecks : Dict String (OperatorCheckInfo -> List (Error {}))
|
|
operatorChecks =
|
|
Dict.fromList
|
|
[ ( "+", plusChecks )
|
|
, ( "-", minusChecks )
|
|
, ( "*", multiplyChecks )
|
|
, ( "/", divisionChecks )
|
|
, ( "++", plusplusChecks )
|
|
, ( "::", consChecks )
|
|
, ( "||", orChecks )
|
|
, ( "&&", andChecks )
|
|
, ( "==", equalityChecks True )
|
|
, ( "/=", equalityChecks False )
|
|
, ( "<", comparisonChecks (<) )
|
|
, ( ">", comparisonChecks (>) )
|
|
, ( "<=", comparisonChecks (<=) )
|
|
, ( ">=", comparisonChecks (>=) )
|
|
]
|
|
|
|
|
|
type alias CompositionCheckInfo =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, importLookup : ImportLookup
|
|
, moduleBindings : Set String
|
|
, localBindings : RangeDict (Set String)
|
|
, direction : LeftOrRightDirection
|
|
, parentRange : Range
|
|
, earlier : Node Expression
|
|
, later : Node Expression
|
|
}
|
|
|
|
|
|
compositionChecks : CompositionCheckInfo -> List (Error {})
|
|
compositionChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> basicsIdentityCompositionChecks checkInfo
|
|
, \() -> basicsNotCompositionChecks checkInfo
|
|
, \() -> basicsNegateCompositionChecks checkInfo
|
|
, \() ->
|
|
case
|
|
( AstHelpers.getValueOrFunctionOrFunctionCall checkInfo.earlier
|
|
, AstHelpers.getValueOrFunctionOrFunctionCall checkInfo.later
|
|
)
|
|
of
|
|
( Just earlierFnOrCall, Just laterFnOrCall ) ->
|
|
case
|
|
( ModuleNameLookupTable.moduleNameAt checkInfo.lookupTable earlierFnOrCall.fnRange
|
|
, ModuleNameLookupTable.moduleNameAt checkInfo.lookupTable laterFnOrCall.fnRange
|
|
)
|
|
of
|
|
( Just earlierFnModuleName, Just laterFnModuleName ) ->
|
|
case Dict.get ( laterFnModuleName, laterFnOrCall.fnName ) compositionIntoChecks of
|
|
Just compositionIntoChecksForSpecificLater ->
|
|
compositionIntoChecksForSpecificLater
|
|
{ lookupTable = checkInfo.lookupTable
|
|
, importLookup = checkInfo.importLookup
|
|
, moduleBindings = checkInfo.moduleBindings
|
|
, localBindings = checkInfo.localBindings
|
|
, direction = checkInfo.direction
|
|
, parentRange = checkInfo.parentRange
|
|
, later =
|
|
{ range = laterFnOrCall.nodeRange
|
|
, fnRange = laterFnOrCall.fnRange
|
|
, args = laterFnOrCall.args
|
|
}
|
|
, earlier =
|
|
{ range = earlierFnOrCall.nodeRange
|
|
, fn = ( earlierFnModuleName, earlierFnOrCall.fnName )
|
|
, fnRange = earlierFnOrCall.fnRange
|
|
, args = earlierFnOrCall.args
|
|
}
|
|
}
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
( Nothing, _ ) ->
|
|
[]
|
|
|
|
( _, Nothing ) ->
|
|
[]
|
|
|
|
( Nothing, _ ) ->
|
|
[]
|
|
|
|
( _, Nothing ) ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
type alias CompositionIntoCheckInfo =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, importLookup : ImportLookup
|
|
, moduleBindings : Set String
|
|
, localBindings : RangeDict (Set String)
|
|
, direction : LeftOrRightDirection
|
|
, parentRange : Range
|
|
, later :
|
|
{ range : Range
|
|
, fnRange : Range
|
|
, args : List (Node Expression)
|
|
}
|
|
, earlier :
|
|
{ range : Range
|
|
, fn : ( ModuleName, String )
|
|
, fnRange : Range
|
|
, args : List (Node Expression)
|
|
}
|
|
}
|
|
|
|
|
|
compositionIntoChecks : Dict ( ModuleName, String ) (CompositionIntoCheckInfo -> List (Error {}))
|
|
compositionIntoChecks =
|
|
Dict.fromList
|
|
[ ( ( [ "Basics" ], "always" ), basicsAlwaysCompositionChecks )
|
|
, ( ( [ "Maybe" ], "map" ), maybeMapCompositionChecks )
|
|
, ( ( [ "Result" ], "map" ), resultMapCompositionChecks )
|
|
, ( ( [ "Result" ], "mapError" ), resultMapErrorCompositionChecks )
|
|
, ( ( [ "Result" ], "toMaybe" ), resultToMaybeCompositionChecks )
|
|
, ( ( [ "List" ], "map" ), listMapCompositionChecks )
|
|
, ( ( [ "List" ], "filterMap" ), listFilterMapCompositionChecks )
|
|
, ( ( [ "List" ], "concat" ), listConcatCompositionChecks )
|
|
, ( ( [ "List" ], "foldl" ), listFoldlCompositionChecks )
|
|
, ( ( [ "List" ], "foldr" ), listFoldrCompositionChecks )
|
|
, ( ( [ "Set" ], "fromList" ), setFromListCompositionChecks )
|
|
, ( ( [ "Random" ], "map" ), randomMapCompositionChecks )
|
|
]
|
|
|
|
|
|
removeAlongWithOtherFunctionCheck :
|
|
{ message : String, details : List String }
|
|
-> (ModuleNameLookupTable -> Node Expression -> Maybe Range)
|
|
-> CheckInfo
|
|
-> List (Error {})
|
|
removeAlongWithOtherFunctionCheck errorMessage secondFunctionCheck checkInfo =
|
|
case Node.value (AstHelpers.removeParens checkInfo.firstArg) of
|
|
Expression.Application (secondFn :: firstArgOfSecondCall :: _) ->
|
|
case secondFunctionCheck checkInfo.lookupTable secondFn of
|
|
Just secondRange ->
|
|
[ Rule.errorWithFix
|
|
errorMessage
|
|
(Range.combine [ checkInfo.fnRange, secondRange ])
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.firstArg }
|
|
++ replaceBySubExpressionFix (Node.range checkInfo.firstArg)
|
|
firstArgOfSecondCall
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Expression.OperatorApplication "|>" _ firstArgOfSecondCall secondFn ->
|
|
case secondFunctionCheck checkInfo.lookupTable secondFn of
|
|
Just secondRange ->
|
|
[ Rule.errorWithFix
|
|
errorMessage
|
|
(Range.combine [ checkInfo.fnRange, secondRange ])
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.firstArg }
|
|
++ replaceBySubExpressionFix (Node.range checkInfo.firstArg)
|
|
firstArgOfSecondCall
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Expression.OperatorApplication "<|" _ secondFn firstArgOfSecondCall ->
|
|
case secondFunctionCheck checkInfo.lookupTable secondFn of
|
|
Just secondRange ->
|
|
[ Rule.errorWithFix
|
|
errorMessage
|
|
(Range.combine [ checkInfo.fnRange, secondRange ])
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.firstArg }
|
|
++ replaceBySubExpressionFix (Node.range checkInfo.firstArg)
|
|
firstArgOfSecondCall
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
findOperatorRange :
|
|
{ extractSourceCode : Range -> String
|
|
, commentRanges : List Range
|
|
, operator : String
|
|
, leftRange : Range
|
|
, rightRange : Range
|
|
}
|
|
-> Range
|
|
findOperatorRange context =
|
|
let
|
|
betweenOperands : String
|
|
betweenOperands =
|
|
context.extractSourceCode
|
|
{ start = context.leftRange.end, end = context.rightRange.start }
|
|
|
|
operatorStartLocationFound : Maybe Location
|
|
operatorStartLocationFound =
|
|
String.indexes context.operator betweenOperands
|
|
|> findMap
|
|
(\operatorOffset ->
|
|
let
|
|
operatorStartLocation : Location
|
|
operatorStartLocation =
|
|
offsetInStringToLocation
|
|
{ offset = operatorOffset
|
|
, startLocation = context.leftRange.end
|
|
, source = betweenOperands
|
|
}
|
|
|
|
isPartOfComment : Bool
|
|
isPartOfComment =
|
|
List.any
|
|
(\commentRange ->
|
|
rangeContainsLocation operatorStartLocation commentRange
|
|
)
|
|
context.commentRanges
|
|
in
|
|
if isPartOfComment then
|
|
Nothing
|
|
|
|
else
|
|
Just operatorStartLocation
|
|
)
|
|
in
|
|
case operatorStartLocationFound of
|
|
Just operatorStartLocation ->
|
|
{ start = operatorStartLocation
|
|
, end =
|
|
{ row = operatorStartLocation.row
|
|
, column = operatorStartLocation.column + String.length context.operator
|
|
}
|
|
}
|
|
|
|
-- there's a bug somewhere
|
|
Nothing ->
|
|
Range.emptyRange
|
|
|
|
|
|
offsetInStringToLocation : { offset : Int, source : String, startLocation : Location } -> Location
|
|
offsetInStringToLocation config =
|
|
case config.source |> String.left config.offset |> String.lines |> List.reverse of
|
|
[] ->
|
|
config.startLocation
|
|
|
|
onlyLine :: [] ->
|
|
{ row = config.startLocation.row
|
|
, column = config.startLocation.column + String.length onlyLine
|
|
}
|
|
|
|
lineWithOffsetLocation :: _ :: linesBeforeBeforeWithOffsetLocation ->
|
|
{ row = config.startLocation.row + 1 + List.length linesBeforeBeforeWithOffsetLocation
|
|
, column = 1 + String.length lineWithOffsetLocation
|
|
}
|
|
|
|
|
|
plusChecks : OperatorCheckInfo -> List (Error {})
|
|
plusChecks checkInfo =
|
|
firstThatReportsError
|
|
[ addingZeroCheck
|
|
, addingOppositesCheck
|
|
]
|
|
checkInfo
|
|
|
|
|
|
addingZeroCheck : OperatorCheckInfo -> List (Error {})
|
|
addingZeroCheck checkInfo =
|
|
findMap
|
|
(\side ->
|
|
if AstHelpers.getUncomputedNumberValue side.node == Just 0 then
|
|
Just
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary addition with 0"
|
|
, details = [ "Adding 0 does not change the value of the number." ]
|
|
}
|
|
side.errorRange
|
|
[ Fix.removeRange side.removeRange ]
|
|
]
|
|
|
|
else
|
|
Nothing
|
|
)
|
|
(operationToSides checkInfo)
|
|
|> Maybe.withDefault []
|
|
|
|
|
|
addingOppositesCheck : OperatorCheckInfo -> List (Error {})
|
|
addingOppositesCheck checkInfo =
|
|
if checkInfo.expectNaN then
|
|
[]
|
|
|
|
else
|
|
case Normalize.compare checkInfo checkInfo.left (Node Range.emptyRange (Expression.Negation checkInfo.right)) of
|
|
Normalize.ConfirmedEquality ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Addition always results in 0"
|
|
, details = [ "These two expressions have an equal absolute value but an opposite sign. This means adding them they will cancel out to 0." ]
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "0" ]
|
|
]
|
|
|
|
Normalize.ConfirmedInequality ->
|
|
[]
|
|
|
|
Normalize.Unconfirmed ->
|
|
[]
|
|
|
|
|
|
minusChecks : OperatorCheckInfo -> List (Error {})
|
|
minusChecks checkInfo =
|
|
if AstHelpers.getUncomputedNumberValue checkInfo.right == Just 0 then
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary subtraction with 0"
|
|
, details = [ "Subtracting 0 does not change the value of the number." ]
|
|
}
|
|
(errorToRightRange checkInfo)
|
|
[ Fix.removeRange (fixToRightRange checkInfo) ]
|
|
]
|
|
|
|
else if AstHelpers.getUncomputedNumberValue checkInfo.left == Just 0 then
|
|
let
|
|
replacedRange : Range
|
|
replacedRange =
|
|
fixToLeftRange checkInfo
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary subtracting from 0"
|
|
, details = [ "You can negate the expression on the right like `-n`." ]
|
|
}
|
|
(errorToLeftRange checkInfo)
|
|
(if needsParens (Node.value checkInfo.right) then
|
|
[ Fix.replaceRangeBy replacedRange "-(", Fix.insertAt checkInfo.rightRange.end ")" ]
|
|
|
|
else
|
|
[ Fix.replaceRangeBy replacedRange "-" ]
|
|
)
|
|
]
|
|
|
|
else if checkInfo.expectNaN then
|
|
[]
|
|
|
|
else
|
|
checkIfMinusResultsInZero checkInfo
|
|
|
|
|
|
checkIfMinusResultsInZero : OperatorCheckInfo -> List (Error {})
|
|
checkIfMinusResultsInZero checkInfo =
|
|
case Normalize.compare checkInfo checkInfo.left checkInfo.right of
|
|
Normalize.ConfirmedEquality ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Subtraction always results in 0"
|
|
, details = [ "These two expressions have the same value, which means they will cancel add when subtracting one by the other." ]
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "0" ]
|
|
]
|
|
|
|
Normalize.ConfirmedInequality ->
|
|
[]
|
|
|
|
Normalize.Unconfirmed ->
|
|
[]
|
|
|
|
|
|
multiplyChecks : OperatorCheckInfo -> List (Error {})
|
|
multiplyChecks checkInfo =
|
|
findMap
|
|
(\side ->
|
|
case AstHelpers.getUncomputedNumberValue side.node of
|
|
Just number ->
|
|
if number == 1 then
|
|
Just
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary multiplication by 1"
|
|
, details = [ "Multiplying by 1 does not change the value of the number." ]
|
|
}
|
|
side.errorRange
|
|
[ Fix.removeRange side.removeRange ]
|
|
]
|
|
|
|
else if number == 0 then
|
|
Just
|
|
[ Rule.errorWithFix
|
|
{ message = "Multiplication by 0 should be replaced"
|
|
, details =
|
|
[ "Multiplying by 0 will turn finite numbers into 0 and keep NaN and (-)Infinity"
|
|
, "Most likely, multiplying by 0 was unintentional and you had a different factor in mind."
|
|
, """If you do want the described behavior, though, make your intention clear for the reader
|
|
by explicitly checking for `Basics.isNaN` and `Basics.isInfinite`."""
|
|
, """Basics.isNaN: https://package.elm-lang.org/packages/elm/core/latest/Basics#isNaN
|
|
Basics.isInfinite: https://package.elm-lang.org/packages/elm/core/latest/Basics#isInfinite"""
|
|
]
|
|
}
|
|
side.errorRange
|
|
(if checkInfo.expectNaN then
|
|
[]
|
|
|
|
else
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "0" ]
|
|
)
|
|
]
|
|
|
|
else
|
|
Nothing
|
|
|
|
Nothing ->
|
|
Nothing
|
|
)
|
|
(operationToSides checkInfo)
|
|
|> Maybe.withDefault []
|
|
|
|
|
|
operationToSides : OperatorCheckInfo -> List { node : Node Expression, removeRange : Range, errorRange : Range }
|
|
operationToSides checkInfo =
|
|
[ { node = checkInfo.right
|
|
, removeRange = fixToRightRange checkInfo
|
|
, errorRange = errorToRightRange checkInfo
|
|
}
|
|
, { node = checkInfo.left
|
|
, removeRange = fixToLeftRange checkInfo
|
|
, errorRange = errorToLeftRange checkInfo
|
|
}
|
|
]
|
|
|
|
|
|
fixToLeftRange : { checkInfo | leftRange : Range, rightRange : Range } -> Range
|
|
fixToLeftRange checkInfo =
|
|
{ start = checkInfo.leftRange.start, end = checkInfo.rightRange.start }
|
|
|
|
|
|
errorToLeftRange : { checkInfo | leftRange : Range, operatorRange : Range } -> Range
|
|
errorToLeftRange checkInfo =
|
|
{ start = checkInfo.leftRange.start, end = checkInfo.operatorRange.end }
|
|
|
|
|
|
fixToRightRange : { checkInfo | leftRange : Range, rightRange : Range } -> Range
|
|
fixToRightRange checkInfo =
|
|
{ start = checkInfo.leftRange.end, end = checkInfo.rightRange.end }
|
|
|
|
|
|
errorToRightRange : { checkInfo | rightRange : Range, operatorRange : Range } -> Range
|
|
errorToRightRange checkInfo =
|
|
{ start = checkInfo.operatorRange.start, end = checkInfo.rightRange.end }
|
|
|
|
|
|
divisionChecks : OperatorCheckInfo -> List (Error {})
|
|
divisionChecks checkInfo =
|
|
if AstHelpers.getUncomputedNumberValue checkInfo.right == Just 1 then
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary division by 1"
|
|
, details = [ "Dividing by 1 does not change the value of the number." ]
|
|
}
|
|
(errorToRightRange checkInfo)
|
|
[ Fix.removeRange (fixToRightRange checkInfo) ]
|
|
]
|
|
|
|
else if not checkInfo.expectNaN && (AstHelpers.getUncomputedNumberValue checkInfo.left == Just 0) then
|
|
[ Rule.errorWithFix
|
|
{ message = "Dividing 0 always returns 0"
|
|
, details =
|
|
[ "Dividing 0 by anything, even infinite numbers, gives 0 which means you can replace the whole division operation by 0."
|
|
, "Most likely, dividing 0 was unintentional and you had a different number in mind."
|
|
]
|
|
}
|
|
(errorToLeftRange checkInfo)
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = checkInfo.leftRange })
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
plusplusChecks : OperatorCheckInfo -> List (Error {})
|
|
plusplusChecks checkInfo =
|
|
case ( Node.value checkInfo.left, Node.value checkInfo.right ) of
|
|
( Expression.Literal "", Expression.Literal _ ) ->
|
|
[ errorForAddingEmptyStrings
|
|
{ removed =
|
|
{ start = checkInfo.leftRange.start
|
|
, end = checkInfo.rightRange.start
|
|
}
|
|
, error =
|
|
{ start = checkInfo.leftRange.start
|
|
, end = checkInfo.operatorRange.end
|
|
}
|
|
}
|
|
]
|
|
|
|
( Expression.Literal _, Expression.Literal "" ) ->
|
|
[ errorForAddingEmptyStrings
|
|
{ removed =
|
|
{ start = checkInfo.leftRange.end
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
, error =
|
|
{ start = checkInfo.operatorRange.start
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
}
|
|
]
|
|
|
|
( Expression.ListExpr [], _ ) ->
|
|
[ errorForAddingEmptyLists
|
|
{ removed =
|
|
{ start = checkInfo.leftRange.start
|
|
, end = checkInfo.rightRange.start
|
|
}
|
|
, error =
|
|
{ start = checkInfo.leftRange.start
|
|
, end = checkInfo.operatorRange.end
|
|
}
|
|
}
|
|
]
|
|
|
|
( _, Expression.ListExpr [] ) ->
|
|
[ errorForAddingEmptyLists
|
|
{ removed =
|
|
{ start = checkInfo.leftRange.end
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
, error =
|
|
{ start = checkInfo.operatorRange.start
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
}
|
|
]
|
|
|
|
( Expression.ListExpr _, Expression.ListExpr _ ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Expression could be simplified to be a single List"
|
|
, details = [ "Try moving all the elements into a single list." ]
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.replaceRangeBy
|
|
{ start = { row = checkInfo.leftRange.end.row, column = checkInfo.leftRange.end.column - 1 }
|
|
, end = { row = checkInfo.rightRange.start.row, column = checkInfo.rightRange.start.column + 1 }
|
|
}
|
|
","
|
|
]
|
|
]
|
|
|
|
( Expression.ListExpr (listElement :: []), _ ) ->
|
|
if checkInfo.isOnTheRightSideOfPlusPlus then
|
|
[]
|
|
|
|
else
|
|
[ Rule.errorWithFix
|
|
{ message = "Should use (::) instead of (++)"
|
|
, details = [ "Concatenating a list with a single value is the same as using (::) on the list with the value." ]
|
|
}
|
|
checkInfo.parentRange
|
|
(Fix.replaceRangeBy checkInfo.operatorRange
|
|
"::"
|
|
:: replaceBySubExpressionFix checkInfo.leftRange listElement
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
errorForAddingEmptyStrings : { error : Range, removed : Range } -> Error {}
|
|
errorForAddingEmptyStrings ranges =
|
|
Rule.errorWithFix
|
|
{ message = "Unnecessary concatenation with an empty string"
|
|
, details = [ "You should remove the concatenation with the empty string." ]
|
|
}
|
|
ranges.error
|
|
[ Fix.removeRange ranges.removed ]
|
|
|
|
|
|
errorForAddingEmptyLists : { error : Range, removed : Range } -> Error {}
|
|
errorForAddingEmptyLists ranges =
|
|
Rule.errorWithFix
|
|
{ message = "Unnecessary concatenation with an empty list"
|
|
, details = [ "You should remove the concatenation with the empty list." ]
|
|
}
|
|
ranges.error
|
|
[ Fix.removeRange ranges.removed ]
|
|
|
|
|
|
consChecks : OperatorCheckInfo -> List (Error {})
|
|
consChecks checkInfo =
|
|
case Node.value checkInfo.right of
|
|
Expression.ListExpr tailElements ->
|
|
let
|
|
fix : List Fix
|
|
fix =
|
|
case tailElements of
|
|
[] ->
|
|
[ Fix.insertAt checkInfo.leftRange.start "[ "
|
|
, Fix.replaceRangeBy
|
|
{ start = checkInfo.leftRange.end
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
" ]"
|
|
]
|
|
|
|
_ :: _ ->
|
|
[ Fix.insertAt checkInfo.leftRange.start "[ "
|
|
, Fix.replaceRangeBy checkInfo.operatorRange ","
|
|
, Fix.removeRange (leftBoundaryRange checkInfo.rightRange)
|
|
]
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Element added to the beginning of the list could be included in the list"
|
|
, details = [ "Try moving the element inside the list it is being added to." ]
|
|
}
|
|
checkInfo.operatorRange
|
|
fix
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
toggleCompositionChecks : ( ModuleName, String ) -> CompositionCheckInfo -> List (Error {})
|
|
toggleCompositionChecks toggle checkInfo =
|
|
let
|
|
errorInfo : { message : String, details : List String }
|
|
errorInfo =
|
|
let
|
|
toggleFullyQualifiedAsString : String
|
|
toggleFullyQualifiedAsString =
|
|
qualifiedToString toggle
|
|
in
|
|
-- TODO rework error info
|
|
{ message = "Unnecessary double " ++ toggleFullyQualifiedAsString
|
|
, details = [ "Composing " ++ toggleFullyQualifiedAsString ++ " with " ++ toggleFullyQualifiedAsString ++ " cancels each other out." ]
|
|
}
|
|
|
|
getToggleFn : Node Expression -> Maybe Range
|
|
getToggleFn =
|
|
AstHelpers.getSpecificValueOrFunction toggle checkInfo.lookupTable
|
|
|
|
maybeEarlierToggleFn : Maybe Range
|
|
maybeEarlierToggleFn =
|
|
getToggleFn checkInfo.earlier
|
|
|
|
maybeLaterToggleFn : Maybe Range
|
|
maybeLaterToggleFn =
|
|
getToggleFn checkInfo.later
|
|
|
|
getToggleComposition : { earlierToLater : Bool } -> Node Expression -> Maybe { removeFix : List Fix, range : Range }
|
|
getToggleComposition takeFirstFunction expressionNode =
|
|
case AstHelpers.getComposition expressionNode of
|
|
Just composition ->
|
|
if takeFirstFunction.earlierToLater then
|
|
getToggleFn composition.earlier
|
|
|> Maybe.map
|
|
(\toggleFn ->
|
|
{ range = toggleFn
|
|
, removeFix = keepOnlyFix { parentRange = composition.parentRange, keep = Node.range composition.later }
|
|
}
|
|
)
|
|
|
|
else
|
|
getToggleFn composition.later
|
|
|> Maybe.map
|
|
(\toggleFn ->
|
|
{ range = toggleFn
|
|
, removeFix = keepOnlyFix { parentRange = composition.parentRange, keep = Node.range composition.earlier }
|
|
}
|
|
)
|
|
|
|
Nothing ->
|
|
Nothing
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case ( maybeEarlierToggleFn, maybeLaterToggleFn ) of
|
|
( Just _, Just _ ) ->
|
|
[ Rule.errorWithFix
|
|
errorInfo
|
|
checkInfo.parentRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "identity" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
( Nothing, _ ) ->
|
|
[]
|
|
|
|
( _, Nothing ) ->
|
|
[]
|
|
, \() ->
|
|
case maybeEarlierToggleFn of
|
|
Just earlierToggleFn ->
|
|
case getToggleComposition { earlierToLater = True } checkInfo.later of
|
|
Just laterToggle ->
|
|
[ Rule.errorWithFix
|
|
errorInfo
|
|
(Range.combine [ earlierToggleFn, laterToggle.range ])
|
|
(laterToggle.removeFix
|
|
++ keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.later }
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case maybeLaterToggleFn of
|
|
Just laterToggleFn ->
|
|
case getToggleComposition { earlierToLater = False } checkInfo.earlier of
|
|
Just earlierToggle ->
|
|
[ Rule.errorWithFix
|
|
errorInfo
|
|
(Range.combine [ earlierToggle.range, laterToggleFn ])
|
|
(earlierToggle.removeFix
|
|
++ keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.earlier }
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
toggleChainErrorInfo : ( ModuleName, String ) -> { message : String, details : List String }
|
|
toggleChainErrorInfo toggle =
|
|
let
|
|
toggleFullyQualifiedAsString : String
|
|
toggleFullyQualifiedAsString =
|
|
qualifiedToString toggle
|
|
in
|
|
-- TODO rework error info
|
|
{ message = "Unnecessary double " ++ toggleFullyQualifiedAsString
|
|
, details = [ "Composing " ++ toggleFullyQualifiedAsString ++ " with " ++ toggleFullyQualifiedAsString ++ " cancels each other out." ]
|
|
}
|
|
|
|
|
|
|
|
-- NEGATE
|
|
|
|
|
|
basicsNegateCompositionChecks : CompositionCheckInfo -> List (Error {})
|
|
basicsNegateCompositionChecks checkInfo =
|
|
toggleCompositionChecks ( [ "Basics" ], "negate" ) checkInfo
|
|
|
|
|
|
basicsNegateChecks : CheckInfo -> List (Error {})
|
|
basicsNegateChecks checkInfo =
|
|
removeAlongWithOtherFunctionCheck
|
|
(toggleChainErrorInfo ( [ "Basics" ], "negate" ))
|
|
(AstHelpers.getSpecificValueOrFunction ( [ "Basics" ], "negate" ))
|
|
checkInfo
|
|
|
|
|
|
|
|
-- BOOLEAN
|
|
|
|
|
|
basicsNotChecks : CheckInfo -> List (Error {})
|
|
basicsNotChecks checkInfo =
|
|
firstThatReportsError
|
|
[ notOnKnownBoolCheck
|
|
, removeAlongWithOtherFunctionCheck
|
|
(toggleChainErrorInfo ( [ "Basics" ], "not" ))
|
|
(AstHelpers.getSpecificValueOrFunction ( [ "Basics" ], "not" ))
|
|
, isNotOnBooleanOperatorCheck
|
|
]
|
|
checkInfo
|
|
|
|
|
|
notOnKnownBoolCheck : CheckInfo -> List (Error {})
|
|
notOnKnownBoolCheck checkInfo =
|
|
case Evaluate.getBoolean checkInfo checkInfo.firstArg of
|
|
Determined bool ->
|
|
let
|
|
notBoolAsString : String
|
|
notBoolAsString =
|
|
AstHelpers.boolToString (not bool)
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Expression is equal to " ++ notBoolAsString
|
|
, details = [ "You can replace the call to `not` by the boolean value directly." ]
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], notBoolAsString ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
isNotOnBooleanOperatorCheck : CheckInfo -> List (Error {})
|
|
isNotOnBooleanOperatorCheck checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ParenthesizedExpression (Node _ (Expression.OperatorApplication operator _ (Node leftRange _) (Node rightRange _))) ->
|
|
case isNegatableOperator operator of
|
|
Just replacement ->
|
|
let
|
|
operatorRange : Range
|
|
operatorRange =
|
|
findOperatorRange
|
|
{ operator = operator
|
|
, commentRanges = checkInfo.commentRanges
|
|
, extractSourceCode = checkInfo.extractSourceCode
|
|
, leftRange = leftRange
|
|
, rightRange = rightRange
|
|
}
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "`not` is used on a negatable boolean operation"
|
|
, details = [ "You can remove the `not` call and use `" ++ replacement ++ "` instead." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange { start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
, Fix.replaceRangeBy operatorRange replacement
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
isNegatableOperator : String -> Maybe String
|
|
isNegatableOperator op =
|
|
case op of
|
|
"<" ->
|
|
Just ">="
|
|
|
|
">" ->
|
|
Just "<="
|
|
|
|
"<=" ->
|
|
Just ">"
|
|
|
|
">=" ->
|
|
Just "<"
|
|
|
|
"==" ->
|
|
Just "/="
|
|
|
|
"/=" ->
|
|
Just "=="
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
basicsNotCompositionChecks : CompositionCheckInfo -> List (Error {})
|
|
basicsNotCompositionChecks checkInfo =
|
|
toggleCompositionChecks ( [ "Basics" ], "not" ) checkInfo
|
|
|
|
|
|
orChecks : OperatorCheckInfo -> List (Error {})
|
|
orChecks operatorCheckInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
List.concat
|
|
[ or_isLeftSimplifiableError operatorCheckInfo
|
|
, or_isRightSimplifiableError operatorCheckInfo
|
|
]
|
|
, \() -> findSimilarConditionsError operatorCheckInfo
|
|
]
|
|
()
|
|
|
|
|
|
type RedundantConditionResolution
|
|
= RemoveFrom Location
|
|
| ReplaceByNoop Bool
|
|
|
|
|
|
findSimilarConditionsError : OperatorCheckInfo -> List (Error {})
|
|
findSimilarConditionsError operatorCheckInfo =
|
|
let
|
|
conditionsOnTheRight : List ( RedundantConditionResolution, Node Expression )
|
|
conditionsOnTheRight =
|
|
listConditions
|
|
operatorCheckInfo.operator
|
|
(RemoveFrom operatorCheckInfo.leftRange.end)
|
|
operatorCheckInfo.right
|
|
|
|
errorsForNode : Node Expression -> List (Error {})
|
|
errorsForNode nodeToCompareTo =
|
|
List.concatMap
|
|
(areSimilarConditionsError
|
|
operatorCheckInfo
|
|
operatorCheckInfo.operator
|
|
nodeToCompareTo
|
|
)
|
|
conditionsOnTheRight
|
|
in
|
|
operatorCheckInfo.left
|
|
|> listConditions operatorCheckInfo.operator (RemoveFrom operatorCheckInfo.leftRange.end)
|
|
|> List.concatMap (Tuple.second >> errorsForNode)
|
|
|
|
|
|
areSimilarConditionsError :
|
|
QualifyResources (Infer.Resources a)
|
|
-> String
|
|
-> Node Expression
|
|
-> ( RedundantConditionResolution, Node Expression )
|
|
-> List (Error {})
|
|
areSimilarConditionsError resources operator nodeToCompareTo ( redundantConditionResolution, nodeToLookAt ) =
|
|
case Normalize.compare resources nodeToCompareTo nodeToLookAt of
|
|
Normalize.ConfirmedEquality ->
|
|
errorForRedundantCondition operator redundantConditionResolution nodeToLookAt resources
|
|
|
|
Normalize.ConfirmedInequality ->
|
|
[]
|
|
|
|
Normalize.Unconfirmed ->
|
|
[]
|
|
|
|
|
|
errorForRedundantCondition : String -> RedundantConditionResolution -> Node a -> QualifyResources b -> List (Error {})
|
|
errorForRedundantCondition operator redundantConditionResolution node qualifyResources =
|
|
let
|
|
( range, fix ) =
|
|
rangeAndFixForRedundantCondition redundantConditionResolution node qualifyResources
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Condition is redundant"
|
|
, details =
|
|
[ "This condition is the same as another one found on the left side of the (" ++ operator ++ ") operator, therefore one of them can be removed."
|
|
]
|
|
}
|
|
range
|
|
fix
|
|
]
|
|
|
|
|
|
rangeAndFixForRedundantCondition : RedundantConditionResolution -> Node a -> QualifyResources b -> ( Range, List Fix )
|
|
rangeAndFixForRedundantCondition redundantConditionResolution (Node nodeRange _) qualifyResources =
|
|
case redundantConditionResolution of
|
|
RemoveFrom locationOfPrevElement ->
|
|
let
|
|
range : Range
|
|
range =
|
|
{ start = locationOfPrevElement
|
|
, end = nodeRange.end
|
|
}
|
|
in
|
|
( range
|
|
, [ Fix.removeRange range ]
|
|
)
|
|
|
|
ReplaceByNoop noopValue ->
|
|
( nodeRange
|
|
, [ Fix.replaceRangeBy nodeRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], AstHelpers.boolToString noopValue ) qualifyResources))
|
|
]
|
|
)
|
|
|
|
|
|
listConditions : String -> RedundantConditionResolution -> Node Expression -> List ( RedundantConditionResolution, Node Expression )
|
|
listConditions operatorToLookFor redundantConditionResolution expressionNode =
|
|
case Node.value expressionNode of
|
|
Expression.ParenthesizedExpression expr ->
|
|
let
|
|
noopValue : Bool
|
|
noopValue =
|
|
operatorToLookFor == "&&"
|
|
in
|
|
listConditions operatorToLookFor (ReplaceByNoop noopValue) expr
|
|
|
|
Expression.OperatorApplication operator _ left right ->
|
|
if operator == operatorToLookFor then
|
|
listConditions operatorToLookFor redundantConditionResolution left
|
|
++ listConditions operatorToLookFor (RemoveFrom (Node.range left).end) right
|
|
|
|
else
|
|
[ ( redundantConditionResolution, expressionNode ) ]
|
|
|
|
_ ->
|
|
[ ( redundantConditionResolution, expressionNode ) ]
|
|
|
|
|
|
or_isLeftSimplifiableError : OperatorCheckInfo -> List (Error {})
|
|
or_isLeftSimplifiableError checkInfo =
|
|
case Evaluate.getBoolean checkInfo checkInfo.left of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Comparison is always True"
|
|
, details = alwaysSameDetails
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.leftRange.end
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.leftRange.start
|
|
, end = checkInfo.rightRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
or_isRightSimplifiableError : OperatorCheckInfo -> List (Error {})
|
|
or_isRightSimplifiableError checkInfo =
|
|
case Evaluate.getBoolean checkInfo checkInfo.right of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.leftRange.start
|
|
, end = checkInfo.rightRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.leftRange.end
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
andChecks : OperatorCheckInfo -> List (Error {})
|
|
andChecks operatorCheckInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
List.concat
|
|
[ and_isLeftSimplifiableError operatorCheckInfo
|
|
, and_isRightSimplifiableError operatorCheckInfo
|
|
]
|
|
, \() -> findSimilarConditionsError operatorCheckInfo
|
|
]
|
|
()
|
|
|
|
|
|
and_isLeftSimplifiableError : OperatorCheckInfo -> List (Error {})
|
|
and_isLeftSimplifiableError checkInfo =
|
|
case Evaluate.getBoolean checkInfo checkInfo.left of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.leftRange.start
|
|
, end = checkInfo.rightRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Comparison is always False"
|
|
, details = alwaysSameDetails
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.leftRange.end
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
and_isRightSimplifiableError : OperatorCheckInfo -> List (Error {})
|
|
and_isRightSimplifiableError checkInfo =
|
|
case Evaluate.getBoolean checkInfo checkInfo.right of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.leftRange.end
|
|
, end = checkInfo.rightRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Comparison is always False"
|
|
, details = alwaysSameDetails
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.leftRange.start
|
|
, end = checkInfo.rightRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
|
|
-- EQUALITY
|
|
|
|
|
|
equalityChecks : Bool -> OperatorCheckInfo -> List (Error {})
|
|
equalityChecks isEqual checkInfo =
|
|
if Evaluate.getBoolean checkInfo checkInfo.right == Determined isEqual then
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary comparison with boolean"
|
|
, details = [ "The result of the expression will be the same with or without the comparison." ]
|
|
}
|
|
(errorToRightRange checkInfo)
|
|
[ Fix.removeRange (fixToRightRange checkInfo) ]
|
|
]
|
|
|
|
else if Evaluate.getBoolean checkInfo checkInfo.left == Determined isEqual then
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary comparison with boolean"
|
|
, details = [ "The result of the expression will be the same with or without the comparison." ]
|
|
}
|
|
(errorToLeftRange checkInfo)
|
|
[ Fix.removeRange (fixToLeftRange checkInfo) ]
|
|
]
|
|
|
|
else
|
|
case
|
|
Maybe.map2 Tuple.pair
|
|
(AstHelpers.getSpecificFunctionCall ( [ "Basics" ], "not" ) checkInfo.lookupTable checkInfo.left)
|
|
(AstHelpers.getSpecificFunctionCall ( [ "Basics" ], "not" ) checkInfo.lookupTable checkInfo.right)
|
|
of
|
|
Just ( leftNot, rightNot ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary negation on both sides"
|
|
, details = [ "Since both sides are negated using `not`, they are redundant and can be removed." ]
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange leftNot.fnRange, Fix.removeRange rightNot.fnRange ]
|
|
]
|
|
|
|
_ ->
|
|
let
|
|
inferred : Infer.Inferred
|
|
inferred =
|
|
Tuple.first checkInfo.inferredConstants
|
|
|
|
normalizeAndInfer : Node Expression -> Node Expression
|
|
normalizeAndInfer expressionNode =
|
|
let
|
|
normalizedExpressionNode : Node Expression
|
|
normalizedExpressionNode =
|
|
Normalize.normalize checkInfo expressionNode
|
|
in
|
|
case Infer.get (Node.value normalizedExpressionNode) inferred of
|
|
Just expr ->
|
|
Node Range.emptyRange expr
|
|
|
|
Nothing ->
|
|
normalizedExpressionNode
|
|
|
|
normalizedLeft : Node Expression
|
|
normalizedLeft =
|
|
normalizeAndInfer checkInfo.left
|
|
|
|
normalizedRight : Node Expression
|
|
normalizedRight =
|
|
normalizeAndInfer checkInfo.right
|
|
in
|
|
case Normalize.compareWithoutNormalization normalizedLeft normalizedRight of
|
|
Normalize.ConfirmedEquality ->
|
|
if checkInfo.expectNaN then
|
|
[]
|
|
|
|
else
|
|
[ comparisonError isEqual checkInfo ]
|
|
|
|
Normalize.ConfirmedInequality ->
|
|
[ comparisonError (not isEqual) checkInfo ]
|
|
|
|
Normalize.Unconfirmed ->
|
|
[]
|
|
|
|
|
|
alwaysSameDetails : List String
|
|
alwaysSameDetails =
|
|
[ "This condition will always result in the same value. You may have hardcoded a value or mistyped a condition."
|
|
]
|
|
|
|
|
|
unnecessaryMessage : String
|
|
unnecessaryMessage =
|
|
"Part of the expression is unnecessary"
|
|
|
|
|
|
unnecessaryDetails : List String
|
|
unnecessaryDetails =
|
|
[ "A part of this condition is unnecessary. You can remove it and it would not impact the behavior of the program."
|
|
]
|
|
|
|
|
|
|
|
-- COMPARISONS
|
|
|
|
|
|
comparisonChecks : (Float -> Float -> Bool) -> OperatorCheckInfo -> List (Error {})
|
|
comparisonChecks operatorFunction operatorCheckInfo =
|
|
case
|
|
Maybe.map2 operatorFunction
|
|
(Normalize.getNumberValue operatorCheckInfo.left)
|
|
(Normalize.getNumberValue operatorCheckInfo.right)
|
|
of
|
|
Just bool ->
|
|
[ comparisonError bool operatorCheckInfo ]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
comparisonError : Bool -> QualifyResources { a | parentRange : Range } -> Error {}
|
|
comparisonError bool checkInfo =
|
|
let
|
|
boolAsString : String
|
|
boolAsString =
|
|
AstHelpers.boolToString bool
|
|
in
|
|
Rule.errorWithFix
|
|
{ message = "Comparison is always " ++ boolAsString
|
|
, details =
|
|
[ "Based on the values and/or the context, we can determine that the value of this operation will always be " ++ boolAsString ++ "."
|
|
]
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], boolAsString ) checkInfo))
|
|
]
|
|
|
|
|
|
|
|
-- IF EXPRESSIONS
|
|
|
|
|
|
targetIfKeyword : Range -> Range
|
|
targetIfKeyword ifExpressionRange =
|
|
let
|
|
ifStart : Location
|
|
ifStart =
|
|
ifExpressionRange.start
|
|
in
|
|
{ start = ifStart
|
|
, end = { ifStart | column = ifStart.column + 2 }
|
|
}
|
|
|
|
|
|
|
|
-- BASICS
|
|
|
|
|
|
basicsIdentityChecks : CheckInfo -> List (Error {})
|
|
basicsIdentityChecks checkInfo =
|
|
[ Rule.errorWithFix
|
|
{ message = "`identity` should be removed"
|
|
, details = [ "`identity` can be a useful function to be passed as arguments to other functions, but calling it manually with an argument is the same thing as writing the argument on its own." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.firstArg })
|
|
]
|
|
|
|
|
|
basicsIdentityCompositionErrorMessage : { message : String, details : List String }
|
|
basicsIdentityCompositionErrorMessage =
|
|
{ message = "`identity` should be removed"
|
|
, details = [ "Composing a function with `identity` is the same as simplify referencing the function." ]
|
|
}
|
|
|
|
|
|
basicsIdentityCompositionChecks : CompositionCheckInfo -> List (Error {})
|
|
basicsIdentityCompositionChecks checkInfo =
|
|
if AstHelpers.isIdentity checkInfo.lookupTable checkInfo.later then
|
|
[ Rule.errorWithFix
|
|
basicsIdentityCompositionErrorMessage
|
|
(Node.range checkInfo.later)
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.earlier })
|
|
]
|
|
|
|
else if AstHelpers.isIdentity checkInfo.lookupTable checkInfo.earlier then
|
|
[ Rule.errorWithFix
|
|
basicsIdentityCompositionErrorMessage
|
|
(Node.range checkInfo.earlier)
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.later })
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
basicsAlwaysChecks : CheckInfo -> List (Error {})
|
|
basicsAlwaysChecks checkInfo =
|
|
case secondArg checkInfo of
|
|
Just (Node secondArgRange _) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Expression can be replaced by the first argument given to `always`"
|
|
, details = [ "The second argument will be ignored because of the `always` call." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix
|
|
(Range.combine [ checkInfo.fnRange, Node.range checkInfo.firstArg, secondArgRange ])
|
|
checkInfo.firstArg
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
basicsAlwaysCompositionErrorMessage : { message : String, details : List String }
|
|
basicsAlwaysCompositionErrorMessage =
|
|
{ message = "Function composed with always will be ignored"
|
|
, details = [ "`always` will swallow the function composed into it." ]
|
|
}
|
|
|
|
|
|
basicsAlwaysCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
basicsAlwaysCompositionChecks checkInfo =
|
|
case checkInfo.later.args of
|
|
_ :: [] ->
|
|
[ Rule.errorWithFix
|
|
basicsAlwaysCompositionErrorMessage
|
|
checkInfo.later.range
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = checkInfo.later.range })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
reportEmptyListSecondArgument : ( ( ModuleName, String ), CheckInfo -> List (Error {}) ) -> ( ( ModuleName, String ), CheckInfo -> List (Error {}) )
|
|
reportEmptyListSecondArgument ( ( moduleName, name ), function ) =
|
|
( ( moduleName, name )
|
|
, \checkInfo ->
|
|
case secondArg checkInfo of
|
|
Just (Node _ (Expression.ListExpr [])) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( moduleName, name ) ++ " on an empty list will result in an empty list"
|
|
, details = [ "You can replace this call by an empty list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
function checkInfo
|
|
)
|
|
|
|
|
|
reportEmptyListFirstArgument : ( ( ModuleName, String ), CheckInfo -> List (Error {}) ) -> ( ( ModuleName, String ), CheckInfo -> List (Error {}) )
|
|
reportEmptyListFirstArgument ( ( moduleName, name ), function ) =
|
|
( ( moduleName, name )
|
|
, \checkInfo ->
|
|
case checkInfo.firstArg of
|
|
Node _ (Expression.ListExpr []) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( moduleName, name ) ++ " on an empty list will result in an empty list"
|
|
, details = [ "You can replace this call by an empty list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
function checkInfo
|
|
)
|
|
|
|
|
|
|
|
-- STRING
|
|
|
|
|
|
stringFromListChecks : CheckInfo -> List (Error {})
|
|
stringFromListChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling " ++ qualifiedToString ( [ "String" ], "fromList" ) ++ " [] will result in " ++ emptyStringAsString
|
|
, details = [ "You can replace this call by " ++ emptyStringAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange emptyStringAsString ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Just listSingletonArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling " ++ qualifiedToString ( [ "String" ], "fromList" ) ++ " with a list with a single char is the same as String.fromChar with the contained char"
|
|
, details = [ "You can replace this call by " ++ qualifiedToString ( [ "String" ], "fromChar" ) ++ " with the contained char." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange listSingletonArg.element
|
|
++ [ Fix.insertAt checkInfo.parentRange.start
|
|
(qualifiedToString (qualify ( [ "String" ], "fromChar" ) checkInfo) ++ " ")
|
|
]
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
stringIsEmptyChecks : CheckInfo -> List (Error {})
|
|
stringIsEmptyChecks checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.Literal str ->
|
|
let
|
|
replacementValueAsString : String
|
|
replacementValueAsString =
|
|
AstHelpers.boolToString (str == "")
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to String.isEmpty will result in " ++ replacementValueAsString
|
|
, details = [ "You can replace this call by " ++ replacementValueAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], replacementValueAsString ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringConcatChecks : CheckInfo -> List (Error {})
|
|
stringConcatChecks checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.concat on an empty list will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange emptyStringAsString ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringWordsChecks : CheckInfo -> List (Error {})
|
|
stringWordsChecks checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.Literal "" ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.words on an empty string will result in an empty list"
|
|
, details = [ "You can replace this call by an empty list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringLinesChecks : CheckInfo -> List (Error {})
|
|
stringLinesChecks checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.Literal "" ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.lines on an empty string will result in an empty list"
|
|
, details = [ "You can replace this call by an empty list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringReverseChecks : CheckInfo -> List (Error {})
|
|
stringReverseChecks checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.Literal "" ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.reverse on an empty string will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange emptyStringAsString ]
|
|
]
|
|
|
|
_ ->
|
|
removeAlongWithOtherFunctionCheck
|
|
reverseReverseCompositionErrorMessage
|
|
(AstHelpers.getSpecificValueOrFunction ( [ "String" ], "reverse" ))
|
|
checkInfo
|
|
|
|
|
|
stringSliceChecks : CheckInfo -> List (Error {})
|
|
stringSliceChecks checkInfo =
|
|
case ( secondArg checkInfo, thirdArg checkInfo ) of
|
|
( _, Just (Node _ (Expression.Literal "")) ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.slice on an empty string will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (thirdArg checkInfo) checkInfo)
|
|
]
|
|
|
|
( Just (Node _ (Expression.Integer 0)), _ ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.slice with end index 0 will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (thirdArg checkInfo) checkInfo)
|
|
]
|
|
|
|
( Just end, _ ) ->
|
|
Maybe.map2
|
|
(\startInt endInt ->
|
|
if
|
|
(startInt >= endInt)
|
|
&& -- have the same sign
|
|
((startInt <= -1 && endInt <= -1)
|
|
|| (startInt >= 0 && endInt >= 0)
|
|
)
|
|
then
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to String.slice will result in " ++ emptyStringAsString
|
|
, details = [ "You can replace this slice operation by " ++ emptyStringAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (thirdArg checkInfo) checkInfo)
|
|
]
|
|
|> Just
|
|
|
|
else
|
|
-- either is negative or startInt < endInt
|
|
Nothing
|
|
)
|
|
(Evaluate.getInt checkInfo checkInfo.firstArg)
|
|
(Evaluate.getInt checkInfo end)
|
|
|> Maybe.withDefault
|
|
(if Normalize.areAllTheSame checkInfo checkInfo.firstArg [ end ] then
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.slice with equal start and end index will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (thirdArg checkInfo) checkInfo)
|
|
]
|
|
|> Just
|
|
|
|
else
|
|
Nothing
|
|
)
|
|
|> Maybe.withDefault []
|
|
|
|
( Nothing, _ ) ->
|
|
[]
|
|
|
|
|
|
stringLeftChecks : CheckInfo -> List (Error {})
|
|
stringLeftChecks checkInfo =
|
|
case ( checkInfo.firstArg, secondArg checkInfo ) of
|
|
( _, Just (Node _ (Expression.Literal "")) ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.left on an empty string will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange emptyStringAsString ]
|
|
]
|
|
|
|
( Node _ (Expression.Integer 0), _ ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.left with length 0 will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
( Node _ (Expression.Negation (Node _ (Expression.Integer _))), _ ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.left with negative length will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringRightChecks : CheckInfo -> List (Error {})
|
|
stringRightChecks checkInfo =
|
|
case ( checkInfo.firstArg, secondArg checkInfo ) of
|
|
( _, Just (Node _ (Expression.Literal "")) ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.right on an empty string will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange emptyStringAsString ]
|
|
]
|
|
|
|
( Node _ (Expression.Integer 0), _ ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.right with length 0 will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
( Node _ (Expression.Negation (Node _ (Expression.Integer _))), _ ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.right with negative length will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
reverseReverseCompositionErrorMessage : { message : String, details : List String }
|
|
reverseReverseCompositionErrorMessage =
|
|
{ message = "Unnecessary double reversal"
|
|
, details = [ "Composing `reverse` with `reverse` cancel each other out." ]
|
|
}
|
|
|
|
|
|
stringJoinChecks : CheckInfo -> List (Error {})
|
|
stringJoinChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case secondArg checkInfo of
|
|
Just (Node _ (Expression.ListExpr [])) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.join on an empty list will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange emptyStringAsString ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.Literal "" ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use String.concat instead"
|
|
, details = [ "Using String.join with an empty separator is the same as using String.concat." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy { start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).end }
|
|
(qualifiedToString (qualify ( [ "String" ], "concat" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
stringLengthChecks : CheckInfo -> List (Error {})
|
|
stringLengthChecks checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.Literal str ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The length of the string is " ++ String.fromInt (String.length str)
|
|
, details = [ "The length of the string can be determined by looking at the code." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange (String.fromInt (String.length str)) ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringRepeatChecks : CheckInfo -> List (Error {})
|
|
stringRepeatChecks checkInfo =
|
|
case secondArg checkInfo of
|
|
Just (Node _ (Expression.Literal "")) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.repeat with an empty string will result in an empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange emptyStringAsString ]
|
|
]
|
|
|
|
_ ->
|
|
case Evaluate.getInt checkInfo checkInfo.firstArg of
|
|
Just intValue ->
|
|
if intValue == 1 then
|
|
[ Rule.errorWithFix
|
|
{ message = "String.repeat 1 won't do anything"
|
|
, details = [ "Using String.repeat with 1 will result in the second argument." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange { start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).end } ]
|
|
]
|
|
|
|
else if intValue < 1 then
|
|
[ Rule.errorWithFix
|
|
{ message = "String.repeat will result in an empty string"
|
|
, details = [ "Using String.repeat with a number less than 1 will result in an empty string. You can replace this call by an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix emptyStringAsString checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringReplaceChecks : CheckInfo -> List (Error {})
|
|
stringReplaceChecks checkInfo =
|
|
case secondArg checkInfo of
|
|
Just replacementArg ->
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Normalize.compare checkInfo checkInfo.firstArg replacementArg of
|
|
Normalize.ConfirmedEquality ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The result of String.replace will be the original string"
|
|
, details = [ "The pattern to replace and the replacement are equal, therefore the result of the String.replace call will be the original string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(toIdentityFix { lastArg = thirdArg checkInfo, resources = checkInfo })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case thirdArg checkInfo of
|
|
Just (Node thirdRange (Expression.Literal "")) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The result of String.replace will be the empty string"
|
|
, details = [ "Replacing anything on an empty string results in an empty string." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.fnRange.start
|
|
, end = thirdRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case ( Node.value checkInfo.firstArg, Node.value replacementArg, thirdArg checkInfo ) of
|
|
( Expression.Literal first, Expression.Literal second, Just (Node thirdRange (Expression.Literal third)) ) ->
|
|
if not (String.contains "\u{000D}" first) && String.replace first second third == third then
|
|
[ Rule.errorWithFix
|
|
{ message = "The result of String.replace will be the original string"
|
|
, details = [ "The replacement doesn't haven't any noticeable impact. You can remove the call to String.replace." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = thirdRange })
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
|
|
-- MAYBE FUNCTIONS
|
|
|
|
|
|
maybeMapChecks : CheckInfo -> List (Error {})
|
|
maybeMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> collectionMapChecks maybeCollection checkInfo
|
|
, \() -> mapPureChecks { moduleName = [ "Maybe" ], pure = "Just", map = "map" } checkInfo
|
|
]
|
|
()
|
|
|
|
|
|
maybeMapCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
maybeMapCompositionChecks checkInfo =
|
|
pureToMapCompositionChecks { moduleName = [ "Maybe" ], pure = "Just", map = "map" } checkInfo
|
|
|
|
|
|
|
|
-- RESULT FUNCTIONS
|
|
|
|
|
|
resultMapChecks : CheckInfo -> List (Error {})
|
|
resultMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> collectionMapChecks resultCollection checkInfo
|
|
, \() -> mapPureChecks { moduleName = [ "Result" ], pure = "Ok", map = "map" } checkInfo
|
|
]
|
|
()
|
|
|
|
|
|
resultMapCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
resultMapCompositionChecks checkInfo =
|
|
pureToMapCompositionChecks { moduleName = [ "Result" ], pure = "Ok", map = "map" } checkInfo
|
|
|
|
|
|
resultMapErrorOnErrErrorInfo : { message : String, details : List String }
|
|
resultMapErrorOnErrErrorInfo =
|
|
{ message = "Using " ++ qualifiedToString ( [ "Result" ], "mapError" ) ++ " on Err will result in Err with the function applied to the error"
|
|
, details = [ "You can replace this call by Err with the function directly applied to the error itself." ]
|
|
}
|
|
|
|
|
|
resultMapErrorOnOkErrorInfo : { message : String, details : List String }
|
|
resultMapErrorOnOkErrorInfo =
|
|
{ message = "Calling " ++ qualifiedToString ( [ "Result" ], "mapError" ) ++ " on a value that is Ok will always return the Ok result value"
|
|
, details = [ "You can remove the " ++ qualifiedToString ( [ "Result" ], "mapError" ) ++ " call." ]
|
|
}
|
|
|
|
|
|
resultMapErrorChecks : CheckInfo -> List (Error {})
|
|
resultMapErrorChecks checkInfo =
|
|
let
|
|
maybeResultArg : Maybe (Node Expression)
|
|
maybeResultArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
-- TODO use collectionMapChecks
|
|
[ \() ->
|
|
if AstHelpers.isIdentity checkInfo.lookupTable checkInfo.firstArg then
|
|
[ identityError
|
|
{ toFix = qualifiedToString ( [ "Result" ], "mapError" ) ++ " identity"
|
|
, lastArgName = "result"
|
|
, lastArg = maybeResultArg
|
|
, resources = checkInfo
|
|
}
|
|
]
|
|
|
|
else
|
|
[]
|
|
, \() ->
|
|
mapPureChecks { moduleName = [ "Result" ], pure = "Err", map = "mapError" } checkInfo
|
|
, \() ->
|
|
case maybeResultArg of
|
|
Just resultArg ->
|
|
case sameCallInAllBranches ( [ "Result" ], "Ok" ) checkInfo.lookupTable resultArg of
|
|
Determined _ ->
|
|
[ Rule.errorWithFix
|
|
resultMapErrorOnOkErrorInfo
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range resultArg })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
resultMapErrorCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
resultMapErrorCompositionChecks checkInfo =
|
|
case checkInfo.later.args of
|
|
(Node errorMappingArgRange _) :: _ ->
|
|
case ( checkInfo.earlier.fn, checkInfo.earlier.args ) of
|
|
( ( [ "Result" ], "Err" ), [] ) ->
|
|
[ Rule.errorWithFix
|
|
resultMapErrorOnErrErrorInfo
|
|
checkInfo.later.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = errorMappingArgRange }
|
|
++ [ case checkInfo.direction of
|
|
LeftToRight ->
|
|
Fix.insertAt checkInfo.parentRange.end
|
|
(" >> " ++ qualifiedToString (qualify ( [ "Result" ], "Err" ) checkInfo))
|
|
|
|
RightToLeft ->
|
|
Fix.insertAt checkInfo.parentRange.start
|
|
(qualifiedToString (qualify ( [ "Result" ], "Err" ) checkInfo) ++ " << ")
|
|
]
|
|
)
|
|
]
|
|
|
|
( ( [ "Result" ], "Ok" ), [] ) ->
|
|
[ Rule.errorWithFix
|
|
resultMapErrorOnOkErrorInfo
|
|
checkInfo.later.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = checkInfo.earlier.fnRange })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
[] ->
|
|
[]
|
|
|
|
|
|
|
|
-- LIST FUNCTIONS
|
|
|
|
|
|
listConcatChecks : CheckInfo -> List (Error {})
|
|
listConcatChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr list ->
|
|
case list of
|
|
(Node elementRange _) :: [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary use of " ++ qualifiedToString ( [ "List" ], "concat" ) ++ " on a list with 1 element"
|
|
, details = [ "The value of the operation will be the element itself. You should replace this expression by that." ]
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.removeRange { start = checkInfo.parentRange.start, end = elementRange.start }
|
|
, Fix.removeRange { start = elementRange.end, end = checkInfo.parentRange.end }
|
|
]
|
|
]
|
|
|
|
firstListElement :: restOfListElements ->
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case findEmptyLiteral list of
|
|
Just emptyLiteral ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Found empty list in the list given " ++ qualifiedToString ( [ "List" ], "concat" )
|
|
, details = [ "This element is unnecessary and can be removed." ]
|
|
}
|
|
emptyLiteral.element
|
|
[ Fix.removeRange emptyLiteral.removalRange ]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case traverse AstHelpers.getListLiteral list of
|
|
Just _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Expression could be simplified to be a single List"
|
|
, details = [ "Try moving all the elements into a single list." ]
|
|
}
|
|
checkInfo.parentRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range checkInfo.firstArg }
|
|
++ List.concatMap removeBoundariesFix (firstListElement :: restOfListElements)
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case findConsecutiveListLiterals firstListElement restOfListElements of
|
|
firstFix :: fixesAFterFirst ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Consecutive literal lists should be merged"
|
|
, details = [ "Try moving all the elements from consecutive list literals so that they form a single list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(firstFix :: fixesAFterFirst)
|
|
]
|
|
|
|
[] ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
_ ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "List" ], "map" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Just listMapArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "List" ], "map" ) ++ " and " ++ qualifiedToString ( [ "List" ], "concat" ) ++ " can be combined using " ++ qualifiedToString ( [ "List" ], "concatMap" )
|
|
, details = [ qualifiedToString ( [ "List" ], "concatMap" ) ++ " is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = listMapArg.nodeRange }
|
|
++ [ Fix.replaceRangeBy listMapArg.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "concatMap" ) checkInfo))
|
|
]
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
findEmptyLiteral : List (Node Expression) -> Maybe { element : Range, removalRange : Range }
|
|
findEmptyLiteral elements =
|
|
case elements of
|
|
[] ->
|
|
Nothing
|
|
|
|
head :: rest ->
|
|
let
|
|
headRange : Range
|
|
headRange =
|
|
Node.range head
|
|
in
|
|
case AstHelpers.getListLiteral head of
|
|
Just [] ->
|
|
let
|
|
end : Location
|
|
end =
|
|
case rest of
|
|
[] ->
|
|
headRange.end
|
|
|
|
(Node nextItem _) :: _ ->
|
|
nextItem.start
|
|
in
|
|
Just
|
|
{ element = headRange
|
|
, removalRange = { start = headRange.start, end = end }
|
|
}
|
|
|
|
_ ->
|
|
findEmptyLiteralHelp rest headRange.end
|
|
|
|
|
|
findEmptyLiteralHelp : List (Node Expression) -> Location -> Maybe { element : Range, removalRange : Range }
|
|
findEmptyLiteralHelp elements previousItemEnd =
|
|
case elements of
|
|
[] ->
|
|
Nothing
|
|
|
|
head :: rest ->
|
|
let
|
|
headRange : Range
|
|
headRange =
|
|
Node.range head
|
|
in
|
|
case AstHelpers.getListLiteral head of
|
|
Just [] ->
|
|
Just
|
|
{ element = headRange
|
|
, removalRange = { start = previousItemEnd, end = headRange.end }
|
|
}
|
|
|
|
_ ->
|
|
findEmptyLiteralHelp rest headRange.end
|
|
|
|
|
|
findConsecutiveListLiterals : Node Expression -> List (Node Expression) -> List Fix
|
|
findConsecutiveListLiterals firstListElement restOfListElements =
|
|
case ( firstListElement, restOfListElements ) of
|
|
( Node firstRange (Expression.ListExpr _), ((Node secondRange (Expression.ListExpr _)) as second) :: rest ) ->
|
|
Fix.replaceRangeBy
|
|
{ start = { row = firstRange.end.row, column = firstRange.end.column - 1 }
|
|
, end = { row = secondRange.start.row, column = secondRange.start.column + 1 }
|
|
}
|
|
", "
|
|
:: findConsecutiveListLiterals second rest
|
|
|
|
( _, x :: xs ) ->
|
|
findConsecutiveListLiterals x xs
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listConcatMapChecks : CheckInfo -> List (Error {})
|
|
listConcatMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
if AstHelpers.isIdentity checkInfo.lookupTable checkInfo.firstArg then
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "concatMap" ) ++ " with an identity function is the same as using " ++ qualifiedToString ( [ "List" ], "concat" ) ++ ""
|
|
, details = [ "You can replace this call by " ++ qualifiedToString ( [ "List" ], "concat" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).end }
|
|
(qualifiedToString (qualify ( [ "List" ], "concat" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getAlwaysResult checkInfo.lookupTable checkInfo.firstArg of
|
|
Just alwaysResult ->
|
|
case AstHelpers.getListLiteral alwaysResult of
|
|
Just [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "List" ], "concatMap" ) ++ " will result in on an empty list"
|
|
, details = [ "You can replace this call by an empty list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix "[]" checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case Node.value (AstHelpers.removeParens checkInfo.firstArg) of
|
|
Expression.LambdaExpression lambda ->
|
|
case replaceSingleElementListBySingleValue checkInfo.lookupTable lambda.expression of
|
|
Just fixes ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ qualifiedToString ( [ "List" ], "map" ) ++ " instead"
|
|
, details = [ "The function passed to " ++ qualifiedToString ( [ "List" ], "concatMap" ) ++ " always returns a list with a single element." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "map" ) checkInfo))
|
|
:: fixes
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case secondArg checkInfo of
|
|
Just (Node listRange (Expression.ListExpr (listElement :: []))) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "concatMap" ) ++ " on an element with a single item is the same as calling the function directly on that lone element."
|
|
, details = [ "You can replace this call by a call to the function directly." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.removeRange checkInfo.fnRange
|
|
:: replaceBySubExpressionFix listRange listElement
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listConcatCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
listConcatCompositionChecks checkInfo =
|
|
case ( checkInfo.earlier.fn, checkInfo.earlier.args ) of
|
|
( ( [ "List" ], "map" ), _ :: [] ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "List" ], "map" ) ++ " and " ++ qualifiedToString ( [ "List" ], "concat" ) ++ " can be combined using " ++ qualifiedToString ( [ "List" ], "concatMap" ) ++ ""
|
|
, details = [ qualifiedToString ( [ "List" ], "concatMap" ) ++ " is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
-- TODO switch to later.fnRange
|
|
checkInfo.later.range
|
|
(Fix.replaceRangeBy checkInfo.earlier.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "concatMap" ) checkInfo))
|
|
:: keepOnlyFix { parentRange = checkInfo.parentRange, keep = checkInfo.earlier.range }
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listIndexedMapChecks : CheckInfo -> List (Error {})
|
|
listIndexedMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case AstHelpers.removeParens checkInfo.firstArg of
|
|
Node lambdaRange (Expression.LambdaExpression lambda) ->
|
|
case Maybe.map AstHelpers.removeParensFromPattern (List.head lambda.args) of
|
|
Just (Node _ Pattern.AllPattern) ->
|
|
let
|
|
rangeToRemove : Range
|
|
rangeToRemove =
|
|
case lambda.args of
|
|
[] ->
|
|
Range.emptyRange
|
|
|
|
_ :: [] ->
|
|
-- Only one argument, remove the entire lambda except the expression
|
|
{ start = lambdaRange.start, end = (Node.range lambda.expression).start }
|
|
|
|
(Node firstRange _) :: (Node secondRange _) :: _ ->
|
|
{ start = firstRange.start, end = secondRange.start }
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ qualifiedToString ( [ "List" ], "map" ) ++ " instead"
|
|
, details = [ "Using " ++ qualifiedToString ( [ "List" ], "indexedMap" ) ++ " while ignoring the first argument is the same thing as calling " ++ qualifiedToString ( [ "List" ], "map" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "map" ) checkInfo))
|
|
, Fix.removeRange rangeToRemove
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Basics" ], "always" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Just alwaysCall ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ qualifiedToString ( [ "List" ], "map" ) ++ " instead"
|
|
, details = [ "Using " ++ qualifiedToString ( [ "List" ], "indexedMap" ) ++ " while ignoring the first argument is the same thing as calling " ++ qualifiedToString ( [ "List" ], "map" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "map" ) checkInfo))
|
|
:: replaceBySubExpressionFix alwaysCall.nodeRange alwaysCall.firstArg
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listAppendEmptyErrorInfo : { message : String, details : List String }
|
|
listAppendEmptyErrorInfo =
|
|
{ message = "Appending [] doesn't have any effect"
|
|
, details = [ "You can remove the " ++ qualifiedToString ( [ "List" ], "append" ) ++ " function and the []." ]
|
|
}
|
|
|
|
|
|
listAppendChecks : CheckInfo -> List (Error {})
|
|
listAppendChecks checkInfo =
|
|
case ( checkInfo.firstArg, secondArg checkInfo ) of
|
|
( Node _ (Expression.ListExpr []), maybeSecondListArg ) ->
|
|
case maybeSecondListArg of
|
|
Nothing ->
|
|
[ Rule.errorWithFix
|
|
{ listAppendEmptyErrorInfo
|
|
| details = [ "You can replace this call by identity." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "identity" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Just secondListArg ->
|
|
[ Rule.errorWithFix
|
|
listAppendEmptyErrorInfo
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange secondListArg)
|
|
]
|
|
|
|
( firstList, Just (Node _ (Expression.ListExpr [])) ) ->
|
|
[ Rule.errorWithFix
|
|
listAppendEmptyErrorInfo
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange firstList)
|
|
]
|
|
|
|
( Node firstListRange (Expression.ListExpr (_ :: _)), Just (Node secondListRange (Expression.ListExpr (_ :: _))) ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Appending literal lists could be simplified to be a single List"
|
|
, details = [ "Try moving all the elements into a single list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange { start = secondListRange.end, end = checkInfo.parentRange.end }
|
|
, Fix.replaceRangeBy
|
|
{ start = checkInfo.parentRange.start, end = startWithoutBoundary secondListRange }
|
|
("[" ++ checkInfo.extractSourceCode (rangeWithoutBoundaries firstListRange) ++ ",")
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listHeadExistsError : { message : String, details : List String }
|
|
listHeadExistsError =
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "head" ) ++ " on a list with a first element will result in Just that element"
|
|
, details = [ "You can replace this call by Just the first list element." ]
|
|
}
|
|
|
|
|
|
listHeadChecks : CheckInfo -> List (Error {})
|
|
listHeadChecks checkInfo =
|
|
let
|
|
justFirstElementError : Node Expression -> List (Error {})
|
|
justFirstElementError keep =
|
|
[ Rule.errorWithFix
|
|
listHeadExistsError
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix (Node.range listArg) keep
|
|
++ [ Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Just" ) checkInfo))
|
|
]
|
|
)
|
|
]
|
|
|
|
listArg : Node Expression
|
|
listArg =
|
|
AstHelpers.removeParens checkInfo.firstArg
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value listArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "head" ) ++ " on an empty list will result in Nothing"
|
|
, details = [ "You can replace this call by Nothing." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Nothing" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Expression.ListExpr (head :: _) ->
|
|
justFirstElementError head
|
|
|
|
Expression.OperatorApplication "::" _ head _ ->
|
|
justFirstElementError head
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable listArg of
|
|
Just single ->
|
|
justFirstElementError single.element
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listTailExistsError : { message : String, details : List String }
|
|
listTailExistsError =
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "tail" ) ++ " on a list with some elements will result in Just the elements after the first"
|
|
, details = [ "You can replace this call by Just the list elements after the first." ]
|
|
}
|
|
|
|
|
|
listEmptyTailExistsError : { message : String, details : List String }
|
|
listEmptyTailExistsError =
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "tail" ) ++ " on a list with a single element will result in Just the empty list"
|
|
, details = [ "You can replace this call by Just the empty list." ]
|
|
}
|
|
|
|
|
|
listTailChecks : CheckInfo -> List (Error {})
|
|
listTailChecks checkInfo =
|
|
let
|
|
listArg : Node Expression
|
|
listArg =
|
|
AstHelpers.removeParens checkInfo.firstArg
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value listArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "tail" ) ++ " on an empty list will result in Nothing"
|
|
, details = [ "You can replace this call by Nothing." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Nothing" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Expression.ListExpr ((Node headRange _) :: (Node tailFirstRange _) :: _) ->
|
|
[ Rule.errorWithFix
|
|
listTailExistsError
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange { start = headRange.start, end = tailFirstRange.start }
|
|
, Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Just" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Expression.OperatorApplication "::" _ _ tail ->
|
|
[ Rule.errorWithFix
|
|
listTailExistsError
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix (Node.range listArg) tail
|
|
++ [ Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Just" ) checkInfo))
|
|
]
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable listArg of
|
|
Just _ ->
|
|
[ Rule.errorWithFix
|
|
listEmptyTailExistsError
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy (Node.range checkInfo.firstArg) "[]"
|
|
, Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Just" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listMapChecks : CheckInfo -> List (Error {})
|
|
listMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> collectionMapChecks listCollection checkInfo
|
|
, \() -> dictToListMapChecks checkInfo
|
|
]
|
|
()
|
|
|
|
|
|
dictToListMapErrorInfo : { toEntryAspectList : String, tuplePart : String } -> { message : String, details : List String }
|
|
dictToListMapErrorInfo info =
|
|
let
|
|
toEntryAspectListAsQualifiedString : String
|
|
toEntryAspectListAsQualifiedString =
|
|
qualifiedToString ( [ "Dict" ], info.toEntryAspectList )
|
|
in
|
|
{ message = "Using " ++ qualifiedToString ( [ "Dict" ], "toList" ) ++ ", then " ++ qualifiedToString ( [ "List" ], "map" ) ++ " " ++ qualifiedToString ( [ "Tuple" ], info.tuplePart ) ++ " is the same as using " ++ toEntryAspectListAsQualifiedString
|
|
, details = [ "Using " ++ toEntryAspectListAsQualifiedString ++ " directly is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
|
|
|
|
dictToListMapChecks : CheckInfo -> List (Error {})
|
|
dictToListMapChecks listMapCheckInfo =
|
|
case secondArg listMapCheckInfo of
|
|
Just listArgument ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Dict" ], "toList" ) listMapCheckInfo.lookupTable listArgument of
|
|
Just dictToListCall ->
|
|
let
|
|
error : { toEntryAspectList : String, tuplePart : String } -> Error {}
|
|
error info =
|
|
Rule.errorWithFix
|
|
(dictToListMapErrorInfo info)
|
|
listMapCheckInfo.fnRange
|
|
(keepOnlyFix { parentRange = Node.range listArgument, keep = Node.range dictToListCall.firstArg }
|
|
++ [ Fix.replaceRangeBy
|
|
(Range.combine [ listMapCheckInfo.fnRange, Node.range listMapCheckInfo.firstArg ])
|
|
(qualifiedToString (qualify ( [ "Dict" ], info.toEntryAspectList ) listMapCheckInfo))
|
|
]
|
|
)
|
|
in
|
|
if AstHelpers.isTupleFirstAccess listMapCheckInfo.lookupTable listMapCheckInfo.firstArg then
|
|
[ error { tuplePart = "first", toEntryAspectList = "keys" } ]
|
|
|
|
else if AstHelpers.isTupleSecondAccess listMapCheckInfo.lookupTable listMapCheckInfo.firstArg then
|
|
[ error { tuplePart = "second", toEntryAspectList = "values" } ]
|
|
|
|
else
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
listMapCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
listMapCompositionChecks checkInfo =
|
|
case
|
|
( ( checkInfo.earlier.fn, checkInfo.earlier.args )
|
|
, checkInfo.later.args
|
|
)
|
|
of
|
|
( ( ( [ "Dict" ], "toList" ), [] ), elementMappingArg :: [] ) ->
|
|
let
|
|
error : { toEntryAspectList : String, tuplePart : String } -> Error {}
|
|
error info =
|
|
Rule.errorWithFix
|
|
(dictToListMapErrorInfo info)
|
|
checkInfo.later.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange (qualifiedToString (qualify ( [ "Dict" ], info.toEntryAspectList ) checkInfo)) ]
|
|
in
|
|
if AstHelpers.isTupleFirstAccess checkInfo.lookupTable elementMappingArg then
|
|
[ error { tuplePart = "first", toEntryAspectList = "keys" } ]
|
|
|
|
else if AstHelpers.isTupleSecondAccess checkInfo.lookupTable elementMappingArg then
|
|
[ error { tuplePart = "second", toEntryAspectList = "values" } ]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listMemberChecks : CheckInfo -> List (Error {})
|
|
listMemberChecks checkInfo =
|
|
let
|
|
needleArg : Node Expression
|
|
needleArg =
|
|
checkInfo.firstArg
|
|
|
|
needleArgNormalized : Node Expression
|
|
needleArgNormalized =
|
|
Normalize.normalize checkInfo needleArg
|
|
|
|
isNeedle : Node Expression -> Bool
|
|
isNeedle element =
|
|
Normalize.compareWithoutNormalization
|
|
(Normalize.normalize checkInfo element)
|
|
needleArgNormalized
|
|
== Normalize.ConfirmedEquality
|
|
in
|
|
case secondArg checkInfo of
|
|
Just listArg ->
|
|
let
|
|
needleRange : Range
|
|
needleRange =
|
|
Node.range needleArg
|
|
|
|
listMemberExistsError : List (Error {})
|
|
listMemberExistsError =
|
|
if checkInfo.expectNaN then
|
|
[]
|
|
|
|
else
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "member" ) ++ " on a list which contains the given element will result in True"
|
|
, details = [ "You can replace this call by True." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "True" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
singleNonNormalizedEqualElementError : Node Expression -> List (Error {})
|
|
singleNonNormalizedEqualElementError element =
|
|
let
|
|
elementRange : Range
|
|
elementRange =
|
|
Node.range element
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "member" ) ++ " on an list with a single element is equivalent to directly checking for equality"
|
|
, details = [ "You can replace this call by checking whether the member to find and the list element are equal." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(List.concat
|
|
[ keepOnlyFix
|
|
{ parentRange = checkInfo.parentRange
|
|
, keep = Range.combine [ needleRange, elementRange ]
|
|
}
|
|
, [ Fix.replaceRangeBy
|
|
(rangeBetweenExclusive ( needleRange, elementRange ))
|
|
" == "
|
|
]
|
|
, parenthesizeIfNeededFix element
|
|
]
|
|
)
|
|
]
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable listArg of
|
|
Just single ->
|
|
if isNeedle single.element then
|
|
listMemberExistsError
|
|
|
|
else
|
|
singleNonNormalizedEqualElementError single.element
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case Node.value (AstHelpers.removeParens listArg) of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "member" ) ++ " on an empty list will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "False" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Expression.ListExpr (el0 :: el1 :: el2Up) ->
|
|
if List.any isNeedle (el0 :: el1 :: el2Up) then
|
|
listMemberExistsError
|
|
|
|
else
|
|
[]
|
|
|
|
Expression.OperatorApplication "::" _ head tail ->
|
|
if List.any isNeedle (head :: getBeforeLastCons tail) then
|
|
listMemberExistsError
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
getBeforeLastCons : Node Expression -> List (Node Expression)
|
|
getBeforeLastCons (Node _ expression) =
|
|
case expression of
|
|
Expression.OperatorApplication "::" _ beforeCons afterCons ->
|
|
beforeCons :: getBeforeLastCons afterCons
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listSumChecks : CheckInfo -> List (Error {})
|
|
listSumChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "sum" ) ++ " on [] will result in 0"
|
|
, details = [ "You can replace this call by 0." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "0" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Just listSingletonArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Summing a list with a single element will result in the element itself"
|
|
, details = [ "You can replace this call by the single element itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange listSingletonArg.element)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listProductChecks : CheckInfo -> List (Error {})
|
|
listProductChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "product" ) ++ " on [] will result in 1"
|
|
, details = [ "You can replace this call by 1." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "1" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Just listSingletonArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "List" ], "product" ) ++ " on a list with a single element will result in the element itself"
|
|
, details = [ "You can replace this call by the single element itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange listSingletonArg.element)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listMinimumChecks : CheckInfo -> List (Error {})
|
|
listMinimumChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "minimum" ) ++ " on [] will result in Nothing"
|
|
, details = [ "You can replace this call by Nothing." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Nothing" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Just listSingletonArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "List" ], "minimum" ) ++ " on a list with a single element will result in Just the element itself"
|
|
, details = [ "You can replace this call by Just the single element itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Just" ) checkInfo))
|
|
:: replaceBySubExpressionFix (Node.range checkInfo.firstArg) listSingletonArg.element
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listMaximumChecks : CheckInfo -> List (Error {})
|
|
listMaximumChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "maximum" ) ++ " on [] will result in Nothing"
|
|
, details = [ "You can replace this call by Nothing." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Nothing" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Just listSingletonArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "List" ], "maximum" ) ++ " on a list with a single element will result in Just the element itself"
|
|
, details = [ "You can replace this call by Just the single element itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Just" ) checkInfo))
|
|
:: replaceBySubExpressionFix (Node.range checkInfo.firstArg) listSingletonArg.element
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listFoldlChecks : CheckInfo -> List (Error {})
|
|
listFoldlChecks checkInfo =
|
|
listFoldAnyDirectionChecks "foldl" checkInfo
|
|
|
|
|
|
listFoldrChecks : CheckInfo -> List (Error {})
|
|
listFoldrChecks checkInfo =
|
|
listFoldAnyDirectionChecks "foldr" checkInfo
|
|
|
|
|
|
listFoldAnyDirectionChecks : String -> CheckInfo -> List (Error {})
|
|
listFoldAnyDirectionChecks foldOperationName checkInfo =
|
|
case secondArg checkInfo of
|
|
Nothing ->
|
|
[]
|
|
|
|
Just initialArg ->
|
|
let
|
|
maybeListArg : Maybe (Node Expression)
|
|
maybeListArg =
|
|
thirdArg checkInfo
|
|
|
|
numberBinaryOperationChecks : { identity : Int, two : String, list : String } -> List (Error {})
|
|
numberBinaryOperationChecks operation =
|
|
let
|
|
fixWith : List Fix -> List (Error {})
|
|
fixWith fixes =
|
|
let
|
|
replacementOperationAsString : String
|
|
replacementOperationAsString =
|
|
qualifiedToString ( [ "List" ], operation.list )
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ replacementOperationAsString ++ " instead"
|
|
, details =
|
|
[ "Using " ++ qualifiedToString ( [ "List" ], foldOperationName ) ++ " (" ++ operation.two ++ ") " ++ String.fromInt operation.identity ++ " is the same as using " ++ replacementOperationAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
fixes
|
|
]
|
|
in
|
|
if AstHelpers.getUncomputedNumberValue initialArg == Just (Basics.toFloat operation.identity) then
|
|
fixWith
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.fnRange.start
|
|
, end = (Node.range initialArg).end
|
|
}
|
|
(qualifiedToString (qualify ( [ "List" ], operation.list ) checkInfo))
|
|
]
|
|
|
|
else
|
|
case maybeListArg of
|
|
Nothing ->
|
|
[]
|
|
|
|
Just _ ->
|
|
if checkInfo.usingRightPizza then
|
|
-- list |> fold op initial --> ((list |> List.op) op initial)
|
|
fixWith
|
|
[ Fix.insertAt (Node.range initialArg).end ")"
|
|
, Fix.insertAt (Node.range initialArg).start (operation.two ++ " ")
|
|
, Fix.replaceRangeBy
|
|
{ start = checkInfo.fnRange.start
|
|
, end = (Node.range checkInfo.firstArg).end
|
|
}
|
|
(qualifiedToString (qualify ( [ "List" ], operation.list ) checkInfo) ++ ")")
|
|
, Fix.insertAt checkInfo.parentRange.start "(("
|
|
]
|
|
|
|
else
|
|
-- <| or application
|
|
-- fold op initial list --> (initial op (List.op list))
|
|
fixWith
|
|
[ Fix.insertAt checkInfo.parentRange.end ")"
|
|
, Fix.insertAt (Node.range initialArg).end
|
|
(" "
|
|
++ operation.two
|
|
++ " ("
|
|
++ qualifiedToString (qualify ( [ "List" ], operation.list ) checkInfo)
|
|
)
|
|
, Fix.removeRange
|
|
{ start = checkInfo.fnRange.start
|
|
, end = (Node.range initialArg).start
|
|
}
|
|
]
|
|
|
|
boolBinaryOperationChecks : { two : String, list : String, determining : Bool } -> Bool -> List (Error {})
|
|
boolBinaryOperationChecks operation initialIsDetermining =
|
|
if initialIsDetermining == operation.determining then
|
|
let
|
|
determiningAsString : String
|
|
determiningAsString =
|
|
AstHelpers.boolToString operation.determining
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( [ "List" ], foldOperationName ) ++ " will result in " ++ determiningAsString
|
|
, details = [ "You can replace this call by " ++ determiningAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix (qualifiedToString (qualify ( [ "Basics" ], determiningAsString ) checkInfo))
|
|
checkInfo.parentRange
|
|
(thirdArg checkInfo)
|
|
checkInfo
|
|
)
|
|
]
|
|
|
|
else
|
|
-- initialIsTrue /= operation.determining
|
|
let
|
|
replacementOperationAsString : String
|
|
replacementOperationAsString =
|
|
qualifiedToString ( [ "List" ], operation.list ) ++ " identity"
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ replacementOperationAsString ++ " instead"
|
|
, details = [ "Using " ++ qualifiedToString ( [ "List" ], foldOperationName ) ++ " (" ++ operation.two ++ ") " ++ AstHelpers.boolToString (not operation.determining) ++ " is the same as using " ++ replacementOperationAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.fnRange.start, end = (Node.range initialArg).end }
|
|
(qualifiedToString (qualify ( [ "List" ], operation.list ) checkInfo)
|
|
++ " "
|
|
++ qualifiedToString (qualify ( [ "Basics" ], "identity" ) checkInfo)
|
|
)
|
|
]
|
|
]
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case maybeListArg of
|
|
Just listArg ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Set" ], "toList" ) checkInfo.lookupTable listArg of
|
|
Just setToListCall ->
|
|
[ Rule.errorWithFix
|
|
{ message = "To fold a set, you don't need to convert to a List"
|
|
, details = [ "Using " ++ qualifiedToString ( [ "Set" ], foldOperationName ) ++ " directly is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix setToListCall.nodeRange setToListCall.firstArg
|
|
++ [ Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Set" ], foldOperationName ) checkInfo))
|
|
]
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case maybeListArg of
|
|
Just listArg ->
|
|
case AstHelpers.getListLiteral listArg of
|
|
Just [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( [ "List" ], foldOperationName ) ++ " will result in the initial accumulator"
|
|
, details = [ "You can replace this call by the initial accumulator." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range initialArg })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getAlwaysResult checkInfo.lookupTable checkInfo.firstArg of
|
|
Just reduceAlwaysResult ->
|
|
if AstHelpers.isIdentity checkInfo.lookupTable reduceAlwaysResult then
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( [ "List" ], foldOperationName ) ++ " will result in the initial accumulator"
|
|
, details = [ "You can replace this call by the initial accumulator." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(case maybeListArg of
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.fnRange.start
|
|
, end = (Node.range checkInfo.firstArg).end
|
|
}
|
|
(qualifiedToString (qualify ( [ "Basics" ], "always" ) checkInfo))
|
|
]
|
|
|
|
Just _ ->
|
|
keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range initialArg }
|
|
)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
if AstHelpers.isSpecificUnappliedBinaryOperation "*" checkInfo checkInfo.firstArg then
|
|
numberBinaryOperationChecks { two = "*", list = "product", identity = 1 }
|
|
|
|
else
|
|
[]
|
|
, \() ->
|
|
if AstHelpers.isSpecificUnappliedBinaryOperation "+" checkInfo checkInfo.firstArg then
|
|
numberBinaryOperationChecks { two = "+", list = "sum", identity = 0 }
|
|
|
|
else
|
|
[]
|
|
, \() ->
|
|
case Evaluate.getBoolean checkInfo initialArg of
|
|
Undetermined ->
|
|
[]
|
|
|
|
Determined initialBool ->
|
|
if AstHelpers.isSpecificUnappliedBinaryOperation "&&" checkInfo checkInfo.firstArg then
|
|
boolBinaryOperationChecks { two = "&&", list = "all", determining = False } initialBool
|
|
|
|
else if AstHelpers.isSpecificUnappliedBinaryOperation "||" checkInfo checkInfo.firstArg then
|
|
boolBinaryOperationChecks { two = "||", list = "any", determining = True } initialBool
|
|
|
|
else
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listFoldlCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
listFoldlCompositionChecks checkInfo =
|
|
foldAndSetToListCompositionChecks "foldl" checkInfo
|
|
|
|
|
|
listFoldrCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
listFoldrCompositionChecks checkInfo =
|
|
foldAndSetToListCompositionChecks "foldr" checkInfo
|
|
|
|
|
|
foldAndSetToListCompositionChecks : String -> CompositionIntoCheckInfo -> List (Error {})
|
|
foldAndSetToListCompositionChecks foldOperationName checkInfo =
|
|
case ( checkInfo.earlier.fn, checkInfo.earlier.args ) of
|
|
( ( [ "Set" ], "toList" ), [] ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "To fold a set, you don't need to convert to a List"
|
|
, details = [ "Using " ++ qualifiedToString ( [ "Set" ], foldOperationName ) ++ " directly is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
checkInfo.later.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = checkInfo.later.range }
|
|
++ [ Fix.replaceRangeBy checkInfo.later.fnRange
|
|
(qualifiedToString (qualify ( [ "Set" ], foldOperationName ) checkInfo))
|
|
]
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listAllChecks : CheckInfo -> List (Error {})
|
|
listAllChecks checkInfo =
|
|
let
|
|
maybeListArg : Maybe (Node Expression)
|
|
maybeListArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case maybeListArg of
|
|
Just (Node _ (Expression.ListExpr [])) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( [ "List" ], "all" ) ++ " will result in True"
|
|
, details = [ "You can replace this call by True." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "True" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case Evaluate.isAlwaysBoolean checkInfo checkInfo.firstArg of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( [ "List" ], "all" ) ++ " will result in True"
|
|
, details = [ "You can replace this call by True." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByBoolWithIrrelevantLastArgFix { lastArg = maybeListArg, replacement = True, checkInfo = checkInfo })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listAnyChecks : CheckInfo -> List (Error {})
|
|
listAnyChecks checkInfo =
|
|
let
|
|
maybeListArg : Maybe (Node Expression)
|
|
maybeListArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case maybeListArg of
|
|
Just (Node _ (Expression.ListExpr [])) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( [ "List" ], "any" ) ++ " will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "False" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case Evaluate.isAlwaysBoolean checkInfo checkInfo.firstArg of
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( [ "List" ], "any" ) ++ " will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByBoolWithIrrelevantLastArgFix { lastArg = maybeListArg, replacement = False, checkInfo = checkInfo })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case Evaluate.isEqualToSomethingFunction checkInfo.firstArg of
|
|
Nothing ->
|
|
[]
|
|
|
|
Just equatedTo ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ qualifiedToString ( [ "List" ], "member" ) ++ " instead"
|
|
, details = [ "This call to " ++ qualifiedToString ( [ "List" ], "any" ) ++ " checks for the presence of a value, which what " ++ qualifiedToString ( [ "List" ], "member" ) ++ " is for." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange (qualifiedToString (qualify ( [ "List" ], "member" ) checkInfo))
|
|
:: replaceBySubExpressionFix (Node.range checkInfo.firstArg) equatedTo.something
|
|
)
|
|
]
|
|
]
|
|
()
|
|
|
|
|
|
listFilterMapChecks : CheckInfo -> List (Error {})
|
|
listFilterMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case constructsSpecificInAllBranches ( [ "Maybe" ], "Just" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Determined justConstruction ->
|
|
case justConstruction of
|
|
NonDirectConstruction fix ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "filterMap" ) ++ " with a function that will always return Just is the same as using " ++ qualifiedToString ( [ "List" ], "map" )
|
|
, details = [ "You can remove the `Just`s and replace the call by " ++ qualifiedToString ( [ "List" ], "map" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "map" ) checkInfo))
|
|
:: fix
|
|
)
|
|
]
|
|
|
|
DirectConstruction ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "filterMap" ) ++ " with a function that will always return Just is the same as not using " ++ qualifiedToString ( [ "List" ], "filterMap" )
|
|
, details = [ "You can remove this call and replace it by the list itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(toIdentityFix
|
|
{ lastArg = secondArg checkInfo, resources = checkInfo }
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
, \() ->
|
|
case returnsSpecificValueOrFunctionInAllBranches ( [ "Maybe" ], "Nothing" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Determined _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "filterMap" ) ++ " with a function that will always return Nothing will result in an empty list"
|
|
, details = [ "You can remove this call and replace it by an empty list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix "[]" checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
, \() ->
|
|
case secondArg checkInfo of
|
|
Just listArg ->
|
|
if AstHelpers.isIdentity checkInfo.lookupTable checkInfo.firstArg then
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "List" ], "map" ) checkInfo.lookupTable listArg of
|
|
Just listMapCall ->
|
|
[ Rule.errorWithFix
|
|
-- TODO rework error info
|
|
{ message = qualifiedToString ( [ "List" ], "map" ) ++ " and " ++ qualifiedToString ( [ "List" ], "filterMap" ) ++ " identity can be combined using " ++ qualifiedToString ( [ "List" ], "filterMap" )
|
|
, details = [ qualifiedToString ( [ "List" ], "filterMap" ) ++ " is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
{ start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).end }
|
|
(replaceBySubExpressionFix checkInfo.parentRange listArg
|
|
++ [ Fix.replaceRangeBy listMapCall.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "filterMap" ) checkInfo))
|
|
]
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case listArg of
|
|
Node listRange (Expression.ListExpr list) ->
|
|
case collectJusts checkInfo.lookupTable list [] of
|
|
Just justRanges ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary use of " ++ qualifiedToString ( [ "List" ], "filterMap" ) ++ " identity"
|
|
, details = [ "All of the elements in the list are `Just`s, which can be simplified by removing all of the `Just`s." ]
|
|
}
|
|
{ start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).end }
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = listRange }
|
|
++ List.map Fix.removeRange justRanges
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
else
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
collectJusts : ModuleNameLookupTable -> List (Node Expression) -> List Range -> Maybe (List Range)
|
|
collectJusts lookupTable list acc =
|
|
case list of
|
|
[] ->
|
|
Just acc
|
|
|
|
(Node _ element) :: restOfList ->
|
|
case element of
|
|
Expression.Application ((Node justRange (Expression.FunctionOrValue _ "Just")) :: (Node justArgRange _) :: []) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Maybe" ] ->
|
|
collectJusts lookupTable restOfList ({ start = justRange.start, end = justArgRange.start } :: acc)
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
listFilterMapCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
listFilterMapCompositionChecks checkInfo =
|
|
case checkInfo.later.args of
|
|
elementToMaybeMappingArg :: [] ->
|
|
if AstHelpers.isIdentity checkInfo.lookupTable elementToMaybeMappingArg then
|
|
case ( checkInfo.earlier.fn, checkInfo.earlier.args ) of
|
|
( ( [ "List" ], "map" ), _ :: [] ) ->
|
|
[ Rule.errorWithFix
|
|
-- TODO rework error info
|
|
{ message = qualifiedToString ( [ "List" ], "map" ) ++ " and " ++ qualifiedToString ( [ "List" ], "filterMap" ) ++ " identity can be combined using " ++ qualifiedToString ( [ "List" ], "filterMap" )
|
|
, details = [ qualifiedToString ( [ "List" ], "filterMap" ) ++ " is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
-- TODO use laterFnRange
|
|
checkInfo.later.range
|
|
(Fix.replaceRangeBy checkInfo.earlier.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "filterMap" ) checkInfo))
|
|
:: keepOnlyFix { parentRange = checkInfo.parentRange, keep = checkInfo.earlier.range }
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listRangeChecks : CheckInfo -> List (Error {})
|
|
listRangeChecks checkInfo =
|
|
case secondArg checkInfo of
|
|
Just rangeEndArg ->
|
|
case ( Evaluate.getInt checkInfo checkInfo.firstArg, Evaluate.getInt checkInfo rangeEndArg ) of
|
|
( Just rangeStartValue, Just rangeEndValue ) ->
|
|
if rangeStartValue > rangeEndValue then
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( [ "List" ], "range" ) ++ " will result in []"
|
|
, details = [ "The second argument to " ++ qualifiedToString ( [ "List" ], "range" ) ++ " is bigger than the first one, therefore you can replace this list by an empty list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix "[]" checkInfo.parentRange (Just rangeEndValue) checkInfo)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
( Nothing, _ ) ->
|
|
[]
|
|
|
|
( _, Nothing ) ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
listRepeatChecks : CheckInfo -> List (Error {})
|
|
listRepeatChecks checkInfo =
|
|
case Evaluate.getInt checkInfo checkInfo.firstArg of
|
|
Just intValue ->
|
|
if intValue < 1 then
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "List" ], "repeat" ) ++ " will result in an empty list"
|
|
, details = [ "Using " ++ qualifiedToString ( [ "List" ], "repeat" ) ++ " with a number less than 1 will result in an empty list. You can replace this call by an empty list." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix "[]" checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listReverseChecks : CheckInfo -> List (Error {})
|
|
listReverseChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value (AstHelpers.removeParens checkInfo.firstArg) of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "reverse" ) ++ " on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
removeAlongWithOtherFunctionCheck
|
|
reverseReverseCompositionErrorMessage
|
|
(AstHelpers.getSpecificValueOrFunction ( [ "List" ], "reverse" ))
|
|
checkInfo
|
|
]
|
|
()
|
|
|
|
|
|
listSortChecks : CheckInfo -> List (Error {})
|
|
listSortChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case checkInfo.firstArg of
|
|
Node _ (Expression.ListExpr []) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "sort" ) ++ " on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Just _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Sorting a list with a single element will result in the list itself"
|
|
, details = [ "You can replace this call by the list itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix
|
|
{ parentRange = checkInfo.parentRange
|
|
, keep = Node.range checkInfo.firstArg
|
|
}
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listSortByChecks : CheckInfo -> List (Error {})
|
|
listSortByChecks checkInfo =
|
|
firstThatReportsError
|
|
[ case secondArg checkInfo of
|
|
Just listArg ->
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case listArg of
|
|
Node _ (Expression.ListExpr []) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "sortBy" ) ++ " on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable listArg of
|
|
Just _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Sorting a list with a single element will result in the list itself"
|
|
, details = [ "You can replace this call by the list itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix
|
|
{ parentRange = checkInfo.parentRange
|
|
, keep = Node.range listArg
|
|
}
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
|
|
Nothing ->
|
|
\() -> []
|
|
, \() ->
|
|
case AstHelpers.getAlwaysResult checkInfo.lookupTable checkInfo.firstArg of
|
|
Just _ ->
|
|
[ identityError
|
|
{ toFix = qualifiedToString ( [ "List" ], "sortBy" ) ++ " (always a)"
|
|
, lastArgName = "list"
|
|
, lastArg = secondArg checkInfo
|
|
, resources = checkInfo
|
|
}
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
if AstHelpers.isIdentity checkInfo.lookupTable checkInfo.firstArg then
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "sortBy" ) ++ " identity is the same as using " ++ qualifiedToString ( [ "List" ], "sort" )
|
|
, details = [ "You can replace this call by " ++ qualifiedToString ( [ "List" ], "sort" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.fnRange.start
|
|
, end = (Node.range checkInfo.firstArg).end
|
|
}
|
|
(qualifiedToString (qualify ( [ "List" ], "sort" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listSortWithChecks : CheckInfo -> List (Error {})
|
|
listSortWithChecks checkInfo =
|
|
firstThatReportsError
|
|
[ case secondArg checkInfo of
|
|
Just listArg ->
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case listArg of
|
|
Node _ (Expression.ListExpr []) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "sortWith" ) ++ " on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable listArg of
|
|
Just _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Sorting a list with a single element will result in the list itself"
|
|
, details = [ "You can replace this call by the list itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix
|
|
{ parentRange = checkInfo.parentRange
|
|
, keep = Node.range listArg
|
|
}
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
|
|
Nothing ->
|
|
\() -> []
|
|
, \() ->
|
|
let
|
|
alwaysAlwaysOrder : Maybe Order
|
|
alwaysAlwaysOrder =
|
|
AstHelpers.getAlwaysResult checkInfo.lookupTable checkInfo.firstArg
|
|
|> Maybe.andThen (AstHelpers.getAlwaysResult checkInfo.lookupTable)
|
|
|> Maybe.andThen (AstHelpers.getOrder checkInfo.lookupTable)
|
|
in
|
|
case alwaysAlwaysOrder of
|
|
Just order ->
|
|
let
|
|
fixToIdentity : List (Error {})
|
|
fixToIdentity =
|
|
[ identityError
|
|
{ toFix = qualifiedToString ( [ "List" ], "sortWith" ) ++ " (\\_ _ -> " ++ AstHelpers.orderToString order ++ ")"
|
|
, lastArgName = "list"
|
|
, lastArg = secondArg checkInfo
|
|
, resources = checkInfo
|
|
}
|
|
]
|
|
in
|
|
case order of
|
|
LT ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "sortWith" ) ++ " (\\_ _ -> LT) is the same as using " ++ qualifiedToString ( [ "List" ], "reverse" )
|
|
, details = [ "You can replace this call by " ++ qualifiedToString ( [ "List" ], "reverse" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.fnRange.start
|
|
, end = (Node.range checkInfo.firstArg).end
|
|
}
|
|
(qualifiedToString (qualify ( [ "List" ], "reverse" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
EQ ->
|
|
fixToIdentity
|
|
|
|
GT ->
|
|
fixToIdentity
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listTakeChecks : CheckInfo -> List (Error {})
|
|
listTakeChecks checkInfo =
|
|
let
|
|
maybeListArg : Maybe (Node Expression)
|
|
maybeListArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
if AstHelpers.getUncomputedNumberValue checkInfo.firstArg == Just 0 then
|
|
[ Rule.errorWithFix
|
|
{ message = "Taking 0 items from a list will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix "[]" checkInfo.parentRange maybeListArg checkInfo)
|
|
]
|
|
|
|
else
|
|
[]
|
|
, \() ->
|
|
case maybeListArg of
|
|
Just listArg ->
|
|
case determineListLength checkInfo.lookupTable listArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "take" ) ++ " on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listDropChecks : CheckInfo -> List (Error {})
|
|
listDropChecks checkInfo =
|
|
let
|
|
maybeListArg : Maybe (Node Expression)
|
|
maybeListArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Evaluate.getInt checkInfo checkInfo.firstArg of
|
|
Just 0 ->
|
|
[ -- TODO use identityError
|
|
Rule.errorWithFix
|
|
(case maybeListArg of
|
|
Just _ ->
|
|
{ message = "Dropping 0 items from a list will result in the list itself"
|
|
, details = [ "You can replace this call by the list itself." ]
|
|
}
|
|
|
|
Nothing ->
|
|
{ message = "Dropping 0 items from a list will result in the list itself"
|
|
, details = [ "You can replace this function by identity." ]
|
|
}
|
|
)
|
|
checkInfo.fnRange
|
|
(toIdentityFix { lastArg = maybeListArg, resources = checkInfo })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case maybeListArg of
|
|
Just listArg ->
|
|
case determineListLength checkInfo.lookupTable listArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "drop" ) ++ " on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
listMapNChecks : { n : Int } -> CheckInfo -> List (Error {})
|
|
listMapNChecks { n } checkInfo =
|
|
if List.any (\(Node _ list) -> list == Expression.ListExpr []) checkInfo.argsAfterFirst then
|
|
let
|
|
callReplacement : String
|
|
callReplacement =
|
|
multiAlways (n - List.length checkInfo.argsAfterFirst) "[]" checkInfo
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "map" ++ String.fromInt n ) ++ " with any list being [] will result in []"
|
|
, details = [ "You can replace this call by " ++ callReplacement ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange callReplacement ]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
listUnzipChecks : CheckInfo -> List (Error {})
|
|
listUnzipChecks checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "List" ], "unzip" ) ++ " on [] will result in ( [], [] )"
|
|
, details = [ "You can replace this call by ( [], [] )." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "( [], [] )" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
setFromListChecks : CheckInfo -> List (Error {})
|
|
setFromListChecks checkInfo =
|
|
collectionFromListChecks setCollection checkInfo
|
|
++ setFromListSingletonChecks checkInfo
|
|
|
|
|
|
setFromListSingletonChecks : CheckInfo -> List (Error {})
|
|
setFromListSingletonChecks checkInfo =
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Nothing ->
|
|
[]
|
|
|
|
Just listSingleton ->
|
|
[ Rule.errorWithFix
|
|
setFromListSingletonError
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix (Node.range checkInfo.firstArg) listSingleton.element
|
|
++ [ Fix.replaceRangeBy checkInfo.fnRange (qualifiedToString (qualify ( [ "Set" ], "singleton" ) checkInfo)) ]
|
|
)
|
|
]
|
|
|
|
|
|
setFromListSingletonError : { message : String, details : List String }
|
|
setFromListSingletonError =
|
|
{ message = qualifiedToString ( [ "Set" ], "fromList" ) ++ " with a single element can be replaced using " ++ qualifiedToString ( [ "Set" ], "singleton" )
|
|
, details = [ "You can replace this call by " ++ qualifiedToString ( [ "Set" ], "singleton" ) ++ " with the list element itself." ]
|
|
}
|
|
|
|
|
|
setFromListCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
setFromListCompositionChecks checkInfo =
|
|
case ( checkInfo.earlier.fn, checkInfo.earlier.args ) of
|
|
( ( [ "List" ], "singleton" ), [] ) ->
|
|
[ Rule.errorWithFix
|
|
setFromListSingletonError
|
|
checkInfo.later.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Set" ], "singleton" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
subAndCmdBatchChecks : String -> CheckInfo -> List (Error {})
|
|
subAndCmdBatchChecks moduleName checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Replace by " ++ moduleName ++ ".batch"
|
|
, details = [ moduleName ++ ".batch [] and " ++ moduleName ++ ".none are equivalent but the latter is more idiomatic in Elm code" ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Platform", moduleName ], "none" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Expression.ListExpr (arg0 :: arg1 :: arg2Up) ->
|
|
neighboringMap
|
|
(\arg ->
|
|
case AstHelpers.removeParens arg.current of
|
|
Node batchRange (Expression.FunctionOrValue _ "none") ->
|
|
if ModuleNameLookupTable.moduleNameAt checkInfo.lookupTable batchRange == Just [ "Platform", moduleName ] then
|
|
let
|
|
argRange : Range
|
|
argRange =
|
|
Node.range arg.current
|
|
in
|
|
Just
|
|
(Rule.errorWithFix
|
|
{ message = "Unnecessary " ++ moduleName ++ ".none"
|
|
, details = [ moduleName ++ ".none will be ignored by " ++ moduleName ++ ".batch." ]
|
|
}
|
|
argRange
|
|
(case arg.before of
|
|
Just (Node prevRange _) ->
|
|
[ Fix.removeRange { start = prevRange.end, end = argRange.end } ]
|
|
|
|
Nothing ->
|
|
case arg.after of
|
|
Just (Node nextRange _) ->
|
|
[ Fix.removeRange { start = argRange.start, end = nextRange.start } ]
|
|
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Platform", moduleName ], "none" ) checkInfo))
|
|
]
|
|
)
|
|
)
|
|
|
|
else
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
)
|
|
(arg0 :: arg1 :: arg2Up)
|
|
|> List.filterMap identity
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Just listSingletonArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary " ++ moduleName ++ ".batch"
|
|
, details = [ moduleName ++ ".batch with a single element is equal to that element." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange listSingletonArg.element)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
|
|
-- HTML.ATTRIBUTES
|
|
|
|
|
|
htmlAttributesClassListFalseElementError : { message : String, details : List String }
|
|
htmlAttributesClassListFalseElementError =
|
|
{ message = "In a " ++ qualifiedToString ( [ "Html", "Attributes" ], "classList" ) ++ ", a tuple paired with False can be removed"
|
|
, details = [ "You can remove the tuple list element where the second part is False." ]
|
|
}
|
|
|
|
|
|
htmlAttributesClassListChecks : CheckInfo -> List (Error {})
|
|
htmlAttributesClassListChecks checkInfo =
|
|
let
|
|
listArg : Node Expression
|
|
listArg =
|
|
checkInfo.firstArg
|
|
|
|
getTupleWithSpecificSecond : Bool -> Node Expression -> Maybe { range : Range, first : Node Expression }
|
|
getTupleWithSpecificSecond specificBool expressionNode =
|
|
case AstHelpers.getTuple expressionNode of
|
|
Just tuple ->
|
|
case AstHelpers.getSpecificBool specificBool checkInfo.lookupTable tuple.second of
|
|
Just _ ->
|
|
Just { range = tuple.range, first = tuple.first }
|
|
|
|
Nothing ->
|
|
Nothing
|
|
|
|
Nothing ->
|
|
Nothing
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable listArg of
|
|
Just single ->
|
|
case AstHelpers.getTuple single.element of
|
|
Just tuple ->
|
|
case AstHelpers.getBool checkInfo.lookupTable tuple.second of
|
|
Just bool ->
|
|
if bool then
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "Html", "Attributes" ], "classList" ) ++ " with a single tuple paired with True can be replaced with " ++ qualifiedToString ( [ "Html", "Attributes" ], "class" )
|
|
, details = [ "You can replace this call by " ++ qualifiedToString ( [ "Html", "Attributes" ], "class" ) ++ " with the String from the single tuple list element." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix (Node.range listArg) tuple.first
|
|
++ [ Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Html", "Attributes" ], "class" ) checkInfo))
|
|
]
|
|
)
|
|
]
|
|
|
|
else
|
|
[ Rule.errorWithFix htmlAttributesClassListFalseElementError
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy (Node.range listArg) "[]" ]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getListLiteral listArg of
|
|
Just (tuple0 :: tuple1 :: tuple2Up) ->
|
|
case findMapNeighboring (getTupleWithSpecificSecond False) (tuple0 :: tuple1 :: tuple2Up) of
|
|
Just classPart ->
|
|
[ Rule.errorWithFix htmlAttributesClassListFalseElementError
|
|
checkInfo.fnRange
|
|
(listLiteralElementRemoveFix classPart)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case AstHelpers.getCollapsedCons listArg of
|
|
Just classParts ->
|
|
case findMapNeighboring (getTupleWithSpecificSecond False) classParts.consed of
|
|
Just classPart ->
|
|
[ Rule.errorWithFix htmlAttributesClassListFalseElementError
|
|
checkInfo.fnRange
|
|
(collapsedConsRemoveElementFix
|
|
{ toRemove = classPart
|
|
, tailRange = Node.range classParts.tail
|
|
}
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
|
|
-- PARSER
|
|
|
|
|
|
oneOfChecks : CheckInfo -> List (Error {})
|
|
oneOfChecks checkInfo =
|
|
case AstHelpers.getListSingleton checkInfo.lookupTable checkInfo.firstArg of
|
|
Just listSingletonArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary oneOf"
|
|
, details = [ "There is only a single element in the list of elements to try out." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange listSingletonArg.element)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
|
|
-- RANDOM
|
|
|
|
|
|
randomUniformChecks : CheckInfo -> List (Error {})
|
|
randomUniformChecks checkInfo =
|
|
case secondArg checkInfo of
|
|
Just otherOptionsArg ->
|
|
case AstHelpers.getListLiteral otherOptionsArg of
|
|
Just [] ->
|
|
let
|
|
onlyValueRange : Range
|
|
onlyValueRange =
|
|
Node.range checkInfo.firstArg
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Random.uniform with only one possible value can be replaced by Random.constant"
|
|
, details = [ "Only a single value can be produced by this Random.uniform call. You can replace the call with Random.constant with the value." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy { start = checkInfo.parentRange.start, end = onlyValueRange.start }
|
|
(qualifiedToString (qualify ( [ "Random" ], "constant" ) checkInfo) ++ " ")
|
|
, Fix.removeRange { start = onlyValueRange.end, end = checkInfo.parentRange.end }
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
randomWeightedChecks : CheckInfo -> List (Error {})
|
|
randomWeightedChecks checkInfo =
|
|
case secondArg checkInfo of
|
|
Just otherOptionsArg ->
|
|
case AstHelpers.getListLiteral otherOptionsArg of
|
|
Just [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Random.weighted with only one possible value can be replaced by Random.constant"
|
|
, details = [ "Only a single value can be produced by this Random.weighted call. You can replace the call with Random.constant with the value." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(case Node.value checkInfo.firstArg of
|
|
Expression.TupledExpression (_ :: (Node valuePartRange _) :: []) ->
|
|
[ Fix.replaceRangeBy { start = checkInfo.parentRange.start, end = valuePartRange.start }
|
|
(qualifiedToString (qualify ( [ "Random" ], "constant" ) checkInfo) ++ " ")
|
|
, Fix.removeRange { start = valuePartRange.end, end = checkInfo.parentRange.end }
|
|
]
|
|
|
|
_ ->
|
|
let
|
|
tupleRange : Range
|
|
tupleRange =
|
|
Node.range checkInfo.firstArg
|
|
in
|
|
[ Fix.replaceRangeBy { start = checkInfo.parentRange.start, end = tupleRange.start }
|
|
(qualifiedToString (qualify ( [ "Random" ], "constant" ) checkInfo) ++ " (Tuple.first ")
|
|
, Fix.replaceRangeBy { start = tupleRange.end, end = checkInfo.parentRange.end }
|
|
")"
|
|
]
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
randomListChecks : CheckInfo -> List (Error {})
|
|
randomListChecks checkInfo =
|
|
let
|
|
maybeElementGeneratorArg : Maybe (Node Expression)
|
|
maybeElementGeneratorArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Evaluate.getInt checkInfo checkInfo.firstArg of
|
|
Just 1 ->
|
|
[ Rule.errorWithFix
|
|
{ message = qualifiedToString ( [ "Random" ], "list" ) ++ " 1 can be replaced by " ++ qualifiedToString ( [ "Random" ], "map" ) ++ " " ++ qualifiedToString ( [ "List" ], "singleton" )
|
|
, details = [ "This " ++ qualifiedToString ( [ "Random" ], "list" ) ++ " call always produces a list with one generated element. This means you can replace the call with " ++ qualifiedToString ( [ "Random" ], "map" ) ++ " " ++ qualifiedToString ( [ "List" ], "singleton" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy
|
|
(Range.combine [ checkInfo.fnRange, Node.range checkInfo.firstArg ])
|
|
(qualifiedToString (qualify ( [ "Random" ], "map" ) checkInfo)
|
|
++ " "
|
|
++ qualifiedToString (qualify ( [ "List" ], "singleton" ) checkInfo)
|
|
)
|
|
]
|
|
]
|
|
|
|
Just non1Length ->
|
|
if non1Length <= 0 then
|
|
let
|
|
replacement : String
|
|
replacement =
|
|
replacementWithIrrelevantLastArg
|
|
{ forNoLastArg =
|
|
qualifiedToString (qualify ( [ "Random" ], "constant" ) checkInfo)
|
|
++ " []"
|
|
, lastArg = maybeElementGeneratorArg
|
|
, resources = checkInfo
|
|
}
|
|
|
|
callDescription : String
|
|
callDescription =
|
|
case non1Length of
|
|
0 ->
|
|
"Random.list 0"
|
|
|
|
_ ->
|
|
"Random.list with a negative length"
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = callDescription ++ " can be replaced by Random.constant []"
|
|
, details = [ callDescription ++ " always generates an empty list. This means you can replace the call with " ++ replacement ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange replacement ]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case maybeElementGeneratorArg of
|
|
Just elementGeneratorArg ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Random" ], "constant" ) checkInfo.lookupTable elementGeneratorArg of
|
|
Just constantCall ->
|
|
let
|
|
currentAsString : String
|
|
currentAsString =
|
|
qualifiedToString ( [ "Random" ], "list" ) ++ " n (" ++ qualifiedToString ( [ "Random" ], "constant" ) ++ " el)"
|
|
|
|
replacementAsString : String
|
|
replacementAsString =
|
|
qualifiedToString ( [ "Random" ], "constant" ) ++ " (" ++ qualifiedToString ( [ "List" ], "repeat" ) ++ " n el)"
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = currentAsString ++ " can be replaced by " ++ replacementAsString
|
|
, details = [ currentAsString ++ " generates the same value for each of the n elements. This means you can replace the call with " ++ replacementAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix constantCall.nodeRange constantCall.firstArg
|
|
++ [ Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "List" ], "repeat" ) checkInfo))
|
|
, Fix.insertAt checkInfo.parentRange.start
|
|
(qualifiedToString (qualify ( [ "Random" ], "constant" ) checkInfo)
|
|
++ " ("
|
|
)
|
|
, Fix.insertAt checkInfo.parentRange.end ")"
|
|
]
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
randomMapChecks : CheckInfo -> List (Error {})
|
|
randomMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> mapIdentityChecks { moduleName = [ "Random" ], represents = "random generator" } checkInfo
|
|
, \() -> mapPureChecks { moduleName = [ "Random" ], pure = "constant", map = "map" } checkInfo
|
|
, \() -> randomMapAlwaysChecks checkInfo
|
|
]
|
|
()
|
|
|
|
|
|
randomMapCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
randomMapCompositionChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> pureToMapCompositionChecks { moduleName = [ "Random" ], pure = "constant", map = "map" } checkInfo
|
|
, \() -> randomMapAlwaysCompositionChecks checkInfo
|
|
]
|
|
()
|
|
|
|
|
|
randomMapAlwaysErrorInfo : { message : String, details : List String }
|
|
randomMapAlwaysErrorInfo =
|
|
{ message = "Always mapping to the same value is equivalent to Random.constant"
|
|
, details = [ "Since your Random.map call always produces the same value, you can replace the whole call by Random.constant that value." ]
|
|
}
|
|
|
|
|
|
randomMapAlwaysChecks : CheckInfo -> List (Error {})
|
|
randomMapAlwaysChecks checkInfo =
|
|
case AstHelpers.getAlwaysResult checkInfo.lookupTable checkInfo.firstArg of
|
|
Just (Node alwaysMapResultRange alwaysMapResult) ->
|
|
let
|
|
( leftParenIfRequired, rightParenIfRequired ) =
|
|
if needsParens alwaysMapResult then
|
|
( "(", ")" )
|
|
|
|
else
|
|
( "", "" )
|
|
in
|
|
[ Rule.errorWithFix
|
|
randomMapAlwaysErrorInfo
|
|
checkInfo.fnRange
|
|
(case secondArg checkInfo of
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.parentRange.start, end = alwaysMapResultRange.start }
|
|
(qualifiedToString (qualify ( [ "Basics" ], "always" ) checkInfo)
|
|
++ " ("
|
|
++ qualifiedToString (qualify ( [ "Random" ], "constant" ) checkInfo)
|
|
++ " "
|
|
++ leftParenIfRequired
|
|
)
|
|
, Fix.replaceRangeBy
|
|
{ start = alwaysMapResultRange.end, end = checkInfo.parentRange.end }
|
|
(rightParenIfRequired ++ ")")
|
|
]
|
|
|
|
Just _ ->
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.parentRange.start, end = alwaysMapResultRange.start }
|
|
(qualifiedToString (qualify ( [ "Random" ], "constant" ) checkInfo)
|
|
++ " "
|
|
++ leftParenIfRequired
|
|
)
|
|
, Fix.replaceRangeBy
|
|
{ start = alwaysMapResultRange.end, end = checkInfo.parentRange.end }
|
|
rightParenIfRequired
|
|
]
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
randomMapAlwaysCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
randomMapAlwaysCompositionChecks checkInfo =
|
|
case ( checkInfo.earlier.fn, checkInfo.earlier.args ) of
|
|
( ( [ "Basics" ], "always" ), [] ) ->
|
|
[ Rule.errorWithFix
|
|
randomMapAlwaysErrorInfo
|
|
checkInfo.later.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Random" ], "constant" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
|
|
--
|
|
|
|
|
|
type alias Collection =
|
|
{ moduleName : ModuleName
|
|
, represents : String
|
|
, emptyAsString : QualifyResources {} -> String
|
|
, emptyDescription : String
|
|
, isEmpty : ModuleNameLookupTable -> Node Expression -> Bool
|
|
, nameForSize : String
|
|
, determineSize : ModuleNameLookupTable -> Node Expression -> Maybe CollectionSize
|
|
}
|
|
|
|
|
|
extractQualifyResources : QualifyResources a -> QualifyResources {}
|
|
extractQualifyResources resources =
|
|
{ importLookup = resources.importLookup
|
|
, moduleBindings = resources.moduleBindings
|
|
, localBindings = resources.localBindings
|
|
}
|
|
|
|
|
|
emptyAsString : QualifyResources a -> { emptiable | emptyAsString : QualifyResources {} -> String } -> String
|
|
emptyAsString qualifyResources emptiable =
|
|
emptiable.emptyAsString (extractQualifyResources qualifyResources)
|
|
|
|
|
|
listCollection : Collection
|
|
listCollection =
|
|
{ moduleName = [ "List" ]
|
|
, represents = "list"
|
|
, emptyAsString = \_ -> "[]"
|
|
, emptyDescription = "[]"
|
|
, isEmpty = \_ expr -> AstHelpers.getListLiteral expr == Just []
|
|
, nameForSize = "length"
|
|
, determineSize = determineListLength
|
|
}
|
|
|
|
|
|
setCollection : Collection
|
|
setCollection =
|
|
{ moduleName = [ "Set" ]
|
|
, represents = "set"
|
|
, emptyAsString =
|
|
\resources ->
|
|
qualifiedToString (qualify ( [ "Set" ], "empty" ) resources)
|
|
, emptyDescription = qualifiedToString ( [ "Set" ], "empty" )
|
|
, isEmpty =
|
|
\lookupTable expr ->
|
|
isJust (AstHelpers.getSpecificValueOrFunction ( [ "Set" ], "empty" ) lookupTable expr)
|
|
, nameForSize = "size"
|
|
, determineSize = setDetermineSize
|
|
}
|
|
|
|
|
|
setDetermineSize :
|
|
ModuleNameLookupTable
|
|
-> Node Expression
|
|
-> Maybe CollectionSize
|
|
setDetermineSize lookupTable expressionNode =
|
|
findMap (\f -> f ())
|
|
[ \() ->
|
|
case AstHelpers.getSpecificValueOrFunction ( [ "Set" ], "empty" ) lookupTable expressionNode of
|
|
Just _ ->
|
|
Just (Exactly 0)
|
|
|
|
Nothing ->
|
|
Nothing
|
|
, \() ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Set" ], "singleton" ) lookupTable expressionNode of
|
|
Just _ ->
|
|
Just (Exactly 1)
|
|
|
|
Nothing ->
|
|
Nothing
|
|
, \() ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Set" ], "fromList" ) lookupTable expressionNode of
|
|
Just fromListCall ->
|
|
case AstHelpers.getListLiteral fromListCall.firstArg of
|
|
Just [] ->
|
|
Just (Exactly 0)
|
|
|
|
Just (_ :: []) ->
|
|
Just (Exactly 1)
|
|
|
|
Just (el0 :: el1 :: el2Up) ->
|
|
case traverse getComparableExpression (el0 :: el1 :: el2Up) of
|
|
Nothing ->
|
|
Just NotEmpty
|
|
|
|
Just comparableExpressions ->
|
|
comparableExpressions |> unique |> List.length |> Exactly |> Just
|
|
|
|
Nothing ->
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
]
|
|
|
|
|
|
dictCollection : Collection
|
|
dictCollection =
|
|
{ moduleName = [ "Dict" ]
|
|
, represents = "Dict"
|
|
, emptyAsString =
|
|
\resources ->
|
|
qualifiedToString (qualify ( [ "Dict" ], "empty" ) resources)
|
|
, emptyDescription = qualifiedToString ( [ "Dict" ], "empty" )
|
|
, isEmpty =
|
|
\lookupTable expr ->
|
|
isJust (AstHelpers.getSpecificValueOrFunction ( [ "Dict" ], "empty" ) lookupTable expr)
|
|
, nameForSize = "size"
|
|
, determineSize = dictDetermineSize
|
|
}
|
|
|
|
|
|
dictDetermineSize :
|
|
ModuleNameLookupTable
|
|
-> Node Expression
|
|
-> Maybe CollectionSize
|
|
dictDetermineSize lookupTable expressionNode =
|
|
findMap (\f -> f ())
|
|
[ \() ->
|
|
case AstHelpers.getSpecificValueOrFunction ( [ "Dict" ], "empty" ) lookupTable expressionNode of
|
|
Just _ ->
|
|
Just (Exactly 0)
|
|
|
|
Nothing ->
|
|
Nothing
|
|
, \() ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Dict" ], "singleton" ) lookupTable expressionNode of
|
|
Just singletonCall ->
|
|
case singletonCall.argsAfterFirst of
|
|
_ :: [] ->
|
|
Just (Exactly 1)
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Nothing ->
|
|
Nothing
|
|
, \() ->
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Dict" ], "fromList" ) lookupTable expressionNode of
|
|
Just fromListCall ->
|
|
case AstHelpers.getListLiteral fromListCall.firstArg of
|
|
Just [] ->
|
|
Just (Exactly 0)
|
|
|
|
Just (_ :: []) ->
|
|
Just (Exactly 1)
|
|
|
|
Just (el0 :: el1 :: el2Up) ->
|
|
case traverse getComparableExpressionInTupleFirst (el0 :: el1 :: el2Up) of
|
|
Nothing ->
|
|
Just NotEmpty
|
|
|
|
Just comparableExpressions ->
|
|
comparableExpressions |> unique |> List.length |> Exactly |> Just
|
|
|
|
Nothing ->
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
]
|
|
|
|
|
|
type alias Mappable =
|
|
{ moduleName : ModuleName
|
|
, represents : String
|
|
, emptyAsString : QualifyResources {} -> String
|
|
, emptyDescription : String
|
|
, isEmpty : ModuleNameLookupTable -> Node Expression -> Bool
|
|
}
|
|
|
|
|
|
type alias Defaultable =
|
|
{ moduleName : ModuleName
|
|
, represents : String
|
|
, emptyAsString : QualifyResources {} -> String
|
|
, emptyDescription : String
|
|
, isEmpty : ModuleNameLookupTable -> Node Expression -> Bool
|
|
, isSomethingConstructor : QualifyResources {} -> String
|
|
}
|
|
|
|
|
|
maybeCollection : Defaultable
|
|
maybeCollection =
|
|
{ moduleName = [ "Maybe" ]
|
|
, represents = "maybe"
|
|
, emptyAsString =
|
|
\resources ->
|
|
qualifiedToString (qualify ( [ "Maybe" ], "Nothing" ) resources)
|
|
, emptyDescription = "Nothing"
|
|
, isEmpty =
|
|
\lookupTable expr ->
|
|
isJust (AstHelpers.getSpecificValueOrFunction ( [ "Maybe" ], "Nothing" ) lookupTable expr)
|
|
, isSomethingConstructor =
|
|
\resources ->
|
|
qualifiedToString (qualify ( [ "Maybe" ], "Just" ) resources)
|
|
}
|
|
|
|
|
|
resultCollection : Defaultable
|
|
resultCollection =
|
|
{ moduleName = [ "Result" ]
|
|
, represents = "result"
|
|
, emptyAsString =
|
|
\resources ->
|
|
qualifiedToString (qualify ( [ "Maybe" ], "Nothing" ) resources)
|
|
, emptyDescription = "an error"
|
|
, isEmpty =
|
|
\lookupTable expr ->
|
|
isJust (AstHelpers.getSpecificFunctionCall ( [ "Result" ], "Err" ) lookupTable expr)
|
|
, isSomethingConstructor =
|
|
\resources ->
|
|
qualifiedToString (qualify ( [ "Result" ], "Ok" ) resources)
|
|
}
|
|
|
|
|
|
cmdCollection : Mappable
|
|
cmdCollection =
|
|
{ moduleName = [ "Platform", "Cmd" ]
|
|
, represents = "command"
|
|
, emptyAsString =
|
|
\resources ->
|
|
qualifiedToString (qualify ( [ "Platform", "Cmd" ], "none" ) resources)
|
|
, emptyDescription =
|
|
-- TODO change to qualifiedToString ( [ "Platform", "Cmd" ], "none" )
|
|
"Cmd.none"
|
|
, isEmpty =
|
|
\lookupTable expr ->
|
|
isJust (AstHelpers.getSpecificValueOrFunction ( [ "Platform", "Cmd" ], "none" ) lookupTable expr)
|
|
}
|
|
|
|
|
|
subCollection : Mappable
|
|
subCollection =
|
|
{ moduleName = [ "Platform", "Sub" ]
|
|
, represents = "subscription"
|
|
, emptyAsString =
|
|
\resources ->
|
|
qualifiedToString (qualify ( [ "Platform", "Sub" ], "none" ) resources)
|
|
, emptyDescription =
|
|
-- TODO change to qualifiedToString ( [ "Platform", "Sub" ], "none" )
|
|
"Sub.none"
|
|
, isEmpty =
|
|
\lookupTable expr ->
|
|
isJust (AstHelpers.getSpecificValueOrFunction ( [ "Platform", "Sub" ], "none" ) lookupTable expr)
|
|
}
|
|
|
|
|
|
collectionMapChecks :
|
|
{ a
|
|
| moduleName : ModuleName
|
|
, represents : String
|
|
, emptyDescription : String
|
|
, emptyAsString : QualifyResources {} -> String
|
|
, isEmpty : ModuleNameLookupTable -> Node Expression -> Bool
|
|
}
|
|
-> CheckInfo
|
|
-> List (Error {})
|
|
collectionMapChecks collection checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> mapIdentityChecks collection checkInfo
|
|
, \() ->
|
|
case secondArg checkInfo of
|
|
Just collectionArg ->
|
|
if collection.isEmpty checkInfo.lookupTable collectionArg then
|
|
[ Rule.errorWithFix
|
|
-- TODO rework error info
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "map" ) ++ " on " ++ collection.emptyDescription ++ " will result in " ++ collection.emptyDescription
|
|
, details = [ "You can replace this call by " ++ collection.emptyDescription ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range collectionArg })
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
{-| TODO merge with identityError
|
|
-}
|
|
mapIdentityChecks :
|
|
{ a
|
|
| moduleName : ModuleName
|
|
, represents : String
|
|
}
|
|
-> CheckInfo
|
|
-> List (Error {})
|
|
mapIdentityChecks mappable checkInfo =
|
|
if AstHelpers.isIdentity checkInfo.lookupTable checkInfo.firstArg then
|
|
-- TODO use identityError
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( mappable.moduleName, "map" ) ++ " with an identity function is the same as not using " ++ qualifiedToString ( mappable.moduleName, "map" )
|
|
, details = [ "You can remove this call and replace it by the " ++ mappable.represents ++ " itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(toIdentityFix
|
|
{ lastArg = secondArg checkInfo, resources = checkInfo }
|
|
)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
mapPureChecks :
|
|
{ a | moduleName : List String, pure : String, map : String }
|
|
-> CheckInfo
|
|
-> List (Error {})
|
|
mapPureChecks mappable checkInfo =
|
|
case secondArg checkInfo of
|
|
Just mappableArg ->
|
|
case sameCallInAllBranches ( mappable.moduleName, mappable.pure ) checkInfo.lookupTable mappableArg of
|
|
Determined pureCalls ->
|
|
let
|
|
mappingArgRange : Range
|
|
mappingArgRange =
|
|
Node.range checkInfo.firstArg
|
|
|
|
removePureCalls : List Fix
|
|
removePureCalls =
|
|
List.concatMap
|
|
(\pureCall ->
|
|
keepOnlyFix
|
|
{ parentRange = pureCall.nodeRange
|
|
, keep = Node.range pureCall.firstArg
|
|
}
|
|
)
|
|
pureCalls
|
|
in
|
|
[ Rule.errorWithFix
|
|
-- TODO reword error info to something more like resultMapErrorOnErrErrorInfo
|
|
{ message = "Calling " ++ qualifiedToString ( mappable.moduleName, mappable.map ) ++ " on a value that is " ++ mappable.pure
|
|
, details = [ "The function can be called without " ++ qualifiedToString ( mappable.moduleName, mappable.map ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(if checkInfo.usingRightPizza then
|
|
[ Fix.removeRange { start = checkInfo.fnRange.start, end = mappingArgRange.start }
|
|
, Fix.insertAt mappingArgRange.end
|
|
(" |> " ++ qualifiedToString (qualify ( mappable.moduleName, mappable.pure ) checkInfo))
|
|
]
|
|
++ removePureCalls
|
|
|
|
else
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.parentRange.start, end = mappingArgRange.start }
|
|
(qualifiedToString (qualify ( mappable.moduleName, mappable.pure ) checkInfo) ++ " (")
|
|
, Fix.insertAt checkInfo.parentRange.end ")"
|
|
]
|
|
++ removePureCalls
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
pureToMapCompositionChecks :
|
|
{ a | moduleName : ModuleName, pure : String, map : String }
|
|
-> CompositionIntoCheckInfo
|
|
-> List (Error {})
|
|
pureToMapCompositionChecks mappable checkInfo =
|
|
case
|
|
( checkInfo.earlier.fn == ( mappable.moduleName, mappable.pure )
|
|
, checkInfo.later.args
|
|
)
|
|
of
|
|
( True, (Node mapperFunctionRange _) :: _ ) ->
|
|
let
|
|
fixes : List Fix
|
|
fixes =
|
|
case checkInfo.direction of
|
|
LeftToRight ->
|
|
[ Fix.removeRange
|
|
{ start = checkInfo.parentRange.start, end = mapperFunctionRange.start }
|
|
, Fix.insertAt mapperFunctionRange.end
|
|
(" >> " ++ qualifiedToString (qualify ( mappable.moduleName, mappable.pure ) checkInfo))
|
|
]
|
|
|
|
RightToLeft ->
|
|
[ Fix.replaceRangeBy
|
|
{ start = checkInfo.parentRange.start, end = mapperFunctionRange.start }
|
|
(qualifiedToString (qualify ( mappable.moduleName, mappable.pure ) checkInfo) ++ " << ")
|
|
, Fix.removeRange { start = mapperFunctionRange.end, end = checkInfo.parentRange.end }
|
|
]
|
|
in
|
|
[ Rule.errorWithFix
|
|
-- TODO reword error info
|
|
{ message = "Calling " ++ qualifiedToString ( mappable.moduleName, mappable.map ) ++ " on a value that is " ++ mappable.pure
|
|
, details = [ "The function can be called without " ++ qualifiedToString ( mappable.moduleName, mappable.map ) ++ "." ]
|
|
}
|
|
checkInfo.later.fnRange
|
|
fixes
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
maybeAndThenChecks : CheckInfo -> List (Error {})
|
|
maybeAndThenChecks checkInfo =
|
|
let
|
|
maybeEmptyAsString : String
|
|
maybeEmptyAsString =
|
|
emptyAsString checkInfo maybeCollection
|
|
|
|
maybeMaybeArg : Maybe (Node Expression)
|
|
maybeMaybeArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ case maybeMaybeArg of
|
|
Just maybeArg ->
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case sameCallInAllBranches ( [ "Maybe" ], "Just" ) checkInfo.lookupTable maybeArg of
|
|
Determined justCalls ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling " ++ qualifiedToString ( maybeCollection.moduleName, "andThen" ) ++ " on a value that is known to be Just"
|
|
, details = [ "You can remove the Just and just call the function directly." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.removeRange { start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
:: List.concatMap (\justCall -> replaceBySubExpressionFix justCall.nodeRange justCall.firstArg) justCalls
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
, \() ->
|
|
case sameValueOrFunctionInAllBranches ( [ "Maybe" ], "Nothing" ) checkInfo.lookupTable maybeArg of
|
|
Determined _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( maybeCollection.moduleName, "andThen" ) ++ " on " ++ maybeEmptyAsString ++ " will result in " ++ maybeEmptyAsString
|
|
, details = [ "You can replace this call by " ++ maybeEmptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range maybeArg })
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
|
|
Nothing ->
|
|
\() -> []
|
|
, \() ->
|
|
case constructsSpecificInAllBranches ( [ "Maybe" ], "Just" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Determined justConstruction ->
|
|
case justConstruction of
|
|
NonDirectConstruction fix ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ qualifiedToString ( maybeCollection.moduleName, "map" ) ++ " instead"
|
|
, details = [ "Using " ++ qualifiedToString ( maybeCollection.moduleName, "andThen" ) ++ " with a function that always returns Just is the same thing as using " ++ qualifiedToString ( maybeCollection.moduleName, "map" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( maybeCollection.moduleName, "map" ) checkInfo))
|
|
:: fix
|
|
)
|
|
]
|
|
|
|
DirectConstruction ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( maybeCollection.moduleName, "andThen" ) ++ " with a function that will always return Just is the same as not using " ++ qualifiedToString ( maybeCollection.moduleName, "andThen" )
|
|
, details = [ "You can remove this call and replace it by the value itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(toIdentityFix
|
|
{ lastArg = maybeMaybeArg, resources = checkInfo }
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
, \() ->
|
|
case returnsSpecificValueOrFunctionInAllBranches ( [ "Maybe" ], "Nothing" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Determined _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( maybeCollection.moduleName, "andThen" ) ++ " with a function that will always return Nothing will result in Nothing"
|
|
, details = [ "You can remove this call and replace it by Nothing." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix maybeEmptyAsString checkInfo.parentRange (secondArg checkInfo) checkInfo)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
resultAndThenChecks : CheckInfo -> List (Error {})
|
|
resultAndThenChecks checkInfo =
|
|
let
|
|
maybeResultArg : Maybe (Node Expression)
|
|
maybeResultArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ case maybeResultArg of
|
|
Just resultArg ->
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case sameCallInAllBranches ( [ "Result" ], "Ok" ) checkInfo.lookupTable resultArg of
|
|
Determined okCalls ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling " ++ qualifiedToString ( resultCollection.moduleName, "andThen" ) ++ " on a value that is known to be Ok"
|
|
, details = [ "You can remove the Ok and just call the function directly." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.removeRange { start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
:: List.concatMap (\okCall -> replaceBySubExpressionFix okCall.nodeRange okCall.firstArg) okCalls
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
, \() ->
|
|
case sameCallInAllBranches ( [ "Result" ], "Err" ) checkInfo.lookupTable resultArg of
|
|
Determined _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( resultCollection.moduleName, "andThen" ) ++ " on an error will result in the error"
|
|
, details = [ "You can replace this call by the error itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range resultArg })
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
|
|
Nothing ->
|
|
\() -> []
|
|
, \() ->
|
|
case constructsSpecificInAllBranches ( [ "Result" ], "Ok" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Determined okConstruction ->
|
|
case okConstruction of
|
|
NonDirectConstruction fix ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ qualifiedToString ( [ "Result" ], "map" ) ++ " instead"
|
|
, details = [ "Using " ++ qualifiedToString ( [ "Result" ], "andThen" ) ++ " with a function that always returns Ok is the same thing as using " ++ qualifiedToString ( [ "Result" ], "map" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( resultCollection.moduleName, "map" ) checkInfo))
|
|
:: fix
|
|
)
|
|
]
|
|
|
|
DirectConstruction ->
|
|
[ Rule.errorWithFix
|
|
-- TODO use identityError and replace Just by Ok
|
|
{ message = "Using " ++ qualifiedToString ( [ "Result" ], "andThen" ) ++ " with a function that will always return Just is the same as not using " ++ qualifiedToString ( [ "Result" ], "andThen" )
|
|
, details = [ "You can remove this call and replace it by the value itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(toIdentityFix
|
|
{ lastArg = maybeResultArg, resources = checkInfo }
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
resultWithDefaultChecks : CheckInfo -> List (Error {})
|
|
resultWithDefaultChecks checkInfo =
|
|
case secondArg checkInfo of
|
|
Just resultArg ->
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case sameCallInAllBranches ( [ "Result" ], "Ok" ) checkInfo.lookupTable resultArg of
|
|
Determined okCalls ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "Result" ], "withDefault" ) ++ " on a value that is Ok will result in that value"
|
|
, details = [ "You can replace this call by the value wrapped in Ok." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(List.concatMap (\okCall -> replaceBySubExpressionFix okCall.nodeRange okCall.firstArg) okCalls
|
|
++ keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range resultArg }
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
, \() ->
|
|
case sameCallInAllBranches ( [ "Result" ], "Err" ) checkInfo.lookupTable resultArg of
|
|
Determined _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "Result" ], "withDefault" ) ++ " on an error will result in the default value"
|
|
, details = [ "You can replace this call by the default value." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange { start = checkInfo.parentRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
, Fix.removeRange { start = (Node.range checkInfo.firstArg).end, end = checkInfo.parentRange.end }
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
resultToMaybeChecks : CheckInfo -> List (Error {})
|
|
resultToMaybeChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case sameCallInAllBranches ( [ "Result" ], "Ok" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Determined okCalls ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "Result" ], "toMaybe" ) ++ " on a value that is Ok will result in Just that value itself"
|
|
, details = [ "You can replace this call by the value itself wrapped in Just." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(List.concatMap (\okCall -> replaceBySubExpressionFix okCall.nodeRange okCall.firstArg) okCalls
|
|
++ [ Fix.replaceRangeBy checkInfo.fnRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Just" ) checkInfo))
|
|
]
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
, \() ->
|
|
case sameCallInAllBranches ( [ "Result" ], "Err" ) checkInfo.lookupTable checkInfo.firstArg of
|
|
Determined _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "Result" ], "toMaybe" ) ++ " on an error will result in Nothing"
|
|
, details = [ "You can replace this call by Nothing." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Nothing" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
resultToMaybeCompositionChecks : CompositionIntoCheckInfo -> List (Error {})
|
|
resultToMaybeCompositionChecks checkInfo =
|
|
let
|
|
resultToMaybeFunctionRange : Range
|
|
resultToMaybeFunctionRange =
|
|
checkInfo.later.fnRange
|
|
in
|
|
case ( checkInfo.earlier.fn, checkInfo.earlier.args ) of
|
|
( ( [ "Result" ], "Err" ), [] ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "Result" ], "toMaybe" ) ++ " on an error will result in Nothing"
|
|
, details = [ "You can replace this call by always Nothing." ]
|
|
}
|
|
resultToMaybeFunctionRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "always" ) checkInfo)
|
|
++ " "
|
|
++ qualifiedToString (qualify ( [ "Maybe" ], "Nothing" ) checkInfo)
|
|
)
|
|
]
|
|
]
|
|
|
|
( ( [ "Result" ], "Ok" ), [] ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "Result" ], "toMaybe" ) ++ " on a value that is Ok will result in Just that value itself"
|
|
, details = [ "You can replace this call by Just." ]
|
|
}
|
|
resultToMaybeFunctionRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Maybe" ], "Just" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
pipelineChecks :
|
|
{ commentRanges : List Range
|
|
, extractSourceCode : Range -> String
|
|
, nodeRange : Range
|
|
, pipedInto : Node Expression
|
|
, arg : Node Expression
|
|
, direction : LeftOrRightDirection
|
|
}
|
|
-> List (Error {})
|
|
pipelineChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> pipingIntoCompositionChecks { commentRanges = checkInfo.commentRanges, extractSourceCode = checkInfo.extractSourceCode } checkInfo.direction checkInfo.pipedInto
|
|
, \() -> fullyAppliedLambdaInPipelineChecks { nodeRange = checkInfo.nodeRange, function = checkInfo.pipedInto, firstArgument = checkInfo.arg }
|
|
]
|
|
()
|
|
|
|
|
|
fullyAppliedLambdaInPipelineChecks : { nodeRange : Range, firstArgument : Node Expression, function : Node Expression } -> List (Error {})
|
|
fullyAppliedLambdaInPipelineChecks checkInfo =
|
|
case Node.value checkInfo.function of
|
|
Expression.ParenthesizedExpression (Node lambdaRange (Expression.LambdaExpression lambda)) ->
|
|
case Node.value (AstHelpers.removeParens checkInfo.firstArgument) of
|
|
Expression.OperatorApplication "|>" _ _ _ ->
|
|
[]
|
|
|
|
Expression.OperatorApplication "<|" _ _ _ ->
|
|
[]
|
|
|
|
_ ->
|
|
appliedLambdaChecks
|
|
{ nodeRange = checkInfo.nodeRange
|
|
, lambdaRange = lambdaRange
|
|
, lambda = lambda
|
|
}
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
type LeftOrRightDirection
|
|
= RightToLeft
|
|
| LeftToRight
|
|
|
|
|
|
pipingIntoCompositionChecks :
|
|
{ commentRanges : List Range, extractSourceCode : Range -> String }
|
|
-> LeftOrRightDirection
|
|
-> Node Expression
|
|
-> List (Error {})
|
|
pipingIntoCompositionChecks context compositionDirection expressionNode =
|
|
let
|
|
( opToFind, replacement ) =
|
|
case compositionDirection of
|
|
RightToLeft ->
|
|
( "<<", "<|" )
|
|
|
|
LeftToRight ->
|
|
( ">>", "|>" )
|
|
|
|
pipingIntoCompositionChecksHelp : Node Expression -> Maybe { opToReplaceRange : Range, fixes : List Fix, firstStepIsComposition : Bool }
|
|
pipingIntoCompositionChecksHelp subExpression =
|
|
case Node.value subExpression of
|
|
Expression.ParenthesizedExpression inParens ->
|
|
case pipingIntoCompositionChecksHelp inParens of
|
|
Nothing ->
|
|
Nothing
|
|
|
|
Just error ->
|
|
if error.firstStepIsComposition then
|
|
-- parens can safely be removed
|
|
Just
|
|
{ error
|
|
| fixes =
|
|
removeBoundariesFix subExpression ++ error.fixes
|
|
}
|
|
|
|
else
|
|
-- inside parenthesis is checked separately because
|
|
-- the parens here can't safely be removed
|
|
Nothing
|
|
|
|
Expression.OperatorApplication symbol _ left right ->
|
|
let
|
|
continuedSearch : Maybe { opToReplaceRange : Range, fixes : List Fix, firstStepIsComposition : Bool }
|
|
continuedSearch =
|
|
case compositionDirection of
|
|
RightToLeft ->
|
|
pipingIntoCompositionChecksHelp left
|
|
|
|
LeftToRight ->
|
|
pipingIntoCompositionChecksHelp right
|
|
in
|
|
if symbol == replacement then
|
|
Maybe.map (\errors -> { errors | firstStepIsComposition = False })
|
|
continuedSearch
|
|
|
|
else if symbol == opToFind then
|
|
let
|
|
opToFindRange : Range
|
|
opToFindRange =
|
|
findOperatorRange
|
|
{ operator = opToFind
|
|
, commentRanges = context.commentRanges
|
|
, extractSourceCode = context.extractSourceCode
|
|
, leftRange = Node.range left
|
|
, rightRange = Node.range right
|
|
}
|
|
in
|
|
Just
|
|
{ opToReplaceRange = opToFindRange
|
|
, fixes =
|
|
Fix.replaceRangeBy opToFindRange replacement
|
|
:: (case continuedSearch of
|
|
Nothing ->
|
|
[]
|
|
|
|
Just additionalErrorsFound ->
|
|
additionalErrorsFound.fixes
|
|
)
|
|
, firstStepIsComposition = True
|
|
}
|
|
|
|
else
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
in
|
|
case pipingIntoCompositionChecksHelp expressionNode of
|
|
Nothing ->
|
|
[]
|
|
|
|
Just error ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ replacement ++ " instead of " ++ opToFind
|
|
, details =
|
|
[ "Because of the precedence of operators, using " ++ opToFind ++ " at this location is the same as using " ++ replacement ++ "."
|
|
, "Please use " ++ replacement ++ " instead as that is more idiomatic in Elm and generally easier to read."
|
|
]
|
|
}
|
|
error.opToReplaceRange
|
|
error.fixes
|
|
]
|
|
|
|
|
|
collectionFilterChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionFilterChecks collection checkInfo =
|
|
let
|
|
collectionEmptyAsString : String
|
|
collectionEmptyAsString =
|
|
emptyAsString checkInfo collection
|
|
|
|
maybeCollectionArg : Maybe (Node Expression)
|
|
maybeCollectionArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case maybeCollectionArg of
|
|
Just collectionArg ->
|
|
case collection.determineSize checkInfo.lookupTable collectionArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "filter" ) ++ " on " ++ collectionEmptyAsString ++ " will result in " ++ collectionEmptyAsString
|
|
, details = [ "You can replace this call by " ++ collectionEmptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range collectionArg })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case Evaluate.isAlwaysBoolean checkInfo checkInfo.firstArg of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "filter" ) ++ " with a function that will always return True is the same as not using " ++ qualifiedToString ( collection.moduleName, "filter" )
|
|
, details = [ "You can remove this call and replace it by the " ++ collection.represents ++ " itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(toIdentityFix
|
|
{ lastArg = maybeCollectionArg, resources = checkInfo }
|
|
)
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "filter" ) ++ " with a function that will always return False will result in " ++ collectionEmptyAsString
|
|
, details = [ "You can remove this call and replace it by " ++ collectionEmptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix collectionEmptyAsString checkInfo.parentRange maybeCollectionArg checkInfo)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
collectionRemoveChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionRemoveChecks collection checkInfo =
|
|
case secondArg checkInfo of
|
|
Just collectionArg ->
|
|
case collection.determineSize checkInfo.lookupTable collectionArg of
|
|
Just (Exactly 0) ->
|
|
let
|
|
collectionEmptyAsString : String
|
|
collectionEmptyAsString =
|
|
emptyAsString checkInfo collection
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "remove" ) ++ " on " ++ collectionEmptyAsString ++ " will result in " ++ collectionEmptyAsString
|
|
, details = [ "You can replace this call by " ++ collectionEmptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range collectionArg })
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
collectionIntersectChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionIntersectChecks collection checkInfo =
|
|
let
|
|
maybeCollectionArg : Maybe (Node Expression)
|
|
maybeCollectionArg =
|
|
secondArg checkInfo
|
|
|
|
collectionEmptyAsString : String
|
|
collectionEmptyAsString =
|
|
emptyAsString checkInfo collection
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case collection.determineSize checkInfo.lookupTable checkInfo.firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "intersect" ) ++ " on " ++ collectionEmptyAsString ++ " will result in " ++ collectionEmptyAsString
|
|
, details = [ "You can replace this call by " ++ collectionEmptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix collectionEmptyAsString checkInfo.parentRange maybeCollectionArg checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case maybeCollectionArg of
|
|
Just collectionArg ->
|
|
case collection.determineSize checkInfo.lookupTable collectionArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "intersect" ) ++ " on " ++ collectionEmptyAsString ++ " will result in " ++ collectionEmptyAsString
|
|
, details = [ "You can replace this call by " ++ collectionEmptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix collectionEmptyAsString checkInfo.parentRange maybeCollectionArg checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
collectionDiffChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionDiffChecks collection checkInfo =
|
|
let
|
|
maybeCollectionArg : Maybe (Node Expression)
|
|
maybeCollectionArg =
|
|
secondArg checkInfo
|
|
|
|
collectionEmptyAsString : String
|
|
collectionEmptyAsString =
|
|
emptyAsString checkInfo collection
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case collection.determineSize checkInfo.lookupTable checkInfo.firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Diffing " ++ collectionEmptyAsString ++ " will result in " ++ collectionEmptyAsString
|
|
, details = [ "You can replace this call by " ++ collectionEmptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceByEmptyFix collectionEmptyAsString checkInfo.parentRange maybeCollectionArg checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case maybeCollectionArg of
|
|
Just collectionArg ->
|
|
case collection.determineSize checkInfo.lookupTable collectionArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Diffing a " ++ collection.represents ++ " with " ++ collectionEmptyAsString ++ " will result in the " ++ collection.represents ++ " itself"
|
|
, details = [ "You can replace this call by the " ++ collection.represents ++ " itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange { start = checkInfo.parentRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
, Fix.removeRange { start = (Node.range checkInfo.firstArg).end, end = checkInfo.parentRange.end }
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
collectionUnionChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionUnionChecks collection checkInfo =
|
|
let
|
|
maybeCollectionArg : Maybe (Node Expression)
|
|
maybeCollectionArg =
|
|
secondArg checkInfo
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case collection.determineSize checkInfo.lookupTable checkInfo.firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary union with " ++ collection.emptyAsString (extractQualifyResources checkInfo)
|
|
, details = [ "You can replace this call by the " ++ collection.represents ++ " itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(toIdentityFix
|
|
{ lastArg = maybeCollectionArg, resources = checkInfo }
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case maybeCollectionArg of
|
|
Just collectionArg ->
|
|
case collection.determineSize checkInfo.lookupTable collectionArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary union with " ++ collection.emptyAsString (extractQualifyResources checkInfo)
|
|
, details = [ "You can replace this call by the " ++ collection.represents ++ " itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange { start = checkInfo.parentRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
, Fix.removeRange { start = (Node.range checkInfo.firstArg).end, end = checkInfo.parentRange.end }
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
collectionInsertChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionInsertChecks collection checkInfo =
|
|
case secondArg checkInfo of
|
|
Just collectionArg ->
|
|
case collection.determineSize checkInfo.lookupTable collectionArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ qualifiedToString ( collection.moduleName, "singleton" ) ++ " instead of inserting in " ++ emptyAsString checkInfo collection
|
|
, details = [ "You can replace this call by " ++ qualifiedToString ( collection.moduleName, "singleton" ) ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange checkInfo.firstArg
|
|
++ [ Fix.insertAt checkInfo.parentRange.start
|
|
(qualifiedToString (qualify ( collection.moduleName, "singleton" ) checkInfo) ++ " ")
|
|
]
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
collectionMemberChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionMemberChecks collection checkInfo =
|
|
case secondArg checkInfo of
|
|
Just collectionArg ->
|
|
case collection.determineSize checkInfo.lookupTable collectionArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "member" ) ++ " on " ++ collection.emptyDescription ++ " will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "False" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
collectionIsEmptyChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionIsEmptyChecks collection checkInfo =
|
|
case collection.determineSize checkInfo.lookupTable checkInfo.firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( collection.moduleName, "isEmpty" ) ++ " will result in True"
|
|
, details = [ "You can replace this call by True." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "True" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Just _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( collection.moduleName, "isEmpty" ) ++ " will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "False" ) checkInfo))
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
collectionSizeChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionSizeChecks collection checkInfo =
|
|
case collection.determineSize checkInfo.lookupTable checkInfo.firstArg of
|
|
Just (Exactly size) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The " ++ collection.nameForSize ++ " of the " ++ collection.represents ++ " is " ++ String.fromInt size
|
|
, details = [ "The " ++ collection.nameForSize ++ " of the " ++ collection.represents ++ " can be determined by looking at the code." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange (String.fromInt size) ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionFromListChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionFromListChecks collection checkInfo =
|
|
case Node.value checkInfo.firstArg of
|
|
Expression.ListExpr [] ->
|
|
let
|
|
collectionEmptyAsString : String
|
|
collectionEmptyAsString =
|
|
emptyAsString checkInfo collection
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( collection.moduleName, "fromList" ) ++ " will result in " ++ collectionEmptyAsString
|
|
, details = [ "You can replace this call by " ++ collectionEmptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange collectionEmptyAsString ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionToListChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionToListChecks collection checkInfo =
|
|
case collection.determineSize checkInfo.lookupTable checkInfo.firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ qualifiedToString ( collection.moduleName, "toList" ) ++ " will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionPartitionChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionPartitionChecks collection checkInfo =
|
|
let
|
|
collectionEmptyAsString : String
|
|
collectionEmptyAsString =
|
|
emptyAsString checkInfo collection
|
|
in
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case secondArg checkInfo of
|
|
Just collectionArg ->
|
|
case collection.determineSize checkInfo.lookupTable collectionArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( collection.moduleName, "partition" ) ++ " on " ++ collection.emptyDescription ++ " will result in ( " ++ collectionEmptyAsString ++ ", " ++ collectionEmptyAsString ++ " )"
|
|
, details = [ "You can replace this call by ( " ++ collectionEmptyAsString ++ ", " ++ collectionEmptyAsString ++ " )." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange ("( " ++ collectionEmptyAsString ++ ", " ++ collectionEmptyAsString ++ " )") ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
, \() ->
|
|
case Evaluate.isAlwaysBoolean checkInfo checkInfo.firstArg of
|
|
Determined True ->
|
|
case secondArg checkInfo of
|
|
Just (Node listArgRange _) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "All elements will go to the first " ++ collection.represents
|
|
, details = [ "Since the predicate function always returns True, the second " ++ collection.represents ++ " will always be " ++ collection.emptyDescription ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy { start = checkInfo.fnRange.start, end = listArgRange.start } "( "
|
|
, Fix.insertAt listArgRange.end (", " ++ collectionEmptyAsString ++ " )")
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "All elements will go to the second " ++ collection.represents
|
|
, details = [ "Since the predicate function always returns False, the first " ++ collection.represents ++ " will always be " ++ collection.emptyDescription ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(case secondArg checkInfo of
|
|
Just listArg ->
|
|
[ Fix.replaceRangeBy { start = checkInfo.fnRange.start, end = (Node.range listArg).start } ("( " ++ collectionEmptyAsString ++ ", ")
|
|
, Fix.insertAt (Node.range listArg).end " )"
|
|
]
|
|
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy checkInfo.parentRange
|
|
("("
|
|
++ qualifiedToString (qualify ( [ "Tuple" ], "pair" ) checkInfo)
|
|
++ " "
|
|
++ collectionEmptyAsString
|
|
++ ")"
|
|
)
|
|
]
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
maybeWithDefaultChecks : CheckInfo -> List (Error {})
|
|
maybeWithDefaultChecks checkInfo =
|
|
case secondArg checkInfo of
|
|
Just maybeArg ->
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case sameCallInAllBranches ( [ "Maybe" ], "Just" ) checkInfo.lookupTable maybeArg of
|
|
Determined justCalls ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "Maybe" ], "withDefault" ) ++ " on a value that is Just will result in that value"
|
|
, details = [ "You can replace this call by the value wrapped in Just." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(List.concatMap (\justCall -> replaceBySubExpressionFix justCall.nodeRange justCall.firstArg) justCalls
|
|
++ keepOnlyFix { parentRange = checkInfo.parentRange, keep = Node.range maybeArg }
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
, \() ->
|
|
case sameValueOrFunctionInAllBranches ( [ "Maybe" ], "Nothing" ) checkInfo.lookupTable maybeArg of
|
|
Determined _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ qualifiedToString ( [ "Maybe" ], "withDefault" ) ++ " on Nothing will result in the default value"
|
|
, details = [ "You can replace this call by the default value." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.removeRange { start = checkInfo.parentRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
, Fix.removeRange { start = (Node.range checkInfo.firstArg).end, end = checkInfo.parentRange.end }
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
type CollectionSize
|
|
= Exactly Int
|
|
| NotEmpty
|
|
|
|
|
|
determineListLength : ModuleNameLookupTable -> Node Expression -> Maybe CollectionSize
|
|
determineListLength lookupTable expressionNode =
|
|
case Node.value (AstHelpers.removeParens expressionNode) of
|
|
Expression.ListExpr list ->
|
|
Just (Exactly (List.length list))
|
|
|
|
Expression.OperatorApplication "::" _ _ right ->
|
|
case determineListLength lookupTable right of
|
|
Just (Exactly n) ->
|
|
Just (Exactly (n + 1))
|
|
|
|
_ ->
|
|
Just NotEmpty
|
|
|
|
Expression.Application ((Node fnRange (Expression.FunctionOrValue _ "singleton")) :: _ :: []) ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable fnRange == Just [ "List" ] then
|
|
Just (Exactly 1)
|
|
|
|
else
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
replaceSingleElementListBySingleValue : ModuleNameLookupTable -> Node Expression -> Maybe (List Fix)
|
|
replaceSingleElementListBySingleValue lookupTable expressionNode =
|
|
case Node.value (AstHelpers.removeParens expressionNode) of
|
|
Expression.ListExpr (listElement :: []) ->
|
|
Just (replaceBySubExpressionFix (Node.range expressionNode) listElement)
|
|
|
|
Expression.Application ((Node fnRange (Expression.FunctionOrValue _ "singleton")) :: _ :: []) ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable fnRange == Just [ "List" ] then
|
|
Just [ Fix.removeRange fnRange ]
|
|
|
|
else
|
|
Nothing
|
|
|
|
Expression.IfBlock _ thenBranch elseBranch ->
|
|
combineSingleElementFixes lookupTable [ thenBranch, elseBranch ] []
|
|
|
|
Expression.CaseExpression caseOf ->
|
|
combineSingleElementFixes lookupTable (List.map Tuple.second caseOf.cases) []
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
combineSingleElementFixes : ModuleNameLookupTable -> List (Node Expression) -> List Fix -> Maybe (List Fix)
|
|
combineSingleElementFixes lookupTable nodes soFar =
|
|
case nodes of
|
|
[] ->
|
|
Just soFar
|
|
|
|
node :: restOfNodes ->
|
|
case replaceSingleElementListBySingleValue lookupTable node of
|
|
Nothing ->
|
|
Nothing
|
|
|
|
Just fixes ->
|
|
combineSingleElementFixes lookupTable restOfNodes (fixes ++ soFar)
|
|
|
|
|
|
|
|
-- RECORD UPDATE
|
|
|
|
|
|
removeRecordFields : Range -> Node String -> List (Node Expression.RecordSetter) -> List (Error {})
|
|
removeRecordFields recordUpdateRange variable fields =
|
|
case fields of
|
|
[] ->
|
|
-- Not possible
|
|
[]
|
|
|
|
(Node _ ( field, valueWithParens )) :: [] ->
|
|
let
|
|
value : Node Expression
|
|
value =
|
|
AstHelpers.removeParens valueWithParens
|
|
in
|
|
if isUnnecessaryRecordUpdateSetter variable field value then
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary field assignment"
|
|
, details = [ "The field is being set to its own value." ]
|
|
}
|
|
(Node.range value)
|
|
(keepOnlyFix { parentRange = recordUpdateRange, keep = Node.range variable })
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
(Node firstRange _) :: (Node secondRange _) :: _ ->
|
|
withBeforeMap
|
|
(\field ->
|
|
let
|
|
(Node currentFieldRange ( currentFieldName, valueWithParens )) =
|
|
field.current
|
|
|
|
value : Node Expression
|
|
value =
|
|
AstHelpers.removeParens valueWithParens
|
|
in
|
|
if isUnnecessaryRecordUpdateSetter variable currentFieldName value then
|
|
Just
|
|
(Rule.errorWithFix
|
|
{ message = "Unnecessary field assignment"
|
|
, details = [ "The field is being set to its own value." ]
|
|
}
|
|
(Node.range value)
|
|
(case field.before of
|
|
Just (Node prevRange _) ->
|
|
[ Fix.removeRange { start = prevRange.end, end = currentFieldRange.end } ]
|
|
|
|
Nothing ->
|
|
-- It's the first element, so we can remove until the second element
|
|
[ Fix.removeRange { start = firstRange.start, end = secondRange.start } ]
|
|
)
|
|
)
|
|
|
|
else
|
|
Nothing
|
|
)
|
|
fields
|
|
|> List.filterMap identity
|
|
|
|
|
|
isUnnecessaryRecordUpdateSetter : Node String -> Node String -> Node Expression -> Bool
|
|
isUnnecessaryRecordUpdateSetter (Node _ variable) (Node _ field) (Node _ value) =
|
|
case value of
|
|
Expression.RecordAccess (Node _ (Expression.FunctionOrValue [] valueHolder)) (Node _ fieldName) ->
|
|
field == fieldName && variable == valueHolder
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
|
|
-- IF
|
|
|
|
|
|
type alias IfCheckInfo =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, inferredConstants : ( Infer.Inferred, List Infer.Inferred )
|
|
, importLookup : ImportLookup
|
|
, moduleBindings : Set String
|
|
, localBindings : RangeDict (Set String)
|
|
, nodeRange : Range
|
|
, condition : Node Expression
|
|
, trueBranch : Node Expression
|
|
, falseBranch : Node Expression
|
|
}
|
|
|
|
|
|
ifChecks :
|
|
IfCheckInfo
|
|
-> Maybe { errors : List (Error {}), rangesToIgnore : RangeDict () }
|
|
ifChecks checkInfo =
|
|
findMap (\f -> f ())
|
|
[ \() ->
|
|
case Evaluate.getBoolean checkInfo checkInfo.condition of
|
|
Determined determinedConditionResultIsTrue ->
|
|
let
|
|
branch : { expressionNode : Node Expression, name : String }
|
|
branch =
|
|
if determinedConditionResultIsTrue then
|
|
{ expressionNode = checkInfo.trueBranch, name = "then" }
|
|
|
|
else
|
|
{ expressionNode = checkInfo.falseBranch, name = "else" }
|
|
in
|
|
Just
|
|
{ errors =
|
|
[ Rule.errorWithFix
|
|
{ message = "The condition will always evaluate to " ++ AstHelpers.boolToString determinedConditionResultIsTrue
|
|
, details = [ "The expression can be replaced by what is inside the '" ++ branch.name ++ "' branch." ]
|
|
}
|
|
(targetIfKeyword checkInfo.nodeRange)
|
|
(replaceBySubExpressionFix checkInfo.nodeRange branch.expressionNode)
|
|
]
|
|
, rangesToIgnore = RangeDict.singleton (Node.range checkInfo.condition) ()
|
|
}
|
|
|
|
Undetermined ->
|
|
Nothing
|
|
, \() ->
|
|
case ( Evaluate.getBoolean checkInfo checkInfo.trueBranch, Evaluate.getBoolean checkInfo checkInfo.falseBranch ) of
|
|
( Determined True, Determined False ) ->
|
|
Just
|
|
{ errors =
|
|
[ Rule.errorWithFix
|
|
{ message = "The if expression's value is the same as the condition"
|
|
, details = [ "The expression can be replaced by the condition." ]
|
|
}
|
|
(targetIfKeyword checkInfo.nodeRange)
|
|
(replaceBySubExpressionFix checkInfo.nodeRange checkInfo.condition)
|
|
]
|
|
, rangesToIgnore = RangeDict.empty
|
|
}
|
|
|
|
( Determined False, Determined True ) ->
|
|
Just
|
|
{ errors =
|
|
[ Rule.errorWithFix
|
|
{ message = "The if expression's value is the inverse of the condition"
|
|
, details = [ "The expression can be replaced by the condition wrapped by `not`." ]
|
|
}
|
|
(targetIfKeyword checkInfo.nodeRange)
|
|
(replaceBySubExpressionFix checkInfo.nodeRange checkInfo.condition
|
|
++ [ Fix.insertAt checkInfo.nodeRange.start
|
|
(qualifiedToString (qualify ( [ "Basics" ], "not" ) checkInfo) ++ " ")
|
|
]
|
|
)
|
|
]
|
|
, rangesToIgnore = RangeDict.empty
|
|
}
|
|
|
|
_ ->
|
|
Nothing
|
|
, \() ->
|
|
case Normalize.compare checkInfo checkInfo.trueBranch checkInfo.falseBranch of
|
|
Normalize.ConfirmedEquality ->
|
|
Just
|
|
{ errors =
|
|
[ Rule.errorWithFix
|
|
{ message = "The values in both branches is the same."
|
|
, details = [ "The expression can be replaced by the contents of either branch." ]
|
|
}
|
|
(targetIfKeyword checkInfo.nodeRange)
|
|
(replaceBySubExpressionFix checkInfo.nodeRange checkInfo.trueBranch)
|
|
]
|
|
, rangesToIgnore = RangeDict.empty
|
|
}
|
|
|
|
_ ->
|
|
Nothing
|
|
]
|
|
|
|
|
|
|
|
-- CASE OF
|
|
|
|
|
|
caseOfChecks : List (CaseOfCheckInfo -> List (Error {}))
|
|
caseOfChecks =
|
|
[ sameBodyForCaseOfChecks
|
|
, booleanCaseOfChecks
|
|
, destructuringCaseOfChecks
|
|
]
|
|
|
|
|
|
type alias CaseOfCheckInfo =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, customTypesToReportInCases : Set ( ModuleName, ConstructorName )
|
|
, extractSourceCode : Range -> String
|
|
, inferredConstants : ( Infer.Inferred, List Infer.Inferred )
|
|
, parentRange : Range
|
|
, caseOf : Expression.CaseBlock
|
|
}
|
|
|
|
|
|
sameBodyForCaseOfChecks :
|
|
CaseOfCheckInfo
|
|
-> List (Error {})
|
|
sameBodyForCaseOfChecks context =
|
|
case context.caseOf.cases of
|
|
[] ->
|
|
[]
|
|
|
|
( firstPattern, firstBody ) :: rest ->
|
|
let
|
|
restPatterns : List (Node Pattern)
|
|
restPatterns =
|
|
List.map Tuple.first rest
|
|
in
|
|
if
|
|
introducesVariableOrUsesTypeConstructor context (firstPattern :: restPatterns)
|
|
|| not (Normalize.areAllTheSame context firstBody (List.map Tuple.second rest))
|
|
then
|
|
[]
|
|
|
|
else
|
|
let
|
|
firstBodyRange : Range
|
|
firstBodyRange =
|
|
Node.range firstBody
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary case expression"
|
|
, details = [ "All the branches of this case expression resolve to the same value. You can remove the case expression and replace it with the body of one of the branches." ]
|
|
}
|
|
(caseKeyWordRange context.parentRange)
|
|
[ Fix.removeRange { start = context.parentRange.start, end = firstBodyRange.start }
|
|
, Fix.removeRange { start = firstBodyRange.end, end = context.parentRange.end }
|
|
]
|
|
]
|
|
|
|
|
|
caseKeyWordRange : Range -> Range
|
|
caseKeyWordRange range =
|
|
{ start = range.start
|
|
, end = { row = range.start.row, column = range.start.column + 4 }
|
|
}
|
|
|
|
|
|
introducesVariableOrUsesTypeConstructor :
|
|
{ a | lookupTable : ModuleNameLookupTable, customTypesToReportInCases : Set ( ModuleName, ConstructorName ) }
|
|
-> List (Node Pattern)
|
|
-> Bool
|
|
introducesVariableOrUsesTypeConstructor resources nodesToLookAt =
|
|
case nodesToLookAt of
|
|
[] ->
|
|
False
|
|
|
|
node :: remaining ->
|
|
case Node.value node of
|
|
Pattern.VarPattern _ ->
|
|
True
|
|
|
|
Pattern.RecordPattern _ ->
|
|
True
|
|
|
|
Pattern.AsPattern _ _ ->
|
|
True
|
|
|
|
Pattern.ParenthesizedPattern pattern ->
|
|
introducesVariableOrUsesTypeConstructor resources (pattern :: remaining)
|
|
|
|
Pattern.TuplePattern nodes ->
|
|
introducesVariableOrUsesTypeConstructor resources (nodes ++ remaining)
|
|
|
|
Pattern.UnConsPattern first rest ->
|
|
introducesVariableOrUsesTypeConstructor resources (first :: rest :: remaining)
|
|
|
|
Pattern.ListPattern nodes ->
|
|
introducesVariableOrUsesTypeConstructor resources (nodes ++ remaining)
|
|
|
|
Pattern.NamedPattern variantQualified nodes ->
|
|
case ModuleNameLookupTable.fullModuleNameFor resources.lookupTable node of
|
|
Just moduleName ->
|
|
if Set.member ( moduleName, variantQualified.name ) resources.customTypesToReportInCases then
|
|
introducesVariableOrUsesTypeConstructor resources (nodes ++ remaining)
|
|
|
|
else
|
|
True
|
|
|
|
Nothing ->
|
|
True
|
|
|
|
_ ->
|
|
introducesVariableOrUsesTypeConstructor resources remaining
|
|
|
|
|
|
booleanCaseOfChecks : CaseOfCheckInfo -> List (Error {})
|
|
booleanCaseOfChecks checkInfo =
|
|
case checkInfo.caseOf.cases of
|
|
( firstPattern, Node firstRange _ ) :: ( Node secondPatternRange _, Node secondExprRange _ ) :: [] ->
|
|
case AstHelpers.getBoolPattern checkInfo.lookupTable firstPattern of
|
|
Just isTrueFirst ->
|
|
let
|
|
expressionRange : Range
|
|
expressionRange =
|
|
Node.range checkInfo.caseOf.expression
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Replace `case..of` by an `if` condition"
|
|
, details =
|
|
[ "The idiomatic way to check for a condition is to use an `if` expression."
|
|
, "Read more about it at: https://guide.elm-lang.org/core_language.html#if-expressions"
|
|
]
|
|
}
|
|
(Node.range firstPattern)
|
|
(if isTrueFirst then
|
|
[ Fix.replaceRangeBy { start = checkInfo.parentRange.start, end = expressionRange.start } "if "
|
|
, Fix.replaceRangeBy { start = expressionRange.end, end = firstRange.start } " then "
|
|
, Fix.replaceRangeBy { start = secondPatternRange.start, end = secondExprRange.start } "else "
|
|
]
|
|
|
|
else
|
|
[ Fix.replaceRangeBy { start = checkInfo.parentRange.start, end = expressionRange.start } "if not ("
|
|
, Fix.replaceRangeBy { start = expressionRange.end, end = firstRange.start } ") then "
|
|
, Fix.replaceRangeBy { start = secondPatternRange.start, end = secondExprRange.start } "else "
|
|
]
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
destructuringCaseOfChecks :
|
|
CaseOfCheckInfo
|
|
-> List (Error {})
|
|
destructuringCaseOfChecks checkInfo =
|
|
case checkInfo.caseOf.cases of
|
|
( rawSinglePattern, Node bodyRange _ ) :: [] ->
|
|
let
|
|
singlePattern : Node Pattern
|
|
singlePattern =
|
|
AstHelpers.removeParensFromPattern rawSinglePattern
|
|
in
|
|
if isSimpleDestructurePattern singlePattern then
|
|
let
|
|
exprRange : Range
|
|
exprRange =
|
|
Node.range checkInfo.caseOf.expression
|
|
|
|
caseIndentation : String
|
|
caseIndentation =
|
|
String.repeat (checkInfo.parentRange.start.column - 1) " "
|
|
|
|
bodyIndentation : String
|
|
bodyIndentation =
|
|
String.repeat (bodyRange.start.column - 1) " "
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Use a let expression to destructure data"
|
|
, details = [ "It is more idiomatic in Elm to use a let expression to define a new variable rather than to use pattern matching. This will also make the code less indented, therefore easier to read." ]
|
|
}
|
|
(Node.range singlePattern)
|
|
[ Fix.replaceRangeBy { start = checkInfo.parentRange.start, end = exprRange.start }
|
|
("let " ++ checkInfo.extractSourceCode (Node.range singlePattern) ++ " = ")
|
|
, Fix.replaceRangeBy { start = exprRange.end, end = bodyRange.start }
|
|
("\n" ++ caseIndentation ++ "in\n" ++ bodyIndentation)
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
isSimpleDestructurePattern : Node Pattern -> Bool
|
|
isSimpleDestructurePattern (Node _ pattern) =
|
|
case pattern of
|
|
Pattern.TuplePattern _ ->
|
|
True
|
|
|
|
Pattern.RecordPattern _ ->
|
|
True
|
|
|
|
Pattern.VarPattern _ ->
|
|
True
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
|
|
-- NEGATION
|
|
|
|
|
|
negationChecks : { parentRange : Range, negatedExpression : Node Expression } -> List (Error {})
|
|
negationChecks checkInfo =
|
|
case AstHelpers.removeParens checkInfo.negatedExpression of
|
|
Node range (Expression.Negation negatedValue) ->
|
|
let
|
|
doubleNegationRange : Range
|
|
doubleNegationRange =
|
|
{ start = checkInfo.parentRange.start
|
|
, end = { row = range.start.row, column = range.start.column + 1 }
|
|
}
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary double number negation"
|
|
, details = [ "Negating a number twice is the same as the number itself." ]
|
|
}
|
|
doubleNegationRange
|
|
(replaceBySubExpressionFix checkInfo.parentRange negatedValue)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
|
|
-- FULLY APPLIED PREFIX OPERATORS
|
|
|
|
|
|
fullyAppliedPrefixOperatorChecks :
|
|
{ operator : String
|
|
, operatorRange : Range
|
|
, left : Node Expression
|
|
, right : Node Expression
|
|
}
|
|
-> List (Error {})
|
|
fullyAppliedPrefixOperatorChecks checkInfo =
|
|
[ Rule.errorWithFix
|
|
{ message = "Use the infix form (a + b) over the prefix form ((+) a b)"
|
|
, details = [ "The prefix form is generally more unfamiliar to Elm developers, and therefore it is nicer when the infix form is used." ]
|
|
}
|
|
checkInfo.operatorRange
|
|
[ Fix.removeRange { start = checkInfo.operatorRange.start, end = (Node.range checkInfo.left).start }
|
|
, Fix.insertAt (Node.range checkInfo.right).start (checkInfo.operator ++ " ")
|
|
]
|
|
]
|
|
|
|
|
|
|
|
-- APPLIED LAMBDA
|
|
|
|
|
|
appliedLambdaChecks : { nodeRange : Range, lambdaRange : Range, lambda : Expression.Lambda } -> List (Error {})
|
|
appliedLambdaChecks checkInfo =
|
|
case checkInfo.lambda.args of
|
|
(Node unitRange Pattern.UnitPattern) :: otherPatterns ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary unit argument"
|
|
, details =
|
|
[ "This function is expecting a unit, but also passing it directly."
|
|
, "Maybe this was made in attempt to make the computation lazy, but in practice the function will be evaluated eagerly."
|
|
]
|
|
}
|
|
unitRange
|
|
(case otherPatterns of
|
|
[] ->
|
|
replaceBySubExpressionFix checkInfo.nodeRange checkInfo.lambda.expression
|
|
|
|
secondPattern :: _ ->
|
|
Fix.removeRange { start = unitRange.start, end = (Node.range secondPattern).start }
|
|
:: keepOnlyAndParenthesizeFix { parentRange = checkInfo.nodeRange, keep = checkInfo.lambdaRange }
|
|
)
|
|
]
|
|
|
|
(Node allRange Pattern.AllPattern) :: otherPatterns ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary wildcard argument argument"
|
|
, details =
|
|
[ "This function is being passed an argument that is directly ignored."
|
|
, "Maybe this was made in attempt to make the computation lazy, but in practice the function will be evaluated eagerly."
|
|
]
|
|
}
|
|
allRange
|
|
(case otherPatterns of
|
|
[] ->
|
|
replaceBySubExpressionFix checkInfo.nodeRange checkInfo.lambda.expression
|
|
|
|
secondPattern :: _ ->
|
|
Fix.removeRange { start = allRange.start, end = (Node.range secondPattern).start }
|
|
:: keepOnlyAndParenthesizeFix { parentRange = checkInfo.nodeRange, keep = checkInfo.lambdaRange }
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[ Rule.error
|
|
{ message = "Anonymous function is immediately invoked"
|
|
, details =
|
|
[ "This expression defines a function which then gets called directly afterwards, which overly complexifies the intended computation."
|
|
, "While there are reasonable uses for this in languages like JavaScript, the same benefits aren't there in Elm because of not allowing name shadowing."
|
|
, "Here are a few ways you can simplify this:"
|
|
, """- Remove the lambda and reference the arguments directly instead of giving them new names
|
|
- Remove the lambda and use let variables to give names to the current arguments
|
|
- Extract the lambda to a named function (at the top-level or defined in a let expression)"""
|
|
]
|
|
}
|
|
checkInfo.lambdaRange
|
|
]
|
|
|
|
|
|
|
|
-- LET IN
|
|
|
|
|
|
letInChecks : Expression.LetBlock -> List (Error {})
|
|
letInChecks letBlock =
|
|
case Node.value letBlock.expression of
|
|
Expression.LetExpression _ ->
|
|
let
|
|
letRange : Range
|
|
letRange =
|
|
letKeyWordRange (Node.range letBlock.expression)
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Let blocks can be joined together"
|
|
, details = [ "Let blocks can contain multiple declarations, and there is no advantage to having multiple chained let expressions rather than one longer let expression." ]
|
|
}
|
|
letRange
|
|
(case listLast letBlock.declarations of
|
|
Just (Node lastDeclRange _) ->
|
|
[ Fix.replaceRangeBy { start = lastDeclRange.end, end = letRange.end } "\n" ]
|
|
|
|
Nothing ->
|
|
[]
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
letKeyWordRange : Range -> Range
|
|
letKeyWordRange range =
|
|
{ start = range.start
|
|
, end = { row = range.start.row, column = range.start.column + 3 }
|
|
}
|
|
|
|
|
|
|
|
-- RECORD ACCESS
|
|
|
|
|
|
recordAccessChecks : Range -> Maybe Range -> String -> List (Node Expression.RecordSetter) -> List (Error {})
|
|
recordAccessChecks nodeRange recordNameRange fieldName setters =
|
|
case
|
|
findMap
|
|
(\(Node _ ( Node _ setterField, setterValue )) ->
|
|
if setterField == fieldName then
|
|
Just setterValue
|
|
|
|
else
|
|
Nothing
|
|
)
|
|
setters
|
|
of
|
|
Just setter ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Field access can be simplified"
|
|
, details = [ "Accessing the field of a record or record update can be simplified to just that field's value" ]
|
|
}
|
|
nodeRange
|
|
(replaceBySubExpressionFix nodeRange setter)
|
|
]
|
|
|
|
Nothing ->
|
|
case recordNameRange of
|
|
Just rnr ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Field access can be simplified"
|
|
, details = [ "Accessing the field of an unrelated record update can be simplified to just the original field's value" ]
|
|
}
|
|
nodeRange
|
|
[ Fix.replaceRangeBy { start = nodeRange.start, end = rnr.start } ""
|
|
, Fix.replaceRangeBy { start = rnr.end, end = nodeRange.end } ("." ++ fieldName)
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
distributeFieldAccess : String -> Range -> List (Node Expression) -> Node String -> List (Error {})
|
|
distributeFieldAccess kind recordRange branches (Node fieldRange fieldName) =
|
|
case recordLeavesRanges branches of
|
|
Just records ->
|
|
[ let
|
|
fieldAccessRange : Range
|
|
fieldAccessRange =
|
|
{ start = recordRange.end, end = fieldRange.end }
|
|
in
|
|
Rule.errorWithFix
|
|
{ message = "Field access can be simplified"
|
|
, details = [ "Accessing the field outside " ++ kind ++ " expression can be simplified to access the field inside it" ]
|
|
}
|
|
fieldAccessRange
|
|
(Fix.removeRange fieldAccessRange
|
|
:: List.map (\leafRange -> Fix.insertAt leafRange.end ("." ++ fieldName)) records
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
injectRecordAccessIntoLetExpression : Range -> Node Expression -> Node String -> Error {}
|
|
injectRecordAccessIntoLetExpression recordRange letBody (Node fieldRange fieldName) =
|
|
let
|
|
removalRange : Range
|
|
removalRange =
|
|
{ start = recordRange.end, end = fieldRange.end }
|
|
in
|
|
Rule.errorWithFix
|
|
{ message = "Field access can be simplified"
|
|
, details = [ "Accessing the field outside a let/in expression can be simplified to access the field inside it" ]
|
|
}
|
|
removalRange
|
|
(Fix.removeRange removalRange
|
|
:: replaceSubExpressionByRecordAccessFix fieldName letBody
|
|
)
|
|
|
|
|
|
recordLeavesRanges : List (Node Expression) -> Maybe (List Range)
|
|
recordLeavesRanges nodes =
|
|
recordLeavesRangesHelp nodes []
|
|
|
|
|
|
recordLeavesRangesHelp : List (Node Expression) -> List Range -> Maybe (List Range)
|
|
recordLeavesRangesHelp nodes foundRanges =
|
|
case nodes of
|
|
[] ->
|
|
Just foundRanges
|
|
|
|
(Node range expr) :: rest ->
|
|
case expr of
|
|
Expression.IfBlock _ thenBranch elseBranch ->
|
|
recordLeavesRangesHelp (thenBranch :: elseBranch :: rest) foundRanges
|
|
|
|
Expression.LetExpression letIn ->
|
|
recordLeavesRangesHelp (letIn.expression :: rest) foundRanges
|
|
|
|
Expression.ParenthesizedExpression child ->
|
|
recordLeavesRangesHelp (child :: rest) foundRanges
|
|
|
|
Expression.CaseExpression caseOf ->
|
|
recordLeavesRangesHelp (List.map Tuple.second caseOf.cases ++ rest) foundRanges
|
|
|
|
Expression.RecordExpr _ ->
|
|
recordLeavesRangesHelp rest (range :: foundRanges)
|
|
|
|
Expression.RecordUpdateExpression _ _ ->
|
|
recordLeavesRangesHelp rest (range :: foundRanges)
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
|
|
-- FIX HELPERS
|
|
|
|
|
|
parenthesizeIfNeededFix : Node Expression -> List Fix
|
|
parenthesizeIfNeededFix (Node expressionRange expression) =
|
|
if needsParens expression then
|
|
parenthesizeFix expressionRange
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
parenthesizeFix : Range -> List Fix
|
|
parenthesizeFix toSurround =
|
|
[ Fix.insertAt toSurround.start "("
|
|
, Fix.insertAt toSurround.end ")"
|
|
]
|
|
|
|
|
|
keepOnlyFix : { parentRange : Range, keep : Range } -> List Fix
|
|
keepOnlyFix config =
|
|
[ Fix.removeRange
|
|
{ start = config.parentRange.start
|
|
, end = config.keep.start
|
|
}
|
|
, Fix.removeRange
|
|
{ start = config.keep.end
|
|
, end = config.parentRange.end
|
|
}
|
|
]
|
|
|
|
|
|
keepOnlyAndParenthesizeFix : { parentRange : Range, keep : Range } -> List Fix
|
|
keepOnlyAndParenthesizeFix config =
|
|
[ Fix.replaceRangeBy { start = config.parentRange.start, end = config.keep.start } "("
|
|
, Fix.replaceRangeBy { start = config.keep.end, end = config.parentRange.end } ")"
|
|
]
|
|
|
|
|
|
replaceBySubExpressionFix : Range -> Node Expression -> List Fix
|
|
replaceBySubExpressionFix outerRange (Node exprRange exprValue) =
|
|
if needsParens exprValue then
|
|
keepOnlyAndParenthesizeFix { parentRange = outerRange, keep = exprRange }
|
|
|
|
else
|
|
keepOnlyFix { parentRange = outerRange, keep = exprRange }
|
|
|
|
|
|
replaceSubExpressionByRecordAccessFix : String -> Node Expression -> List Fix
|
|
replaceSubExpressionByRecordAccessFix fieldName (Node exprRange exprValue) =
|
|
if needsParens exprValue then
|
|
[ Fix.insertAt exprRange.start "("
|
|
, Fix.insertAt exprRange.end (")." ++ fieldName)
|
|
]
|
|
|
|
else
|
|
[ Fix.insertAt exprRange.end ("." ++ fieldName) ]
|
|
|
|
|
|
rangeBetweenExclusive : ( Range, Range ) -> Range
|
|
rangeBetweenExclusive ( aRange, bRange ) =
|
|
case Range.compareLocations aRange.start bRange.start of
|
|
GT ->
|
|
{ start = bRange.end, end = aRange.start }
|
|
|
|
-- EQ | LT
|
|
_ ->
|
|
{ start = aRange.end, end = bRange.start }
|
|
|
|
|
|
rangeContainsLocation : Location -> Range -> Bool
|
|
rangeContainsLocation location =
|
|
\range ->
|
|
not
|
|
((Range.compareLocations location range.start == LT)
|
|
|| (Range.compareLocations location range.end == GT)
|
|
)
|
|
|
|
|
|
rangeWithoutBoundaries : Range -> Range
|
|
rangeWithoutBoundaries range =
|
|
{ start = startWithoutBoundary range
|
|
, end = endWithoutBoundary range
|
|
}
|
|
|
|
|
|
startWithoutBoundary : Range -> Location
|
|
startWithoutBoundary range =
|
|
{ row = range.start.row, column = range.start.column + 1 }
|
|
|
|
|
|
endWithoutBoundary : Range -> Location
|
|
endWithoutBoundary range =
|
|
{ row = range.end.row, column = range.end.column - 1 }
|
|
|
|
|
|
removeBoundariesFix : Node a -> List Fix
|
|
removeBoundariesFix (Node nodeRange _) =
|
|
[ Fix.removeRange (leftBoundaryRange nodeRange)
|
|
, Fix.removeRange (rightBoundaryRange nodeRange)
|
|
]
|
|
|
|
|
|
leftBoundaryRange : Range -> Range
|
|
leftBoundaryRange range =
|
|
{ start = range.start
|
|
, end = { row = range.start.row, column = range.start.column + 1 }
|
|
}
|
|
|
|
|
|
rightBoundaryRange : Range -> Range
|
|
rightBoundaryRange range =
|
|
{ start = { row = range.end.row, column = range.end.column - 1 }
|
|
, end = range.end
|
|
}
|
|
|
|
|
|
replaceByEmptyFix : String -> Range -> Maybe a -> QualifyResources b -> List Fix
|
|
replaceByEmptyFix empty parentRange lastArg qualifyResources =
|
|
[ case lastArg of
|
|
Just _ ->
|
|
Fix.replaceRangeBy parentRange empty
|
|
|
|
Nothing ->
|
|
Fix.replaceRangeBy parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "always" ) qualifyResources)
|
|
++ " "
|
|
++ empty
|
|
)
|
|
]
|
|
|
|
|
|
replaceByBoolWithIrrelevantLastArgFix :
|
|
{ replacement : Bool, lastArg : Maybe a, checkInfo : QualifyResources { b | parentRange : Range } }
|
|
-> List Fix
|
|
replaceByBoolWithIrrelevantLastArgFix config =
|
|
let
|
|
replacementAsString : String
|
|
replacementAsString =
|
|
qualifiedToString (qualify ( [ "Basics" ], AstHelpers.boolToString config.replacement ) config.checkInfo)
|
|
in
|
|
case config.lastArg of
|
|
Just _ ->
|
|
[ Fix.replaceRangeBy config.checkInfo.parentRange replacementAsString ]
|
|
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy config.checkInfo.parentRange
|
|
("("
|
|
++ qualifiedToString (qualify ( [ "Basics" ], "always" ) config.checkInfo)
|
|
++ " "
|
|
++ replacementAsString
|
|
++ ")"
|
|
)
|
|
]
|
|
|
|
|
|
replacementWithIrrelevantLastArg : { resources : QualifyResources a, lastArg : Maybe arg, forNoLastArg : String } -> String
|
|
replacementWithIrrelevantLastArg config =
|
|
case config.lastArg of
|
|
Just _ ->
|
|
config.forNoLastArg
|
|
|
|
Nothing ->
|
|
qualifiedToString (qualify ( [ "Basics" ], "always" ) config.resources)
|
|
++ (" (" ++ config.forNoLastArg ++ ")")
|
|
|
|
|
|
identityError :
|
|
{ toFix : String
|
|
, lastArgName : String
|
|
, lastArg : Maybe (Node lastArgument)
|
|
, resources : QualifyResources { a | fnRange : Range, parentRange : Range }
|
|
}
|
|
-> Error {}
|
|
identityError config =
|
|
Rule.errorWithFix
|
|
{ message = "Using " ++ config.toFix ++ " will always return the same given " ++ config.lastArgName
|
|
, details =
|
|
case config.lastArg of
|
|
Nothing ->
|
|
[ "You can replace this call by identity." ]
|
|
|
|
Just _ ->
|
|
[ "You can replace this call by the " ++ config.lastArgName ++ " itself." ]
|
|
}
|
|
config.resources.fnRange
|
|
(toIdentityFix { lastArg = config.lastArg, resources = config.resources })
|
|
|
|
|
|
toIdentityFix :
|
|
{ lastArg : Maybe (Node lastArgument)
|
|
, resources : QualifyResources { a | parentRange : Range }
|
|
}
|
|
-> List Fix
|
|
toIdentityFix config =
|
|
case config.lastArg of
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy config.resources.parentRange
|
|
(qualifiedToString (qualify ( [ "Basics" ], "identity" ) config.resources))
|
|
]
|
|
|
|
Just (Node lastArgRange _) ->
|
|
keepOnlyFix { parentRange = config.resources.parentRange, keep = lastArgRange }
|
|
|
|
|
|
multiAlways : Int -> String -> QualifyResources a -> String
|
|
multiAlways alwaysCount alwaysResultExpressionAsString qualifyResources =
|
|
case alwaysCount of
|
|
0 ->
|
|
alwaysResultExpressionAsString
|
|
|
|
1 ->
|
|
qualifiedToString (qualify ( [ "Basics" ], "always" ) qualifyResources)
|
|
++ " "
|
|
++ alwaysResultExpressionAsString
|
|
|
|
alwaysCountPositive ->
|
|
"(\\" ++ String.repeat alwaysCountPositive "_ " ++ "-> " ++ alwaysResultExpressionAsString ++ ")"
|
|
|
|
|
|
{-| Use in combination with
|
|
`findMapNeighboring` where finding returns a record containing the element's Range
|
|
Works for patterns and expressions.
|
|
-}
|
|
listLiteralElementRemoveFix : { before : Maybe (Node element), found : { found | range : Range }, after : Maybe (Node element) } -> List Fix
|
|
listLiteralElementRemoveFix toRemove =
|
|
case ( toRemove.before, toRemove.after ) of
|
|
-- found the only element
|
|
( Nothing, Nothing ) ->
|
|
[ Fix.removeRange toRemove.found.range ]
|
|
|
|
-- found first element
|
|
( Nothing, Just (Node afterRange _) ) ->
|
|
[ Fix.removeRange
|
|
{ start = toRemove.found.range.start
|
|
, end = afterRange.start
|
|
}
|
|
]
|
|
|
|
-- found after first element
|
|
( Just (Node beforeRange _), _ ) ->
|
|
[ Fix.removeRange
|
|
{ start = beforeRange.end
|
|
, end = toRemove.found.range.end
|
|
}
|
|
]
|
|
|
|
|
|
{-| Use in combination with
|
|
`findMapNeighboring` where finding returns a record containing the element's Range
|
|
Works for patterns and expressions.
|
|
-}
|
|
collapsedConsRemoveElementFix :
|
|
{ toRemove : { before : Maybe (Node element), after : Maybe (Node element), found : { found | range : Range } }
|
|
, tailRange : Range
|
|
}
|
|
-> List Fix
|
|
collapsedConsRemoveElementFix config =
|
|
case ( config.toRemove.before, config.toRemove.after ) of
|
|
-- found the only consed element
|
|
( Nothing, Nothing ) ->
|
|
[ Fix.removeRange
|
|
{ start = config.toRemove.found.range.start, end = config.tailRange.start }
|
|
]
|
|
|
|
-- found first consed element
|
|
( Nothing, Just (Node afterRange _) ) ->
|
|
[ Fix.removeRange
|
|
{ start = config.toRemove.found.range.start
|
|
, end = afterRange.start
|
|
}
|
|
]
|
|
|
|
-- found after first consed element
|
|
( Just (Node beforeRange _), _ ) ->
|
|
[ Fix.removeRange
|
|
{ start = beforeRange.end
|
|
, end = config.toRemove.found.range.end
|
|
}
|
|
]
|
|
|
|
|
|
|
|
-- STRING
|
|
|
|
|
|
wrapInBackticks : String -> String
|
|
wrapInBackticks s =
|
|
"`" ++ s ++ "`"
|
|
|
|
|
|
|
|
-- MATCHERS AND PARSERS
|
|
|
|
|
|
needsParens : Expression -> Bool
|
|
needsParens expr =
|
|
case expr of
|
|
Expression.Application _ ->
|
|
True
|
|
|
|
Expression.OperatorApplication _ _ _ _ ->
|
|
True
|
|
|
|
Expression.IfBlock _ _ _ ->
|
|
True
|
|
|
|
Expression.Negation _ ->
|
|
True
|
|
|
|
Expression.LetExpression _ ->
|
|
True
|
|
|
|
Expression.CaseExpression _ ->
|
|
True
|
|
|
|
Expression.LambdaExpression _ ->
|
|
True
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
returnsSpecificValueOrFunctionInAllBranches : ( ModuleName, String ) -> ModuleNameLookupTable -> Node Expression -> Match (List Range)
|
|
returnsSpecificValueOrFunctionInAllBranches specificQualified lookupTable expressionNode =
|
|
constructs (sameValueOrFunctionInAllBranches specificQualified) lookupTable expressionNode
|
|
|
|
|
|
constructsSpecificInAllBranches : ( ModuleName, String ) -> ModuleNameLookupTable -> Node Expression -> Match ConstructionKind
|
|
constructsSpecificInAllBranches specificFullyQualifiedFn lookupTable expressionNode =
|
|
case AstHelpers.getSpecificValueOrFunction specificFullyQualifiedFn lookupTable expressionNode of
|
|
Just _ ->
|
|
Determined DirectConstruction
|
|
|
|
Nothing ->
|
|
constructs (sameCallInAllBranches specificFullyQualifiedFn) lookupTable expressionNode
|
|
|> Match.map
|
|
(\calls ->
|
|
NonDirectConstruction
|
|
(List.concatMap (\call -> replaceBySubExpressionFix call.nodeRange call.firstArg) calls)
|
|
)
|
|
|
|
|
|
type ConstructionKind
|
|
= -- either
|
|
-- - `always specific`
|
|
-- - `\a -> ... (specific a)`
|
|
-- - `... specific`
|
|
DirectConstruction
|
|
| -- `a` argument not directly used,
|
|
-- e.g. `\a -> ... (specific (f a))` or `\a -> if a then specific b`
|
|
NonDirectConstruction (List Fix)
|
|
|
|
|
|
constructs :
|
|
(ModuleNameLookupTable -> Node Expression -> Match specific)
|
|
-> ModuleNameLookupTable
|
|
-> Node Expression
|
|
-> Match specific
|
|
constructs getSpecific lookupTable expressionNode =
|
|
case AstHelpers.getSpecificFunctionCall ( [ "Basics" ], "always" ) lookupTable expressionNode of
|
|
Just alwaysCall ->
|
|
getSpecific lookupTable alwaysCall.firstArg
|
|
|
|
Nothing ->
|
|
case Node.value (AstHelpers.removeParens expressionNode) of
|
|
Expression.LambdaExpression lambda ->
|
|
getSpecific lookupTable lambda.expression
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
|
|
sameCallInAllBranches :
|
|
( ModuleName, String )
|
|
-> ModuleNameLookupTable
|
|
-> Node Expression
|
|
->
|
|
Match
|
|
(List
|
|
{ argsAfterFirst : List (Node Expression)
|
|
, firstArg : Node Expression
|
|
, fnRange : Range
|
|
, nodeRange : Range
|
|
}
|
|
)
|
|
sameCallInAllBranches pureFullyQualified lookupTable baseExpressionNode =
|
|
sameInAllBranches (AstHelpers.getSpecificFunctionCall pureFullyQualified lookupTable) baseExpressionNode
|
|
|
|
|
|
sameValueOrFunctionInAllBranches :
|
|
( ModuleName, String )
|
|
-> ModuleNameLookupTable
|
|
-> Node Expression
|
|
-> Match (List Range)
|
|
sameValueOrFunctionInAllBranches pureFullyQualified lookupTable baseExpressionNode =
|
|
sameInAllBranches (AstHelpers.getSpecificValueOrFunction pureFullyQualified lookupTable) baseExpressionNode
|
|
|
|
|
|
sameInAllBranches :
|
|
(Node Expression -> Maybe info)
|
|
-> Node Expression
|
|
-> Match (List info)
|
|
sameInAllBranches getSpecific baseExpressionNode =
|
|
case getSpecific baseExpressionNode of
|
|
Just specific ->
|
|
Determined [ specific ]
|
|
|
|
Nothing ->
|
|
case Node.value (AstHelpers.removeParens baseExpressionNode) of
|
|
Expression.LetExpression letIn ->
|
|
sameInAllBranches getSpecific letIn.expression
|
|
|
|
Expression.IfBlock _ thenBranch elseBranch ->
|
|
Match.traverse
|
|
(\branchExpression -> sameInAllBranches getSpecific branchExpression)
|
|
[ thenBranch, elseBranch ]
|
|
|> Match.map List.concat
|
|
|
|
Expression.CaseExpression caseOf ->
|
|
Match.traverse
|
|
(\( _, caseExpression ) -> sameInAllBranches getSpecific caseExpression)
|
|
caseOf.cases
|
|
|> Match.map List.concat
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
|
|
getComparableExpressionInTupleFirst : Node Expression -> Maybe (List Expression)
|
|
getComparableExpressionInTupleFirst expressionNode =
|
|
case AstHelpers.getTuple expressionNode of
|
|
Just tuple ->
|
|
getComparableExpression tuple.first
|
|
|
|
Nothing ->
|
|
Nothing
|
|
|
|
|
|
getComparableExpression : Node Expression -> Maybe (List Expression)
|
|
getComparableExpression =
|
|
getComparableExpressionHelper 1
|
|
|
|
|
|
getComparableExpressionHelper : Int -> Node Expression -> Maybe (List Expression)
|
|
getComparableExpressionHelper sign (Node _ expression) =
|
|
case expression of
|
|
Expression.Integer int ->
|
|
Just [ Expression.Integer (sign * int) ]
|
|
|
|
Expression.Hex hex ->
|
|
Just [ Expression.Integer (sign * hex) ]
|
|
|
|
Expression.Floatable float ->
|
|
Just [ Expression.Floatable (toFloat sign * float) ]
|
|
|
|
Expression.Negation expr ->
|
|
getComparableExpressionHelper (-1 * sign) expr
|
|
|
|
Expression.Literal string ->
|
|
Just [ Expression.Literal string ]
|
|
|
|
Expression.CharLiteral char ->
|
|
Just [ Expression.CharLiteral char ]
|
|
|
|
Expression.ParenthesizedExpression expr ->
|
|
getComparableExpressionHelper 1 expr
|
|
|
|
Expression.TupledExpression exprs ->
|
|
exprs
|
|
|> traverse (getComparableExpressionHelper 1)
|
|
|> Maybe.map List.concat
|
|
|
|
Expression.ListExpr exprs ->
|
|
exprs
|
|
|> traverse (getComparableExpressionHelper 1)
|
|
|> Maybe.map List.concat
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
|
|
-- LIST HELPERS
|
|
|
|
|
|
listLast : List a -> Maybe a
|
|
listLast list =
|
|
case list of
|
|
[] ->
|
|
Nothing
|
|
|
|
head :: tail ->
|
|
Just (listFilledLast ( head, tail ))
|
|
|
|
|
|
listFilledLast : ( a, List a ) -> a
|
|
listFilledLast ( head, tail ) =
|
|
case tail of
|
|
[] ->
|
|
head
|
|
|
|
tailHead :: tailTail ->
|
|
listFilledLast ( tailHead, tailTail )
|
|
|
|
|
|
findMap : (a -> Maybe b) -> List a -> Maybe b
|
|
findMap mapper nodes =
|
|
case nodes of
|
|
[] ->
|
|
Nothing
|
|
|
|
node :: rest ->
|
|
case mapper node of
|
|
Just value ->
|
|
Just value
|
|
|
|
Nothing ->
|
|
findMap mapper rest
|
|
|
|
|
|
findMapNeighboringAfter : Maybe a -> (a -> Maybe b) -> List a -> Maybe { before : Maybe a, found : b, after : Maybe a }
|
|
findMapNeighboringAfter before tryMap list =
|
|
case list of
|
|
[] ->
|
|
Nothing
|
|
|
|
now :: after ->
|
|
case tryMap now of
|
|
Just found ->
|
|
Just { before = before, found = found, after = after |> List.head }
|
|
|
|
Nothing ->
|
|
findMapNeighboringAfter (Just now) tryMap after
|
|
|
|
|
|
findMapNeighboring : (a -> Maybe b) -> List a -> Maybe { before : Maybe a, found : b, after : Maybe a }
|
|
findMapNeighboring tryMap list =
|
|
findMapNeighboringAfter Nothing tryMap list
|
|
|
|
|
|
neighboringMap : ({ before : Maybe a, current : a, after : Maybe a } -> b) -> List a -> List b
|
|
neighboringMap changeWithNeighboring list =
|
|
List.map3
|
|
(\before current after ->
|
|
changeWithNeighboring { before = before, current = current, after = after }
|
|
)
|
|
(Nothing :: List.map Just list)
|
|
list
|
|
(List.map Just (List.drop 1 list) ++ [ Nothing ])
|
|
|
|
|
|
withBeforeMap : ({ before : Maybe a, current : a } -> b) -> List a -> List b
|
|
withBeforeMap changeWithBefore list =
|
|
List.map2
|
|
(\before current ->
|
|
changeWithBefore { before = before, current = current }
|
|
)
|
|
(Nothing :: List.map Just list)
|
|
list
|
|
|
|
|
|
traverse : (a -> Maybe b) -> List a -> Maybe (List b)
|
|
traverse f list =
|
|
traverseHelp f list []
|
|
|
|
|
|
traverseHelp : (a -> Maybe b) -> List a -> List b -> Maybe (List b)
|
|
traverseHelp f list acc =
|
|
case list of
|
|
head :: tail ->
|
|
case f head of
|
|
Just a ->
|
|
traverseHelp f tail (a :: acc)
|
|
|
|
Nothing ->
|
|
Nothing
|
|
|
|
[] ->
|
|
Just (List.reverse acc)
|
|
|
|
|
|
unique : List a -> List a
|
|
unique list =
|
|
uniqueHelp [] list []
|
|
|
|
|
|
uniqueHelp : List a -> List a -> List a -> List a
|
|
uniqueHelp existing remaining accumulator =
|
|
case remaining of
|
|
[] ->
|
|
List.reverse accumulator
|
|
|
|
first :: rest ->
|
|
if List.member first existing then
|
|
uniqueHelp existing rest accumulator
|
|
|
|
else
|
|
uniqueHelp (first :: existing) rest (first :: accumulator)
|
|
|
|
|
|
|
|
-- MAYBE HELPERS
|
|
|
|
|
|
isJust : Maybe a -> Bool
|
|
isJust maybe =
|
|
case maybe of
|
|
Just _ ->
|
|
True
|
|
|
|
Nothing ->
|
|
False
|