mirror of
https://github.com/jfmengels/elm-review.git
synced 2024-11-30 03:32:30 +03:00
5680 lines
201 KiB
Elm
5680 lines
201 KiB
Elm
module Simplify exposing
|
|
( rule
|
|
, Configuration, defaults, 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, 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
|
|
|
|
not >> not
|
|
--> identity
|
|
|
|
|
|
### 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) data
|
|
--> (\y -> x)
|
|
|
|
(\\_ y -> x) data
|
|
--> (\y -> x)
|
|
|
|
|
|
### Operators
|
|
|
|
(++) a b
|
|
--> a ++ b
|
|
|
|
|
|
### Numbers
|
|
|
|
n + 0
|
|
--> n
|
|
|
|
n - 0
|
|
--> n
|
|
|
|
0 - n
|
|
--> -n
|
|
|
|
n * 1
|
|
--> n
|
|
|
|
n * 0
|
|
--> 0
|
|
|
|
n / 1
|
|
--> n
|
|
|
|
-(-n)
|
|
--> n
|
|
|
|
negate >> negate
|
|
--> identity
|
|
|
|
negate (negate x)
|
|
--> x
|
|
|
|
|
|
### Strings
|
|
|
|
"a" ++ ""
|
|
--> "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 x
|
|
--> x
|
|
|
|
|
|
### Maybe
|
|
|
|
Maybe.map identity x
|
|
--> x
|
|
|
|
Maybe.map f Nothing
|
|
--> Nothing
|
|
|
|
Maybe.map f (Just x)
|
|
--> Just (f x)
|
|
|
|
MaybeThen 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
|
|
|
|
|
|
### Result
|
|
|
|
Result.map identity x
|
|
--> x.and
|
|
|
|
Result.map f (Err x)
|
|
--> Err x
|
|
|
|
Result.map f (Ok x)
|
|
--> Ok (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
|
|
|
|
|
|
### Lists
|
|
|
|
a :: []
|
|
--> [ a ]
|
|
|
|
a :: [ b ]
|
|
--> [ a, b ]
|
|
|
|
[ a ] ++ list
|
|
--> a :: list
|
|
|
|
[] ++ list
|
|
--> list
|
|
|
|
[ a, b ] ++ [ c ]
|
|
--> [ a, b, c ]
|
|
|
|
[ a, b ] ++ [ c ]
|
|
--> [ a, b, c ]
|
|
|
|
List.map fn [] -- same for most List functions like List.filter, List.filterMap, ...
|
|
--> []
|
|
|
|
List.map identity list
|
|
--> list
|
|
|
|
List.map identity
|
|
--> identity
|
|
|
|
List.filter (always True) list
|
|
--> list
|
|
|
|
List.filter (\a -> True) list
|
|
--> list
|
|
|
|
List.filter (always False) list
|
|
--> []
|
|
|
|
List.filter (always True)
|
|
--> identity
|
|
|
|
List.filter (always False)
|
|
--> always []
|
|
|
|
List.filterMap Just list
|
|
--> list
|
|
|
|
List.filterMap (\a -> Just a) list
|
|
--> list
|
|
|
|
List.filterMap Just
|
|
--> identity
|
|
|
|
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 (always Nothing)
|
|
--> (always [])
|
|
|
|
List.filterMap identity (List.map f x)
|
|
--> List.filterMap f x
|
|
|
|
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.concatMap identity x
|
|
--> List.concat list
|
|
|
|
List.concatMap identity
|
|
--> List.concat
|
|
|
|
List.concatMap (\a -> a) list
|
|
--> List.concat list
|
|
|
|
List.concatMap (\a -> [ b ]) list
|
|
--> List.map (\a -> b) list
|
|
|
|
List.concatMap fn [ x ]
|
|
--> fn x
|
|
|
|
List.concatMap (always []) list
|
|
--> []
|
|
|
|
List.concat (List.map f x)
|
|
--> List.concatMap f x
|
|
|
|
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.all fn []
|
|
--> True
|
|
|
|
List.all (always True) list
|
|
--> True
|
|
|
|
List.any fn []
|
|
--> True
|
|
|
|
List.any (always False) list
|
|
--> True
|
|
|
|
List.range 6 3
|
|
--> []
|
|
|
|
List.length [ a ]
|
|
--> 1
|
|
|
|
List.repeat 0 list
|
|
--> []
|
|
|
|
List.partition fn []
|
|
--> ( [], [] )
|
|
|
|
List.partition (always True) list
|
|
--> ( list, [] )
|
|
|
|
|
|
List.take 0 x
|
|
--> []
|
|
|
|
|
|
List.drop 0 x
|
|
--> x
|
|
|
|
|
|
List.reverse <| List.reverse x
|
|
--> x
|
|
|
|
|
|
### Set
|
|
|
|
Set.map fn Set.empty -- same for Set.filter, Set.remove...
|
|
--> Set.empty
|
|
|
|
Set.map identity set
|
|
--> set
|
|
|
|
Set.map identity
|
|
--> identity
|
|
|
|
Set.isEmpty Set.empty
|
|
--> True
|
|
|
|
Set.member x Set.empty
|
|
--> False
|
|
|
|
Set.fromList []
|
|
--> Set.empty
|
|
|
|
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
|
|
|
|
Set.partition fn 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
|
|
|
|
|
|
### 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 fn Cmd.none
|
|
--> Cmd.none
|
|
|
|
|
|
### Json.Decode
|
|
|
|
Json.Decode.oneOf [a]
|
|
--> a
|
|
|
|
|
|
### Parser
|
|
|
|
Parser.oneOf [a]
|
|
--> a
|
|
|
|
-}
|
|
|
|
import Dict exposing (Dict)
|
|
import Elm.Docs
|
|
import Elm.Syntax.Expression as Expression exposing (Expression, RecordSetter)
|
|
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
|
|
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
|
|
|> Rule.withModuleContextUsingContextCreator
|
|
{ fromProjectToModule = fromProjectToModule
|
|
, fromModuleToProject = fromModuleToProject
|
|
, foldProjectContexts = \_ previous -> previous
|
|
}
|
|
|> Rule.providesFixesForProjectRule
|
|
|> Rule.fromProjectRuleSchema
|
|
|
|
|
|
moduleVisitor : Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
|
|
moduleVisitor schema =
|
|
schema
|
|
|> Rule.withDeclarationEnterVisitor (\node context -> ( [], declarationVisitor node context ))
|
|
|> Rule.withExpressionEnterVisitor expressionVisitor
|
|
|> 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
|
|
}
|
|
|
|
|
|
{-| Default configuration for this rule. Use [`ignoreCaseOfForTypes`](#ignoreCaseOfForTypes) if you want to change the configuration.
|
|
|
|
config =
|
|
[ Simplify.defaults
|
|
|> Simplify.ignoreCaseOfForTypes [ "Module.Name.Type" ]
|
|
|> Simplify.rule
|
|
]
|
|
|
|
-}
|
|
defaults : Configuration
|
|
defaults =
|
|
Configuration { ignoreConstructors = [] }
|
|
|
|
|
|
{-| 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 { config | ignoreConstructors = ignoreConstructors ++ config.ignoreConstructors }
|
|
|
|
|
|
|
|
-- CONTEXT
|
|
|
|
|
|
type alias ProjectContext =
|
|
{ customTypesToReportInCases : Set ( ModuleName, ConstructorName )
|
|
}
|
|
|
|
|
|
type alias ModuleContext =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, moduleName : ModuleName
|
|
, rangesToIgnore : List Range
|
|
, rightSidesOfPlusPlus : List Range
|
|
, customTypesToReportInCases : Set ( ModuleName, ConstructorName )
|
|
, localIgnoredCustomTypes : List Constructor
|
|
, constructorsToIgnore : Set ( ModuleName, String )
|
|
, inferredConstantsDict : RangeDict Infer.Inferred
|
|
, inferredConstants : ( Infer.Inferred, List Infer.Inferred )
|
|
, extractSourceCode : Range -> String
|
|
}
|
|
|
|
|
|
type alias ConstructorName =
|
|
String
|
|
|
|
|
|
type alias Constructor =
|
|
{ moduleName : ModuleName
|
|
, name : String
|
|
, constructors : List String
|
|
}
|
|
|
|
|
|
initialContext : ProjectContext
|
|
initialContext =
|
|
{ customTypesToReportInCases = Set.empty
|
|
}
|
|
|
|
|
|
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
|
|
fromModuleToProject =
|
|
Rule.initContextCreator (\_ -> initialContext)
|
|
|
|
|
|
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
|
|
fromProjectToModule =
|
|
Rule.initContextCreator
|
|
(\lookupTable metadata extractSourceCode projectContext ->
|
|
{ lookupTable = lookupTable
|
|
, moduleName = Rule.moduleNameFromMetadata metadata
|
|
, rangesToIgnore = []
|
|
, rightSidesOfPlusPlus = []
|
|
, localIgnoredCustomTypes = []
|
|
, customTypesToReportInCases = projectContext.customTypesToReportInCases
|
|
, constructorsToIgnore = Set.empty
|
|
, inferredConstantsDict = RangeDict.empty
|
|
, inferredConstants = ( Infer.empty, [] )
|
|
, extractSourceCode = extractSourceCode
|
|
}
|
|
)
|
|
|> Rule.withModuleNameLookupTable
|
|
|> Rule.withMetadata
|
|
|> Rule.withSourceCodeExtractor
|
|
|
|
|
|
|
|
-- DEPENDENCIES VISITOR
|
|
|
|
|
|
dependenciesVisitor : Set String -> Dict String Dependency -> ProjectContext -> ( List (Error scope), ProjectContext )
|
|
dependenciesVisitor typeNamesAsStrings dict _ =
|
|
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 =
|
|
String.split "." 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
|
|
in
|
|
( if List.isEmpty unknownTypesToIgnore then
|
|
[]
|
|
|
|
else
|
|
[ errorForUnknownIgnoredConstructor unknownTypesToIgnore ]
|
|
, { customTypesToReportInCases = customTypesToReportInCases }
|
|
)
|
|
|
|
|
|
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."
|
|
]
|
|
}
|
|
|
|
|
|
wrapInBackticks : String -> String
|
|
wrapInBackticks s =
|
|
"`" ++ s ++ "`"
|
|
|
|
|
|
|
|
-- DECLARATION VISITOR
|
|
|
|
|
|
declarationVisitor : Node a -> ModuleContext -> ModuleContext
|
|
declarationVisitor _ context =
|
|
{ context
|
|
| rangesToIgnore = []
|
|
, rightSidesOfPlusPlus = []
|
|
, inferredConstantsDict = RangeDict.empty
|
|
}
|
|
|
|
|
|
|
|
-- EXPRESSION VISITOR
|
|
|
|
|
|
expressionVisitor : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
|
|
expressionVisitor node context =
|
|
let
|
|
newContext : ModuleContext
|
|
newContext =
|
|
case RangeDict.get (Node.range node) context.inferredConstantsDict of
|
|
Just inferredConstants ->
|
|
let
|
|
( previous, previousStack ) =
|
|
context.inferredConstants
|
|
in
|
|
{ context | inferredConstants = ( inferredConstants, previous :: previousStack ) }
|
|
|
|
Nothing ->
|
|
context
|
|
in
|
|
if List.member (Node.range node) newContext.rangesToIgnore then
|
|
( [], newContext )
|
|
|
|
else
|
|
let
|
|
{ errors, rangesToIgnore, rightSidesOfPlusPlus, inferredConstants } =
|
|
expressionVisitorHelp node newContext
|
|
in
|
|
( errors
|
|
, { newContext
|
|
| rangesToIgnore = rangesToIgnore ++ newContext.rangesToIgnore
|
|
, rightSidesOfPlusPlus = rightSidesOfPlusPlus ++ newContext.rightSidesOfPlusPlus
|
|
, inferredConstantsDict = List.foldl (\( range, constants ) acc -> RangeDict.insert range constants acc) newContext.inferredConstantsDict inferredConstants
|
|
}
|
|
)
|
|
|
|
|
|
expressionExitVisitor : Node Expression -> ModuleContext -> ModuleContext
|
|
expressionExitVisitor node context =
|
|
if RangeDict.member (Node.range node) context.inferredConstantsDict then
|
|
case Tuple.second context.inferredConstants of
|
|
topOfStack :: restOfStack ->
|
|
{ context | inferredConstants = ( topOfStack, restOfStack ) }
|
|
|
|
[] ->
|
|
-- should never be empty
|
|
context
|
|
|
|
else
|
|
context
|
|
|
|
|
|
errorsAndRangesToIgnore : List (Error {}) -> List Range -> { errors : List (Error {}), rangesToIgnore : List Range, rightSidesOfPlusPlus : List Range, inferredConstants : List ( Range, Infer.Inferred ) }
|
|
errorsAndRangesToIgnore errors rangesToIgnore =
|
|
{ errors = errors
|
|
, rangesToIgnore = rangesToIgnore
|
|
, rightSidesOfPlusPlus = []
|
|
, inferredConstants = []
|
|
}
|
|
|
|
|
|
onlyErrors : List (Error {}) -> { errors : List (Error {}), rangesToIgnore : List Range, rightSidesOfPlusPlus : List Range, inferredConstants : List ( Range, Infer.Inferred ) }
|
|
onlyErrors errors =
|
|
{ errors = errors
|
|
, rangesToIgnore = []
|
|
, rightSidesOfPlusPlus = []
|
|
, inferredConstants = []
|
|
}
|
|
|
|
|
|
expressionVisitorHelp : Node Expression -> ModuleContext -> { errors : List (Error {}), rangesToIgnore : List Range, rightSidesOfPlusPlus : List Range, inferredConstants : List ( Range, Infer.Inferred ) }
|
|
expressionVisitorHelp node context =
|
|
case Node.value node of
|
|
--------------------
|
|
-- FUNCTION CALLS --
|
|
--------------------
|
|
Expression.Application ((Node fnRange (Expression.FunctionOrValue _ fnName)) :: firstArg :: restOfArguments) ->
|
|
case
|
|
ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange
|
|
|> Maybe.andThen (\moduleName -> Dict.get ( moduleName, fnName ) functionCallChecks)
|
|
of
|
|
Just checkFn ->
|
|
onlyErrors
|
|
(checkFn
|
|
{ lookupTable = context.lookupTable
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = Node.range node
|
|
, fnRange = fnRange
|
|
, firstArg = firstArg
|
|
, secondArg = List.head restOfArguments
|
|
, thirdArg = List.head (List.drop 1 restOfArguments)
|
|
, usingRightPizza = False
|
|
}
|
|
)
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
-------------------
|
|
-- IF EXPRESSION --
|
|
-------------------
|
|
Expression.IfBlock condition trueBranch falseBranch ->
|
|
ifChecks
|
|
context
|
|
(Node.range node)
|
|
{ condition = condition
|
|
, trueBranch = trueBranch
|
|
, falseBranch = falseBranch
|
|
}
|
|
|
|
-------------------------------
|
|
-- APPLIED LAMBDA FUNCTIONS --
|
|
-------------------------------
|
|
Expression.Application ((Node _ (Expression.ParenthesizedExpression (Node lambdaRange (Expression.LambdaExpression lambda)))) :: firstArgument :: _) ->
|
|
case lambda.args of
|
|
(Node unitRange Pattern.UnitPattern) :: otherPatterns ->
|
|
onlyErrors
|
|
[ 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
|
|
[] ->
|
|
[ Fix.removeRange { start = lambdaRange.start, end = (Node.range lambda.expression).start }
|
|
, Fix.removeRange (Node.range firstArgument)
|
|
]
|
|
|
|
secondPattern :: _ ->
|
|
[ Fix.removeRange { start = unitRange.start, end = (Node.range secondPattern).start }
|
|
, Fix.removeRange (Node.range firstArgument)
|
|
]
|
|
)
|
|
]
|
|
|
|
(Node allRange Pattern.AllPattern) :: otherPatterns ->
|
|
onlyErrors
|
|
[ 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
|
|
[] ->
|
|
[ Fix.removeRange { start = lambdaRange.start, end = (Node.range lambda.expression).start }
|
|
, Fix.removeRange (Node.range firstArgument)
|
|
]
|
|
|
|
secondPattern :: _ ->
|
|
[ Fix.removeRange { start = allRange.start, end = (Node.range secondPattern).start }
|
|
, Fix.removeRange (Node.range firstArgument)
|
|
]
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
-----------------------------------
|
|
-- FULLY APPLIED PREFIX OPERATOR --
|
|
-----------------------------------
|
|
Expression.Application [ Node.Node operatorRange (Expression.PrefixOperator operator), left, right ] ->
|
|
onlyErrors
|
|
[ 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." ]
|
|
}
|
|
operatorRange
|
|
[ Fix.removeRange { start = operatorRange.start, end = (Node.range left).start }
|
|
, Fix.insertAt (Node.range right).start (operator ++ " ")
|
|
]
|
|
]
|
|
|
|
-------------------
|
|
-- RECORD UPDATE --
|
|
-------------------
|
|
Expression.RecordUpdateExpression variable fields ->
|
|
onlyErrors (removeRecordFields (Node.range node) variable fields)
|
|
|
|
-------------
|
|
-- CASE OF --
|
|
-------------
|
|
Expression.CaseExpression caseBlock ->
|
|
onlyErrors (caseOfChecks context (Node.range node) caseBlock)
|
|
|
|
------------
|
|
-- LET IN --
|
|
------------
|
|
Expression.LetExpression caseBlock ->
|
|
onlyErrors (letInChecks caseBlock)
|
|
|
|
----------
|
|
-- (<|) --
|
|
----------
|
|
Expression.OperatorApplication "<|" _ (Node fnRange (Expression.FunctionOrValue _ fnName)) firstArg ->
|
|
case
|
|
ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange
|
|
|> Maybe.andThen (\moduleName -> Dict.get ( moduleName, fnName ) functionCallChecks)
|
|
of
|
|
Just checkFn ->
|
|
onlyErrors
|
|
(checkFn
|
|
{ lookupTable = context.lookupTable
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = Node.range node
|
|
, fnRange = fnRange
|
|
, firstArg = firstArg
|
|
, secondArg = Nothing
|
|
, thirdArg = Nothing
|
|
, usingRightPizza = False
|
|
}
|
|
)
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
Expression.OperatorApplication "<|" _ (Node applicationRange (Expression.Application ((Node fnRange (Expression.FunctionOrValue _ fnName)) :: firstArg :: []))) secondArgument ->
|
|
case
|
|
ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange
|
|
|> Maybe.andThen (\moduleName -> Dict.get ( moduleName, fnName ) functionCallChecks)
|
|
of
|
|
Just checkFn ->
|
|
errorsAndRangesToIgnore
|
|
(checkFn
|
|
{ lookupTable = context.lookupTable
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = Node.range node
|
|
, fnRange = fnRange
|
|
, firstArg = firstArg
|
|
, secondArg = Just secondArgument
|
|
, thirdArg = Nothing
|
|
, usingRightPizza = False
|
|
}
|
|
)
|
|
[ applicationRange ]
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
----------
|
|
-- (|>) --
|
|
----------
|
|
Expression.OperatorApplication "|>" _ firstArg (Node fnRange (Expression.FunctionOrValue _ fnName)) ->
|
|
case
|
|
ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange
|
|
|> Maybe.andThen (\moduleName -> Dict.get ( moduleName, fnName ) functionCallChecks)
|
|
of
|
|
Just checkFn ->
|
|
onlyErrors
|
|
(checkFn
|
|
{ lookupTable = context.lookupTable
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = Node.range node
|
|
, fnRange = fnRange
|
|
, firstArg = firstArg
|
|
, secondArg = Nothing
|
|
, thirdArg = Nothing
|
|
, usingRightPizza = True
|
|
}
|
|
)
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
Expression.OperatorApplication "|>" _ secondArgument (Node applicationRange (Expression.Application ((Node fnRange (Expression.FunctionOrValue _ fnName)) :: firstArg :: []))) ->
|
|
case
|
|
ModuleNameLookupTable.moduleNameAt context.lookupTable fnRange
|
|
|> Maybe.andThen (\moduleName -> Dict.get ( moduleName, fnName ) functionCallChecks)
|
|
of
|
|
Just checkFn ->
|
|
errorsAndRangesToIgnore
|
|
(checkFn
|
|
{ lookupTable = context.lookupTable
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = Node.range node
|
|
, fnRange = fnRange
|
|
, firstArg = firstArg
|
|
, secondArg = Just secondArgument
|
|
, thirdArg = Nothing
|
|
, usingRightPizza = True
|
|
}
|
|
)
|
|
[ applicationRange ]
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
Expression.OperatorApplication ">>" _ left (Node _ (Expression.OperatorApplication ">>" _ right _)) ->
|
|
onlyErrors
|
|
(firstThatReportsError compositionChecks
|
|
{ lookupTable = context.lookupTable
|
|
, fromLeftToRight = True
|
|
, parentRange = { start = (Node.range left).start, end = (Node.range right).end }
|
|
, left = left
|
|
, leftRange = Node.range left
|
|
, right = right
|
|
, rightRange = Node.range right
|
|
}
|
|
)
|
|
|
|
Expression.OperatorApplication ">>" _ left right ->
|
|
onlyErrors
|
|
(firstThatReportsError compositionChecks
|
|
{ lookupTable = context.lookupTable
|
|
, fromLeftToRight = True
|
|
, parentRange = Node.range node
|
|
, left = left
|
|
, leftRange = Node.range left
|
|
, right = right
|
|
, rightRange = Node.range right
|
|
}
|
|
)
|
|
|
|
Expression.OperatorApplication "<<" _ (Node _ (Expression.OperatorApplication "<<" _ _ left)) right ->
|
|
onlyErrors
|
|
(firstThatReportsError compositionChecks
|
|
{ lookupTable = context.lookupTable
|
|
, fromLeftToRight = False
|
|
, parentRange = { start = (Node.range left).start, end = (Node.range right).end }
|
|
, left = left
|
|
, leftRange = Node.range left
|
|
, right = right
|
|
, rightRange = Node.range right
|
|
}
|
|
)
|
|
|
|
Expression.OperatorApplication "<<" _ left right ->
|
|
onlyErrors
|
|
(firstThatReportsError compositionChecks
|
|
{ lookupTable = context.lookupTable
|
|
, fromLeftToRight = False
|
|
, parentRange = Node.range node
|
|
, left = left
|
|
, leftRange = Node.range left
|
|
, right = right
|
|
, rightRange = Node.range right
|
|
}
|
|
)
|
|
|
|
Expression.OperatorApplication operator _ left right ->
|
|
case Dict.get operator operatorChecks of
|
|
Just checkFn ->
|
|
{ errors =
|
|
checkFn
|
|
{ lookupTable = context.lookupTable
|
|
, inferredConstants = context.inferredConstants
|
|
, parentRange = Node.range node
|
|
, operator = operator
|
|
, left = left
|
|
, leftRange = Node.range left
|
|
, right = right
|
|
, rightRange = Node.range right
|
|
, isOnTheRightSideOfPlusPlus = List.member (Node.range node) context.rightSidesOfPlusPlus
|
|
}
|
|
, rangesToIgnore = []
|
|
, rightSidesOfPlusPlus =
|
|
if operator == "++" then
|
|
[ Node.range <| AstHelpers.removeParens right ]
|
|
|
|
else
|
|
[]
|
|
, inferredConstants = []
|
|
}
|
|
|
|
Nothing ->
|
|
onlyErrors []
|
|
|
|
Expression.Negation baseExpr ->
|
|
case AstHelpers.removeParens baseExpr of
|
|
Node range (Expression.Negation negatedValue) ->
|
|
let
|
|
doubleNegationRange : Range
|
|
doubleNegationRange =
|
|
{ start = (Node.range node).start
|
|
, end = { row = range.start.row, column = range.start.column + 1 }
|
|
}
|
|
in
|
|
onlyErrors
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary double number negation"
|
|
, details = [ "Negating a number twice is the same as the number itself." ]
|
|
}
|
|
doubleNegationRange
|
|
(replaceBySubExpressionFix (Node.range node) negatedValue)
|
|
]
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
Expression.RecordAccess record field ->
|
|
case Node.value (AstHelpers.removeParens record) of
|
|
Expression.RecordExpr setters ->
|
|
onlyErrors (recordAccessChecks (Node.range node) Nothing (Node.value field) setters)
|
|
|
|
Expression.RecordUpdateExpression (Node recordNameRange _) setters ->
|
|
onlyErrors (recordAccessChecks (Node.range node) (Just recordNameRange) (Node.value field) setters)
|
|
|
|
Expression.LetExpression { expression } ->
|
|
onlyErrors [ injectRecordAccessIntoLetExpression (Node.range record) expression field ]
|
|
|
|
Expression.IfBlock _ thenBranch elseBranch ->
|
|
onlyErrors (distributeFieldAccess "an if/then/else" (Node.range record) [ thenBranch, elseBranch ] field)
|
|
|
|
Expression.CaseExpression { cases } ->
|
|
onlyErrors (distributeFieldAccess "a case/of" (Node.range record) (List.map Tuple.second cases) field)
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
_ ->
|
|
onlyErrors []
|
|
|
|
|
|
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 -> Rule.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 { expression } ->
|
|
recordLeavesRangesHelp (expression :: rest) foundRanges
|
|
|
|
Expression.ParenthesizedExpression child ->
|
|
recordLeavesRangesHelp (child :: rest) foundRanges
|
|
|
|
Expression.CaseExpression { cases } ->
|
|
recordLeavesRangesHelp (List.map Tuple.second cases ++ rest) foundRanges
|
|
|
|
Expression.RecordExpr _ ->
|
|
recordLeavesRangesHelp rest (range :: foundRanges)
|
|
|
|
Expression.RecordUpdateExpression _ _ ->
|
|
recordLeavesRangesHelp rest (range :: foundRanges)
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
recordAccessChecks : Range -> Maybe Range -> String -> List (Node RecordSetter) -> List (Error {})
|
|
recordAccessChecks nodeRange recordNameRange fieldName setters =
|
|
case
|
|
findMap
|
|
(\(Node _ ( setterField, setterValue )) ->
|
|
if Node.value 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 ->
|
|
[]
|
|
|
|
|
|
replaceBySubExpressionFix : Range -> Node Expression -> List Fix
|
|
replaceBySubExpressionFix outerRange (Node exprRange exprValue) =
|
|
if needsParens exprValue then
|
|
[ Fix.replaceRangeBy { start = outerRange.start, end = exprRange.start } "("
|
|
, Fix.replaceRangeBy { start = exprRange.end, end = outerRange.end } ")"
|
|
]
|
|
|
|
else
|
|
[ Fix.removeRange { start = outerRange.start, end = exprRange.start }
|
|
, Fix.removeRange { start = exprRange.end, end = outerRange.end }
|
|
]
|
|
|
|
|
|
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) ]
|
|
|
|
|
|
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
|
|
|
|
|
|
type alias CheckInfo =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, inferredConstants : ( Infer.Inferred, List Infer.Inferred )
|
|
, parentRange : Range
|
|
, fnRange : Range
|
|
, firstArg : Node Expression
|
|
, secondArg : Maybe (Node Expression)
|
|
, thirdArg : Maybe (Node Expression)
|
|
, usingRightPizza : Bool
|
|
}
|
|
|
|
|
|
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" ], "andThen" ), resultAndThenChecks )
|
|
, ( ( [ "Result" ], "withDefault" ), resultWithDefaultChecks )
|
|
, ( ( [ "List" ], "map" ), collectionMapChecks listCollection )
|
|
, ( ( [ "List" ], "filter" ), collectionFilterChecks listCollection )
|
|
, reportEmptyListSecondArgument ( ( [ "List" ], "filterMap" ), listFilterMapChecks )
|
|
, reportEmptyListFirstArgument ( ( [ "List" ], "concat" ), listConcatChecks )
|
|
, reportEmptyListSecondArgument ( ( [ "List" ], "concatMap" ), listConcatMapChecks )
|
|
, reportEmptyListSecondArgument ( ( [ "List" ], "indexedMap" ), listIndexedMapChecks )
|
|
, reportEmptyListSecondArgument ( ( [ "List" ], "intersperse" ), listIndexedMapChecks )
|
|
, ( ( [ "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" ], "take" ), listTakeChecks )
|
|
, ( ( [ "List" ], "drop" ), listDropChecks )
|
|
, ( ( [ "List" ], "member" ), collectionMemberChecks listCollection )
|
|
, ( ( [ "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" ), collectionFromListChecks setCollection )
|
|
, ( ( [ "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 )
|
|
, ( ( [ "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 )
|
|
, ( ( [ "Platform", "Cmd" ], "batch" ), subAndCmdBatchChecks "Cmd" )
|
|
, ( ( [ "Platform", "Cmd" ], "map" ), collectionMapChecks cmdCollection )
|
|
, ( ( [ "Platform", "Sub" ], "batch" ), subAndCmdBatchChecks "Sub" )
|
|
, ( ( [ "Platform", "Sub" ], "map" ), collectionMapChecks subCollection )
|
|
, ( ( [ "Json", "Decode" ], "oneOf" ), oneOfChecks )
|
|
, ( ( [ "Parser" ], "oneOf" ), oneOfChecks )
|
|
, ( ( [ "Parser", "Advanced" ], "oneOf" ), oneOfChecks )
|
|
]
|
|
|
|
|
|
type alias OperatorCheckInfo =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, inferredConstants : ( Infer.Inferred, List Infer.Inferred )
|
|
, parentRange : Range
|
|
, operator : String
|
|
, 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
|
|
, fromLeftToRight : Bool
|
|
, parentRange : Range
|
|
, left : Node Expression
|
|
, leftRange : Range
|
|
, right : Node Expression
|
|
, rightRange : Range
|
|
}
|
|
|
|
|
|
firstThatReportsError : List (a -> List (Error {})) -> a -> List (Error {})
|
|
firstThatReportsError remainingChecks data =
|
|
case remainingChecks of
|
|
[] ->
|
|
[]
|
|
|
|
checkFn :: restOfFns ->
|
|
case checkFn data of
|
|
[] ->
|
|
firstThatReportsError restOfFns data
|
|
|
|
errors ->
|
|
errors
|
|
|
|
|
|
compositionChecks : List (CompositionCheckInfo -> List (Error {}))
|
|
compositionChecks =
|
|
[ identityCompositionCheck
|
|
, notNotCompositionCheck
|
|
, negateCompositionCheck
|
|
, alwaysCompositionCheck
|
|
, maybeMapCompositionChecks
|
|
, resultMapCompositionChecks
|
|
, filterAndMapCompositionCheck
|
|
, concatAndMapCompositionCheck
|
|
]
|
|
|
|
|
|
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 ])
|
|
[ removeFunctionFromFunctionCall checkInfo
|
|
, removeFunctionFromFunctionCall
|
|
{ fnRange = Node.range secondFn
|
|
, firstArg = firstArgOfSecondCall
|
|
, usingRightPizza = False
|
|
}
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Expression.OperatorApplication "|>" _ firstArgOfSecondCall secondFn ->
|
|
case secondFunctionCheck checkInfo.lookupTable secondFn of
|
|
Just secondRange ->
|
|
[ Rule.errorWithFix
|
|
errorMessage
|
|
(Range.combine [ checkInfo.fnRange, secondRange ])
|
|
[ removeFunctionFromFunctionCall checkInfo
|
|
, removeFunctionFromFunctionCall
|
|
{ fnRange = Node.range secondFn
|
|
, firstArg = firstArgOfSecondCall
|
|
, usingRightPizza = True
|
|
}
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Expression.OperatorApplication "<|" _ secondFn firstArgOfSecondCall ->
|
|
case secondFunctionCheck checkInfo.lookupTable secondFn of
|
|
Just secondRange ->
|
|
[ Rule.errorWithFix
|
|
errorMessage
|
|
(Range.combine [ checkInfo.fnRange, secondRange ])
|
|
[ removeFunctionFromFunctionCall checkInfo
|
|
, removeFunctionFromFunctionCall
|
|
{ fnRange = Node.range secondFn
|
|
, firstArg = firstArgOfSecondCall
|
|
, usingRightPizza = False
|
|
}
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
plusChecks : OperatorCheckInfo -> List (Error {})
|
|
plusChecks { leftRange, rightRange, left, right } =
|
|
findMap
|
|
(\( node, getRange ) ->
|
|
if getUncomputedNumberValue node == Just 0 then
|
|
Just
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary addition with 0"
|
|
, details = [ "Adding 0 does not change the value of the number." ]
|
|
}
|
|
(getRange ())
|
|
[ Fix.removeRange (getRange ()) ]
|
|
]
|
|
|
|
else
|
|
Nothing
|
|
)
|
|
[ ( right, \() -> { start = leftRange.end, end = rightRange.end } )
|
|
, ( left, \() -> { start = leftRange.start, end = rightRange.start } )
|
|
]
|
|
|> Maybe.withDefault []
|
|
|
|
|
|
minusChecks : OperatorCheckInfo -> List (Error {})
|
|
minusChecks { leftRange, rightRange, left, right } =
|
|
if getUncomputedNumberValue right == Just 0 then
|
|
let
|
|
range : Range
|
|
range =
|
|
{ start = leftRange.end, end = rightRange.end }
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary subtraction with 0"
|
|
, details = [ "Subtracting 0 does not change the value of the number." ]
|
|
}
|
|
range
|
|
[ Fix.removeRange range ]
|
|
]
|
|
|
|
else if getUncomputedNumberValue left == Just 0 then
|
|
let
|
|
range : Range
|
|
range =
|
|
{ start = leftRange.start, end = rightRange.start }
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary subtracting from 0"
|
|
, details = [ "You can negate the expression on the right like `-n`." ]
|
|
}
|
|
range
|
|
(if needsParens (Node.value right) then
|
|
[ Fix.replaceRangeBy range "-(", Fix.insertAt rightRange.end ")" ]
|
|
|
|
else
|
|
[ Fix.replaceRangeBy range "-" ]
|
|
)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
multiplyChecks : OperatorCheckInfo -> List (Error {})
|
|
multiplyChecks { parentRange, leftRange, rightRange, left, right } =
|
|
findMap
|
|
(\( node, getRange ) ->
|
|
case getUncomputedNumberValue node of
|
|
Just value ->
|
|
if value == 1 then
|
|
Just
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary multiplication by 1"
|
|
, details = [ "Multiplying by 1 does not change the value of the number." ]
|
|
}
|
|
(getRange ())
|
|
[ Fix.removeRange (getRange ()) ]
|
|
]
|
|
|
|
else if value == 0 then
|
|
Just
|
|
[ Rule.errorWithFix
|
|
{ message = "Multiplying by 0 equals 0"
|
|
, details = [ "You can replace this value by 0." ]
|
|
}
|
|
(getRange ())
|
|
[ Fix.replaceRangeBy parentRange "0" ]
|
|
]
|
|
|
|
else
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
)
|
|
[ ( right, \() -> { start = leftRange.end, end = rightRange.end } )
|
|
, ( left, \() -> { start = leftRange.start, end = rightRange.start } )
|
|
]
|
|
|> Maybe.withDefault []
|
|
|
|
|
|
divisionChecks : OperatorCheckInfo -> List (Error {})
|
|
divisionChecks { leftRange, rightRange, right } =
|
|
if getUncomputedNumberValue right == Just 1 then
|
|
let
|
|
range : Range
|
|
range =
|
|
{ start = leftRange.end, end = rightRange.end }
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary division by 1"
|
|
, details = [ "Dividing by 1 does not change the value of the number." ]
|
|
}
|
|
range
|
|
[ Fix.removeRange range ]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
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
|
|
|
|
|
|
plusplusChecks : OperatorCheckInfo -> List (Error {})
|
|
plusplusChecks { parentRange, leftRange, rightRange, left, right, isOnTheRightSideOfPlusPlus } =
|
|
case ( Node.value left, Node.value right ) of
|
|
( Expression.Literal "", Expression.Literal _ ) ->
|
|
[ errorForAddingEmptyStrings leftRange
|
|
{ start = leftRange.start
|
|
, end = rightRange.start
|
|
}
|
|
]
|
|
|
|
( Expression.Literal _, Expression.Literal "" ) ->
|
|
[ errorForAddingEmptyStrings rightRange
|
|
{ start = leftRange.end
|
|
, end = rightRange.end
|
|
}
|
|
]
|
|
|
|
( Expression.ListExpr [], _ ) ->
|
|
[ errorForAddingEmptyLists leftRange
|
|
{ start = leftRange.start
|
|
, end = rightRange.start
|
|
}
|
|
]
|
|
|
|
( _, Expression.ListExpr [] ) ->
|
|
[ errorForAddingEmptyLists rightRange
|
|
{ start = leftRange.end
|
|
, end = 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." ]
|
|
}
|
|
parentRange
|
|
[ Fix.replaceRangeBy
|
|
{ start = { row = leftRange.end.row, column = leftRange.end.column - 1 }
|
|
, end = { row = rightRange.start.row, column = rightRange.start.column + 1 }
|
|
}
|
|
","
|
|
]
|
|
]
|
|
|
|
( Expression.ListExpr [ listElement ], _ ) ->
|
|
if 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." ]
|
|
}
|
|
parentRange
|
|
(Fix.replaceRangeBy
|
|
{ start = leftRange.end
|
|
, end = rightRange.start
|
|
}
|
|
" :: "
|
|
:: replaceBySubExpressionFix leftRange listElement
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
errorForAddingEmptyStrings : Range -> Range -> Error {}
|
|
errorForAddingEmptyStrings range rangeToRemove =
|
|
Rule.errorWithFix
|
|
{ message = "Unnecessary concatenation with an empty string"
|
|
, details = [ "You should remove the concatenation with the empty string." ]
|
|
}
|
|
range
|
|
[ Fix.removeRange rangeToRemove ]
|
|
|
|
|
|
errorForAddingEmptyLists : Range -> Range -> Error {}
|
|
errorForAddingEmptyLists range rangeToRemove =
|
|
Rule.errorWithFix
|
|
{ message = "Concatenating with a single list doesn't have any effect"
|
|
, details = [ "You should remove the concatenation with the empty list." ]
|
|
}
|
|
range
|
|
[ Fix.removeRange rangeToRemove ]
|
|
|
|
|
|
consChecks : OperatorCheckInfo -> List (Error {})
|
|
consChecks { right, leftRange, rightRange } =
|
|
case Node.value right of
|
|
Expression.ListExpr [] ->
|
|
[ 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." ]
|
|
}
|
|
leftRange
|
|
[ Fix.insertAt leftRange.start "[ "
|
|
, Fix.replaceRangeBy
|
|
{ start = leftRange.end
|
|
, end = rightRange.end
|
|
}
|
|
" ]"
|
|
]
|
|
]
|
|
|
|
Expression.ListExpr _ ->
|
|
[ 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." ]
|
|
}
|
|
leftRange
|
|
[ Fix.insertAt leftRange.start "[ "
|
|
, Fix.replaceRangeBy
|
|
{ start = leftRange.end
|
|
, end = { row = rightRange.start.row, column = rightRange.start.column + 1 }
|
|
}
|
|
","
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
|
|
-- NUMBERS
|
|
|
|
|
|
negateNegateCompositionErrorMessage : { message : String, details : List String }
|
|
negateNegateCompositionErrorMessage =
|
|
{ message = "Unnecessary double negation"
|
|
, details = [ "Composing `negate` with `negate` cancel each other out." ]
|
|
}
|
|
|
|
|
|
negateCompositionCheck : CompositionCheckInfo -> List (Error {})
|
|
negateCompositionCheck { lookupTable, fromLeftToRight, parentRange, left, right, leftRange, rightRange } =
|
|
case Maybe.map2 Tuple.pair (getNegateFunction lookupTable left) (getNegateFunction lookupTable right) of
|
|
Just _ ->
|
|
[ Rule.errorWithFix
|
|
negateNegateCompositionErrorMessage
|
|
parentRange
|
|
[ Fix.replaceRangeBy parentRange "identity" ]
|
|
]
|
|
|
|
_ ->
|
|
case getNegateFunction lookupTable left of
|
|
Just leftNotRange ->
|
|
case getNegateComposition lookupTable fromLeftToRight right of
|
|
Just rightNotRange ->
|
|
[ Rule.errorWithFix
|
|
negateNegateCompositionErrorMessage
|
|
{ start = leftNotRange.start, end = rightNotRange.end }
|
|
[ Fix.removeRange { start = leftNotRange.start, end = rightRange.start }
|
|
, Fix.removeRange rightNotRange
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
case getNegateFunction lookupTable right of
|
|
Just rightNotRange ->
|
|
case getNegateComposition lookupTable (not fromLeftToRight) left of
|
|
Just leftNotRange ->
|
|
[ Rule.errorWithFix
|
|
negateNegateCompositionErrorMessage
|
|
{ start = leftNotRange.start, end = rightNotRange.end }
|
|
[ Fix.removeRange leftNotRange
|
|
, Fix.removeRange { start = leftRange.end, end = rightNotRange.end }
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
getNegateComposition : ModuleNameLookupTable -> Bool -> Node Expression -> Maybe Range
|
|
getNegateComposition lookupTable takeFirstFunction node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.OperatorApplication "<<" _ left right ->
|
|
if takeFirstFunction then
|
|
getNegateFunction lookupTable right
|
|
|> Maybe.map (\_ -> { start = (Node.range left).end, end = (Node.range right).end })
|
|
|
|
else
|
|
getNegateFunction lookupTable left
|
|
|> Maybe.map (\_ -> { start = (Node.range left).start, end = (Node.range right).start })
|
|
|
|
Expression.OperatorApplication ">>" _ left right ->
|
|
if takeFirstFunction then
|
|
getNegateFunction lookupTable left
|
|
|> Maybe.map (\_ -> { start = (Node.range left).start, end = (Node.range right).start })
|
|
|
|
else
|
|
getNegateFunction lookupTable right
|
|
|> Maybe.map (\_ -> { start = (Node.range left).end, end = (Node.range right).end })
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
basicsNegateChecks : CheckInfo -> List (Error {})
|
|
basicsNegateChecks checkInfo =
|
|
removeAlongWithOtherFunctionCheck negateNegateCompositionErrorMessage getNegateFunction checkInfo
|
|
|
|
|
|
getNegateFunction : ModuleNameLookupTable -> Node Expression -> Maybe Range
|
|
getNegateFunction lookupTable baseNode =
|
|
case AstHelpers.removeParens baseNode of
|
|
Node range (Expression.FunctionOrValue _ "negate") ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable range of
|
|
Just [ "Basics" ] ->
|
|
Just range
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
|
|
-- BOOLEAN
|
|
|
|
|
|
basicsNotChecks : CheckInfo -> List (Error {})
|
|
basicsNotChecks checkInfo =
|
|
case Evaluate.getBoolean checkInfo checkInfo.firstArg of
|
|
Determined bool ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Expression is equal to " ++ boolToString (not bool)
|
|
, details = [ "You can replace the call to `not` by the boolean value directly." ]
|
|
}
|
|
checkInfo.parentRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange (boolToString (not bool)) ]
|
|
]
|
|
|
|
Undetermined ->
|
|
removeAlongWithOtherFunctionCheck notNotCompositionErrorMessage getNotFunction checkInfo
|
|
|
|
|
|
notNotCompositionCheck : CompositionCheckInfo -> List (Error {})
|
|
notNotCompositionCheck { lookupTable, fromLeftToRight, parentRange, left, right, leftRange, rightRange } =
|
|
let
|
|
notOnLeft : Maybe Range
|
|
notOnLeft =
|
|
getNotFunction lookupTable left
|
|
|
|
notOnRight : Maybe Range
|
|
notOnRight =
|
|
getNotFunction lookupTable right
|
|
in
|
|
case ( notOnLeft, notOnRight ) of
|
|
( Just _, Just _ ) ->
|
|
[ Rule.errorWithFix
|
|
notNotCompositionErrorMessage
|
|
parentRange
|
|
[ Fix.replaceRangeBy parentRange "identity" ]
|
|
]
|
|
|
|
( Just leftNotRange, _ ) ->
|
|
case getNotComposition lookupTable fromLeftToRight right of
|
|
Just rightNotRange ->
|
|
[ Rule.errorWithFix
|
|
notNotCompositionErrorMessage
|
|
{ start = leftNotRange.start, end = rightNotRange.end }
|
|
[ Fix.removeRange { start = leftNotRange.start, end = rightRange.start }
|
|
, Fix.removeRange rightNotRange
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
( _, Just rightNotRange ) ->
|
|
case getNotComposition lookupTable (not fromLeftToRight) left of
|
|
Just leftNotRange ->
|
|
[ Rule.errorWithFix
|
|
notNotCompositionErrorMessage
|
|
{ start = leftNotRange.start, end = rightNotRange.end }
|
|
[ Fix.removeRange leftNotRange
|
|
, Fix.removeRange { start = leftRange.end, end = rightNotRange.end }
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
notNotCompositionErrorMessage : { message : String, details : List String }
|
|
notNotCompositionErrorMessage =
|
|
{ message = "Unnecessary double negation"
|
|
, details = [ "Composing `not` with `not` cancel each other out." ]
|
|
}
|
|
|
|
|
|
getNotComposition : ModuleNameLookupTable -> Bool -> Node Expression -> Maybe Range
|
|
getNotComposition lookupTable takeFirstFunction node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.OperatorApplication "<<" _ left right ->
|
|
if takeFirstFunction then
|
|
getNotFunction lookupTable right
|
|
|> Maybe.map (\_ -> { start = (Node.range left).end, end = (Node.range right).end })
|
|
|
|
else
|
|
getNotFunction lookupTable left
|
|
|> Maybe.map (\_ -> { start = (Node.range left).start, end = (Node.range right).start })
|
|
|
|
Expression.OperatorApplication ">>" _ left right ->
|
|
if takeFirstFunction then
|
|
getNotFunction lookupTable left
|
|
|> Maybe.map (\_ -> { start = (Node.range left).start, end = (Node.range right).start })
|
|
|
|
else
|
|
getNotFunction lookupTable right
|
|
|> Maybe.map (\_ -> { start = (Node.range left).end, end = (Node.range right).end })
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
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 : 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
|
|
|
|
Normalize.ConfirmedInequality ->
|
|
[]
|
|
|
|
Normalize.Unconfirmed ->
|
|
[]
|
|
|
|
|
|
errorForRedundantCondition : String -> RedundantConditionResolution -> Node a -> List (Error {})
|
|
errorForRedundantCondition operator redundantConditionResolution node =
|
|
let
|
|
( range, fix ) =
|
|
rangeAndFixForRedundantCondition redundantConditionResolution node
|
|
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 -> ( Range, List Fix )
|
|
rangeAndFixForRedundantCondition redundantConditionResolution node =
|
|
case redundantConditionResolution of
|
|
RemoveFrom locationOfPrevElement ->
|
|
let
|
|
range : Range
|
|
range =
|
|
{ start = locationOfPrevElement
|
|
, end = (Node.range node).end
|
|
}
|
|
in
|
|
( range
|
|
, [ Fix.removeRange range ]
|
|
)
|
|
|
|
ReplaceByNoop noopValue ->
|
|
let
|
|
range : Range
|
|
range =
|
|
Node.range node
|
|
in
|
|
( range
|
|
, [ Fix.replaceRangeBy range (boolToString noopValue) ]
|
|
)
|
|
|
|
|
|
listConditions : String -> RedundantConditionResolution -> Node Expression -> List ( RedundantConditionResolution, Node Expression )
|
|
listConditions operatorToLookFor redundantConditionResolution node =
|
|
case Node.value node 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, node ) ]
|
|
|
|
_ ->
|
|
[ ( redundantConditionResolution, node ) ]
|
|
|
|
|
|
or_isLeftSimplifiableError : OperatorCheckInfo -> List (Error {})
|
|
or_isLeftSimplifiableError ({ parentRange, left, leftRange, rightRange } as checkInfo) =
|
|
case Evaluate.getBoolean checkInfo left of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Comparison is always True"
|
|
, details = alwaysSameDetails
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange
|
|
{ start = leftRange.end
|
|
, end = rightRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange
|
|
{ start = leftRange.start
|
|
, end = rightRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
or_isRightSimplifiableError : OperatorCheckInfo -> List (Error {})
|
|
or_isRightSimplifiableError ({ parentRange, right, leftRange, rightRange } as checkInfo) =
|
|
case Evaluate.getBoolean checkInfo right of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange
|
|
{ start = leftRange.start
|
|
, end = rightRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange
|
|
{ start = leftRange.end
|
|
, end = rightRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
andChecks : OperatorCheckInfo -> List (Error {})
|
|
andChecks operatorCheckInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
List.concat
|
|
[ and_isLeftSimplifiableError operatorCheckInfo
|
|
, and_isRightSimplifiableError operatorCheckInfo
|
|
]
|
|
, \() -> findSimilarConditionsError operatorCheckInfo
|
|
]
|
|
()
|
|
|
|
|
|
and_isLeftSimplifiableError : OperatorCheckInfo -> List (Rule.Error {})
|
|
and_isLeftSimplifiableError ({ parentRange, left, leftRange, rightRange } as checkInfo) =
|
|
case Evaluate.getBoolean checkInfo left of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange
|
|
{ start = leftRange.start
|
|
, end = rightRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Comparison is always False"
|
|
, details = alwaysSameDetails
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange
|
|
{ start = leftRange.end
|
|
, end = rightRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
and_isRightSimplifiableError : OperatorCheckInfo -> List (Rule.Error {})
|
|
and_isRightSimplifiableError ({ parentRange, leftRange, right, rightRange } as checkInfo) =
|
|
case Evaluate.getBoolean checkInfo right of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = unnecessaryMessage
|
|
, details = unnecessaryDetails
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange
|
|
{ start = leftRange.end
|
|
, end = rightRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Comparison is always False"
|
|
, details = alwaysSameDetails
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange
|
|
{ start = leftRange.start
|
|
, end = rightRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
|
|
-- EQUALITY
|
|
|
|
|
|
equalityChecks : Bool -> OperatorCheckInfo -> List (Error {})
|
|
equalityChecks isEqual ({ lookupTable, parentRange, left, right, leftRange, rightRange } as checkInfo) =
|
|
if Evaluate.getBoolean 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." ]
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange { start = leftRange.end, end = rightRange.end } ]
|
|
]
|
|
|
|
else if Evaluate.getBoolean 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." ]
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange { start = leftRange.start, end = rightRange.start } ]
|
|
]
|
|
|
|
else
|
|
case Maybe.map2 Tuple.pair (getNotCall lookupTable left) (getNotCall lookupTable right) of
|
|
Just ( notRangeLeft, notRangeRight ) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary negation on both sides"
|
|
, details = [ "Since both sides are negated using `not`, they are redundant and can be removed." ]
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange notRangeLeft, Fix.removeRange notRangeRight ]
|
|
]
|
|
|
|
_ ->
|
|
let
|
|
inferred : Infer.Inferred
|
|
inferred =
|
|
Tuple.first checkInfo.inferredConstants
|
|
|
|
normalizeAndInfer : Node Expression -> Node Expression
|
|
normalizeAndInfer node =
|
|
let
|
|
newNode : Node Expression
|
|
newNode =
|
|
Normalize.normalize checkInfo node
|
|
in
|
|
case Infer.get (Node.value newNode) inferred of
|
|
Just expr ->
|
|
Node Range.emptyRange expr
|
|
|
|
Nothing ->
|
|
newNode
|
|
|
|
normalizedLeft : Node Expression
|
|
normalizedLeft =
|
|
normalizeAndInfer left
|
|
|
|
normalizedRight : Node Expression
|
|
normalizedRight =
|
|
normalizeAndInfer right
|
|
in
|
|
case Normalize.compareWithoutNormalization normalizedLeft normalizedRight of
|
|
Normalize.ConfirmedEquality ->
|
|
[ comparisonError isEqual parentRange ]
|
|
|
|
Normalize.ConfirmedInequality ->
|
|
[ comparisonError (not isEqual) parentRange ]
|
|
|
|
Normalize.Unconfirmed ->
|
|
[]
|
|
|
|
|
|
getNotCall : ModuleNameLookupTable -> Node Expression -> Maybe Range
|
|
getNotCall lookupTable baseNode =
|
|
case Node.value (AstHelpers.removeParens baseNode) of
|
|
Expression.Application ((Node notRange (Expression.FunctionOrValue _ "not")) :: _) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable notRange of
|
|
Just [ "Basics" ] ->
|
|
Just notRange
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
getNotFunction : ModuleNameLookupTable -> Node Expression -> Maybe Range
|
|
getNotFunction lookupTable baseNode =
|
|
case AstHelpers.removeParens baseNode of
|
|
Node notRange (Expression.FunctionOrValue _ "not") ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable notRange of
|
|
Just [ "Basics" ] ->
|
|
Just notRange
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
getSpecificFunction : ( ModuleName, String ) -> ModuleNameLookupTable -> Node Expression -> Maybe Range
|
|
getSpecificFunction ( moduleName, name ) lookupTable baseNode =
|
|
case AstHelpers.removeParens baseNode of
|
|
Node fnRange (Expression.FunctionOrValue _ foundName) ->
|
|
if
|
|
(foundName == name)
|
|
&& (ModuleNameLookupTable.moduleNameAt lookupTable fnRange == Just moduleName)
|
|
then
|
|
Just fnRange
|
|
|
|
else
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
getSpecificFunctionCall : ( ModuleName, String ) -> ModuleNameLookupTable -> Node Expression -> Maybe { nodeRange : Range, fnRange : Range }
|
|
getSpecificFunctionCall ( moduleName, name ) lookupTable baseNode =
|
|
let
|
|
match : Maybe ( Range, String )
|
|
match =
|
|
case Node.value (AstHelpers.removeParens baseNode) of
|
|
Expression.Application ((Node fnRange (Expression.FunctionOrValue _ foundName)) :: _ :: _) ->
|
|
Just ( fnRange, foundName )
|
|
|
|
Expression.OperatorApplication "|>" _ _ (Node _ (Expression.Application ((Node fnRange (Expression.FunctionOrValue _ foundName)) :: _))) ->
|
|
Just ( fnRange, foundName )
|
|
|
|
Expression.OperatorApplication "<|" _ (Node _ (Expression.Application ((Node fnRange (Expression.FunctionOrValue _ foundName)) :: _))) _ ->
|
|
Just ( fnRange, foundName )
|
|
|
|
_ ->
|
|
Nothing
|
|
in
|
|
case match of
|
|
Just ( fnRange, foundName ) ->
|
|
if
|
|
(foundName == name)
|
|
&& (ModuleNameLookupTable.moduleNameAt lookupTable fnRange == Just moduleName)
|
|
then
|
|
Just { nodeRange = Node.range baseNode, fnRange = fnRange }
|
|
|
|
else
|
|
Nothing
|
|
|
|
Nothing ->
|
|
Nothing
|
|
|
|
|
|
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.parentRange ]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
comparisonError : Bool -> Range -> Error {}
|
|
comparisonError bool range =
|
|
let
|
|
boolAsString : String
|
|
boolAsString =
|
|
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 ++ "."
|
|
]
|
|
}
|
|
range
|
|
[ Fix.replaceRangeBy range boolAsString ]
|
|
|
|
|
|
|
|
-- IF EXPRESSIONS
|
|
|
|
|
|
targetIfKeyword : Range -> Range
|
|
targetIfKeyword { start } =
|
|
{ start = start
|
|
, end = { start | column = start.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
|
|
[ removeFunctionFromFunctionCall checkInfo
|
|
]
|
|
]
|
|
|
|
|
|
identityCompositionCheck : CompositionCheckInfo -> List (Error {})
|
|
identityCompositionCheck { lookupTable, left, right } =
|
|
if isIdentity lookupTable right then
|
|
[ Rule.errorWithFix
|
|
{ message = "`identity` should be removed"
|
|
, details = [ "Composing a function with `identity` is the same as simplify referencing the function." ]
|
|
}
|
|
(Node.range right)
|
|
[ Fix.removeRange { start = (Node.range left).end, end = (Node.range right).end }
|
|
]
|
|
]
|
|
|
|
else if isIdentity lookupTable left then
|
|
[ Rule.errorWithFix
|
|
{ message = "`identity` should be removed"
|
|
, details = [ "Composing a function with `identity` is the same as simplify referencing the function." ]
|
|
}
|
|
(Node.range left)
|
|
[ Fix.removeRange { start = (Node.range left).start, end = (Node.range right).start }
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
basicsAlwaysChecks : CheckInfo -> List (Error {})
|
|
basicsAlwaysChecks ({ fnRange, firstArg, secondArg, usingRightPizza } as checkInfo) =
|
|
case secondArg 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." ]
|
|
}
|
|
fnRange
|
|
(if usingRightPizza then
|
|
[ Fix.removeRange { start = secondArgRange.start, end = (Node.range firstArg).start }
|
|
]
|
|
|
|
else
|
|
[ removeFunctionFromFunctionCall checkInfo
|
|
, Fix.removeRange { start = (Node.range firstArg).end, end = secondArgRange.end }
|
|
]
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
alwaysCompositionCheck : CompositionCheckInfo -> List (Error {})
|
|
alwaysCompositionCheck { lookupTable, fromLeftToRight, left, right, leftRange, rightRange } =
|
|
if fromLeftToRight then
|
|
if isAlwaysCall lookupTable right then
|
|
[ Rule.errorWithFix
|
|
{ message = "Function composed with always will be ignored"
|
|
, details = [ "`always` will swallow the function composed into it." ]
|
|
}
|
|
rightRange
|
|
[ Fix.removeRange { start = leftRange.start, end = rightRange.start } ]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
else if isAlwaysCall lookupTable left then
|
|
[ Rule.errorWithFix
|
|
{ message = "Function composed with always will be ignored"
|
|
, details = [ "`always` will swallow the function composed into it." ]
|
|
}
|
|
leftRange
|
|
[ Fix.removeRange { start = leftRange.end, end = rightRange.end } ]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
isAlwaysCall : ModuleNameLookupTable -> Node Expression -> Bool
|
|
isAlwaysCall lookupTable node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.Application ((Node alwaysRange (Expression.FunctionOrValue _ "always")) :: _ :: []) ->
|
|
ModuleNameLookupTable.moduleNameAt lookupTable alwaysRange == Just [ "Basics" ]
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
getAlwaysArgument : ModuleNameLookupTable -> Node Expression -> Maybe { alwaysRange : Range, rangeToRemove : Range }
|
|
getAlwaysArgument lookupTable node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.Application ((Node alwaysRange (Expression.FunctionOrValue _ "always")) :: arg :: []) ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable alwaysRange == Just [ "Basics" ] then
|
|
Just
|
|
{ alwaysRange = alwaysRange
|
|
, rangeToRemove = { start = alwaysRange.start, end = (Node.range arg).start }
|
|
}
|
|
|
|
else
|
|
Nothing
|
|
|
|
Expression.OperatorApplication "<|" _ (Node alwaysRange (Expression.FunctionOrValue _ "always")) arg ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable alwaysRange == Just [ "Basics" ] then
|
|
Just
|
|
{ alwaysRange = alwaysRange
|
|
, rangeToRemove = { start = alwaysRange.start, end = (Node.range arg).start }
|
|
}
|
|
|
|
else
|
|
Nothing
|
|
|
|
Expression.OperatorApplication "|>" _ arg (Node alwaysRange (Expression.FunctionOrValue _ "always")) ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable alwaysRange == Just [ "Basics" ] then
|
|
Just
|
|
{ alwaysRange = alwaysRange
|
|
, rangeToRemove = { start = (Node.range arg).end, end = alwaysRange.end }
|
|
}
|
|
|
|
else
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
reportEmptyListSecondArgument : ( ( ModuleName, String ), CheckInfo -> List (Error {}) ) -> ( ( ModuleName, String ), CheckInfo -> List (Error {}) )
|
|
reportEmptyListSecondArgument ( ( moduleName, name ), function ) =
|
|
( ( moduleName, name )
|
|
, \checkInfo ->
|
|
case checkInfo.secondArg of
|
|
Just (Node _ (Expression.ListExpr [])) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ String.join "." 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 " ++ String.join "." 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
|
|
|
|
|
|
stringIsEmptyChecks : CheckInfo -> List (Error {})
|
|
stringIsEmptyChecks { parentRange, fnRange, firstArg } =
|
|
case Node.value firstArg of
|
|
Expression.Literal str ->
|
|
let
|
|
replacementValue : String
|
|
replacementValue =
|
|
boolToString (str == "")
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to String.isEmpty will result in " ++ replacementValue
|
|
, details = [ "You can replace this call by " ++ replacementValue ++ "." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange replacementValue ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringConcatChecks : CheckInfo -> List (Error {})
|
|
stringConcatChecks { parentRange, fnRange, firstArg } =
|
|
case Node.value firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.concat on an empty list will result in a empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "\"\"" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringWordsChecks : CheckInfo -> List (Error {})
|
|
stringWordsChecks { parentRange, fnRange, firstArg } =
|
|
case Node.value 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." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringLinesChecks : CheckInfo -> List (Error {})
|
|
stringLinesChecks { parentRange, fnRange, firstArg } =
|
|
case Node.value 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." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringReverseChecks : CheckInfo -> List (Error {})
|
|
stringReverseChecks ({ parentRange, fnRange, firstArg } as checkInfo) =
|
|
case Node.value firstArg of
|
|
Expression.Literal "" ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.reverse on an empty string will result in a empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "\"\"" ]
|
|
]
|
|
|
|
_ ->
|
|
removeAlongWithOtherFunctionCheck
|
|
reverseReverseCompositionErrorMessage
|
|
(getSpecificFunction ( [ "String" ], "reverse" ))
|
|
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 { parentRange, fnRange, firstArg, secondArg } =
|
|
case secondArg of
|
|
Just (Node _ (Expression.ListExpr [])) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.join on an empty list will result in a empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "\"\"" ]
|
|
]
|
|
|
|
_ ->
|
|
case Node.value 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." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy { start = fnRange.start, end = (Node.range firstArg).end } "String.concat" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringLengthChecks : CheckInfo -> List (Error {})
|
|
stringLengthChecks { parentRange, fnRange, firstArg } =
|
|
case Node.value 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." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange (String.fromInt (String.length str)) ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringRepeatChecks : CheckInfo -> List (Error {})
|
|
stringRepeatChecks ({ parentRange, fnRange, firstArg, secondArg } as checkInfo) =
|
|
case secondArg of
|
|
Just (Node _ (Expression.Literal "")) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using String.repeat with an empty string will result in a empty string"
|
|
, details = [ "You can replace this call by an empty string." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "\"\"" ]
|
|
]
|
|
|
|
_ ->
|
|
case Evaluate.getInt 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." ]
|
|
}
|
|
fnRange
|
|
[ Fix.removeRange { start = fnRange.start, end = (Node.range 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." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix "\"\"" parentRange secondArg)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
stringReplaceChecks : CheckInfo -> List (Error {})
|
|
stringReplaceChecks ({ fnRange, firstArg, secondArg, thirdArg } as checkInfo) =
|
|
case secondArg of
|
|
Just secondArg_ ->
|
|
case Normalize.compare checkInfo firstArg secondArg_ 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." ]
|
|
}
|
|
fnRange
|
|
(case thirdArg of
|
|
Just thirdArg_ ->
|
|
[ Fix.removeRange
|
|
{ start = fnRange.start
|
|
, end = (Node.range thirdArg_).start
|
|
}
|
|
]
|
|
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy
|
|
{ start = fnRange.start
|
|
, end = (Node.range secondArg_).end
|
|
}
|
|
"identity"
|
|
]
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
case ( Node.value firstArg, Node.value secondArg_, thirdArg ) 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." ]
|
|
}
|
|
fnRange
|
|
[ Fix.removeRange
|
|
{ start = fnRange.start
|
|
, end = thirdRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
( Expression.Literal first, Expression.Literal second, Just (Node thirdRange (Expression.Literal third)) ) ->
|
|
if 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." ]
|
|
}
|
|
fnRange
|
|
[ Fix.removeRange
|
|
{ start = fnRange.start
|
|
, end = thirdRange.start
|
|
}
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
|
|
-- MAYBE FUNCTIONS
|
|
|
|
|
|
maybeMapChecks : CheckInfo -> List (Error {})
|
|
maybeMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> collectionMapChecks maybeCollection checkInfo
|
|
, \() ->
|
|
case Match.maybeAndThen (getMaybeValues checkInfo.lookupTable) checkInfo.secondArg of
|
|
Determined (Just justRanges) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling Maybe.map on a value that is Just"
|
|
, details = [ "The function can be called without Maybe.map." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(if checkInfo.usingRightPizza then
|
|
[ Fix.removeRange { start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
, Fix.insertAt (Node.range checkInfo.firstArg).end " |> Just"
|
|
]
|
|
++ List.map Fix.removeRange justRanges
|
|
|
|
else
|
|
[ Fix.replaceRangeBy { start = checkInfo.parentRange.start, end = (Node.range checkInfo.firstArg).start } "Just ("
|
|
, Fix.insertAt checkInfo.parentRange.end ")"
|
|
]
|
|
++ List.map Fix.removeRange justRanges
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
maybeMapCompositionChecks : CompositionCheckInfo -> List (Error {})
|
|
maybeMapCompositionChecks { lookupTable, fromLeftToRight, parentRange, left, right } =
|
|
if fromLeftToRight then
|
|
case ( AstHelpers.removeParens left, Node.value (AstHelpers.removeParens right) ) of
|
|
( Node justRange (Expression.FunctionOrValue _ "Just"), Expression.Application ((Node maybeMapRange (Expression.FunctionOrValue _ "map")) :: mapperFunction :: []) ) ->
|
|
if
|
|
(ModuleNameLookupTable.moduleNameAt lookupTable justRange == Just [ "Maybe" ])
|
|
&& (ModuleNameLookupTable.moduleNameAt lookupTable maybeMapRange == Just [ "Maybe" ])
|
|
then
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling Maybe.map on a value that is Just"
|
|
, details = [ "The function can be called without Maybe.map." ]
|
|
}
|
|
maybeMapRange
|
|
[ Fix.removeRange { start = parentRange.start, end = (Node.range mapperFunction).start }
|
|
, Fix.insertAt (Node.range mapperFunction).end " >> Just"
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
case ( Node.value (AstHelpers.removeParens left), AstHelpers.removeParens right ) of
|
|
( Expression.Application ((Node maybeMapRange (Expression.FunctionOrValue _ "map")) :: mapperFunction :: []), Node justRange (Expression.FunctionOrValue _ "Just") ) ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable justRange == Just [ "Maybe" ] && ModuleNameLookupTable.moduleNameAt lookupTable maybeMapRange == Just [ "Maybe" ] then
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling Maybe.map on a value that is Just"
|
|
, details = [ "The function can be called without Maybe.map." ]
|
|
}
|
|
maybeMapRange
|
|
[ Fix.replaceRangeBy { start = parentRange.start, end = (Node.range mapperFunction).start } "Just << "
|
|
, Fix.removeRange { start = (Node.range mapperFunction).end, end = parentRange.end }
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
resultMapCompositionChecks : CompositionCheckInfo -> List (Error {})
|
|
resultMapCompositionChecks { lookupTable, fromLeftToRight, parentRange, left, right } =
|
|
if fromLeftToRight then
|
|
case ( AstHelpers.removeParens left, Node.value (AstHelpers.removeParens right) ) of
|
|
( Node justRange (Expression.FunctionOrValue _ "Ok"), Expression.Application ((Node resultMapRange (Expression.FunctionOrValue _ "map")) :: mapperFunction :: []) ) ->
|
|
if
|
|
(ModuleNameLookupTable.moduleNameAt lookupTable justRange == Just [ "Result" ])
|
|
&& (ModuleNameLookupTable.moduleNameAt lookupTable resultMapRange == Just [ "Result" ])
|
|
then
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling Result.map on a value that is Ok"
|
|
, details = [ "The function can be called without Result.map." ]
|
|
}
|
|
resultMapRange
|
|
[ Fix.removeRange { start = parentRange.start, end = (Node.range mapperFunction).start }
|
|
, Fix.insertAt (Node.range mapperFunction).end " >> Ok"
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
case ( Node.value (AstHelpers.removeParens left), AstHelpers.removeParens right ) of
|
|
( Expression.Application ((Node resultMapRange (Expression.FunctionOrValue _ "map")) :: mapperFunction :: []), Node justRange (Expression.FunctionOrValue _ "Ok") ) ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable justRange == Just [ "Result" ] && ModuleNameLookupTable.moduleNameAt lookupTable resultMapRange == Just [ "Result" ] then
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling Result.map on a value that is Ok"
|
|
, details = [ "The function can be called without Result.map." ]
|
|
}
|
|
resultMapRange
|
|
[ Fix.replaceRangeBy { start = parentRange.start, end = (Node.range mapperFunction).start } "Ok << "
|
|
, Fix.removeRange { start = (Node.range mapperFunction).end, end = parentRange.end }
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
|
|
-- RESULT FUNCTIONS
|
|
|
|
|
|
resultMapChecks : CheckInfo -> List (Error {})
|
|
resultMapChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() -> collectionMapChecks resultCollection checkInfo
|
|
, \() ->
|
|
case Maybe.andThen (getResultValues checkInfo.lookupTable) checkInfo.secondArg of
|
|
Just (Ok okRanges) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling Result.map on a value that is Ok"
|
|
, details = [ "The function can be called without Result.map." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(if checkInfo.usingRightPizza then
|
|
[ Fix.removeRange { start = checkInfo.fnRange.start, end = (Node.range checkInfo.firstArg).start }
|
|
, Fix.insertAt (Node.range checkInfo.firstArg).end " |> Ok"
|
|
]
|
|
++ List.map Fix.removeRange okRanges
|
|
|
|
else
|
|
[ Fix.replaceRangeBy { start = checkInfo.parentRange.start, end = (Node.range checkInfo.firstArg).start } "Ok ("
|
|
, Fix.insertAt checkInfo.parentRange.end ")"
|
|
]
|
|
++ List.map Fix.removeRange okRanges
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
|
|
-- LIST FUNCTIONS
|
|
|
|
|
|
listConcatChecks : CheckInfo -> List (Error {})
|
|
listConcatChecks ({ lookupTable, parentRange, fnRange, firstArg } as checkInfo) =
|
|
case Node.value firstArg of
|
|
Expression.ListExpr list ->
|
|
case list of
|
|
[ Node elementRange _ ] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary use of 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." ]
|
|
}
|
|
parentRange
|
|
[ Fix.removeRange { start = parentRange.start, end = elementRange.start }
|
|
, Fix.removeRange { start = elementRange.end, end = parentRange.end }
|
|
]
|
|
]
|
|
|
|
(firstListElement :: restOfListElements) as args ->
|
|
if List.all isListLiteral list then
|
|
[ Rule.errorWithFix
|
|
{ message = "Expression could be simplified to be a single List"
|
|
, details = [ "Try moving all the elements into a single list." ]
|
|
}
|
|
parentRange
|
|
(Fix.removeRange fnRange
|
|
:: List.concatMap removeBoundariesFix args
|
|
)
|
|
]
|
|
|
|
else
|
|
case findConsecutiveListLiterals firstListElement restOfListElements of
|
|
[] ->
|
|
[]
|
|
|
|
fixes ->
|
|
[ 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." ]
|
|
}
|
|
fnRange
|
|
fixes
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
_ ->
|
|
case getSpecificFunctionCall ( [ "List" ], "map" ) lookupTable firstArg of
|
|
Just match ->
|
|
[ Rule.errorWithFix
|
|
{ message = "List.map and List.concat can be combined using List.concatMap"
|
|
, details = [ "List.concatMap is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
fnRange
|
|
[ removeFunctionFromFunctionCall checkInfo
|
|
, Fix.replaceRangeBy match.fnRange "List.concatMap"
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
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 { lookupTable, parentRange, fnRange, firstArg, secondArg } =
|
|
if isIdentity lookupTable firstArg then
|
|
[ Rule.errorWithFix
|
|
{ message = "Using List.concatMap with an identity function is the same as using List.concat"
|
|
, details = [ "You can replace this call by List.concat." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy { start = fnRange.start, end = (Node.range firstArg).end } "List.concat" ]
|
|
]
|
|
|
|
else if isAlwaysEmptyList lookupTable firstArg then
|
|
[ Rule.errorWithFix
|
|
{ message = "List.concatMap will result in on an empty list"
|
|
, details = [ "You can replace this call by an empty list." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix "[]" parentRange secondArg)
|
|
]
|
|
|
|
else
|
|
case replaceSingleElementListBySingleValue_RENAME lookupTable fnRange firstArg of
|
|
Just errors ->
|
|
errors
|
|
|
|
Nothing ->
|
|
case secondArg of
|
|
Just (Node listRange (Expression.ListExpr [ listElement ])) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using 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." ]
|
|
}
|
|
fnRange
|
|
(Fix.removeRange fnRange
|
|
:: replaceBySubExpressionFix listRange listElement
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
concatAndMapCompositionCheck : CompositionCheckInfo -> List (Error {})
|
|
concatAndMapCompositionCheck { lookupTable, fromLeftToRight, left, right } =
|
|
if fromLeftToRight then
|
|
if isSpecificFunction [ "List" ] "concat" lookupTable right then
|
|
case Node.value (AstHelpers.removeParens left) of
|
|
Expression.Application [ leftFunction, _ ] ->
|
|
if isSpecificFunction [ "List" ] "map" lookupTable leftFunction then
|
|
[ Rule.errorWithFix
|
|
{ message = "List.map and List.concat can be combined using List.concatMap"
|
|
, details = [ "List.concatMap is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
(Node.range right)
|
|
[ Fix.removeRange { start = (Node.range left).end, end = (Node.range right).end }
|
|
, Fix.replaceRangeBy (Node.range leftFunction) "List.concatMap"
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
[]
|
|
|
|
else if isSpecificFunction [ "List" ] "concat" lookupTable left then
|
|
case Node.value (AstHelpers.removeParens right) of
|
|
Expression.Application [ rightFunction, _ ] ->
|
|
if isSpecificFunction [ "List" ] "map" lookupTable rightFunction then
|
|
[ Rule.errorWithFix
|
|
{ message = "List.map and List.concat can be combined using List.concatMap"
|
|
, details = [ "List.concatMap is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
(Node.range left)
|
|
[ Fix.removeRange { start = (Node.range left).start, end = (Node.range right).start }
|
|
, Fix.replaceRangeBy (Node.range rightFunction) "List.concatMap"
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
listIndexedMapChecks : CheckInfo -> List (Error {})
|
|
listIndexedMapChecks { lookupTable, fnRange, firstArg } =
|
|
case AstHelpers.removeParens firstArg of
|
|
Node lambdaRange (Expression.LambdaExpression { args, expression }) ->
|
|
case Maybe.map AstHelpers.removeParensFromPattern (List.head args) of
|
|
Just (Node patternRange Pattern.AllPattern) ->
|
|
let
|
|
rangeToRemove : Range
|
|
rangeToRemove =
|
|
case args of
|
|
[] ->
|
|
Range.emptyRange
|
|
|
|
[ _ ] ->
|
|
-- Only one argument, remove the entire lambda except the expression
|
|
{ start = lambdaRange.start, end = (Node.range expression).start }
|
|
|
|
first :: second :: _ ->
|
|
{ start = (Node.range first).start, end = (Node.range second).start }
|
|
in
|
|
[ Rule.errorWithFix
|
|
{ message = "Use List.map instead"
|
|
, details = [ "Using List.indexedMap while ignoring the first argument is the same thing as calling List.map." ]
|
|
}
|
|
patternRange
|
|
[ Fix.replaceRangeBy fnRange "List.map"
|
|
, Fix.removeRange rangeToRemove
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
_ ->
|
|
case getAlwaysArgument lookupTable firstArg of
|
|
Just { alwaysRange, rangeToRemove } ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use List.map instead"
|
|
, details = [ "Using List.indexedMap while ignoring the first argument is the same thing as calling List.map." ]
|
|
}
|
|
alwaysRange
|
|
[ Fix.replaceRangeBy fnRange "List.map"
|
|
, Fix.removeRange rangeToRemove
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
listAllChecks : CheckInfo -> List (Error {})
|
|
listAllChecks ({ parentRange, fnRange, firstArg, secondArg } as checkInfo) =
|
|
case Maybe.map (AstHelpers.removeParens >> Node.value) secondArg of
|
|
Just (Expression.ListExpr []) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to List.all will result in True"
|
|
, details = [ "You can replace this call by True." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "True" ]
|
|
]
|
|
|
|
_ ->
|
|
case Evaluate.isAlwaysBoolean checkInfo firstArg of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to List.all will result in True"
|
|
, details = [ "You can replace this call by True." ]
|
|
}
|
|
fnRange
|
|
(replaceByBoolFix parentRange secondArg True)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listAnyChecks : CheckInfo -> List (Error {})
|
|
listAnyChecks ({ parentRange, fnRange, firstArg, secondArg } as checkInfo) =
|
|
case Maybe.map (AstHelpers.removeParens >> Node.value) secondArg of
|
|
Just (Expression.ListExpr []) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to List.any will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "False" ]
|
|
]
|
|
|
|
_ ->
|
|
case Evaluate.isAlwaysBoolean checkInfo firstArg of
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to List.any will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
fnRange
|
|
(replaceByBoolFix parentRange secondArg False)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listFilterMapChecks : CheckInfo -> List (Error {})
|
|
listFilterMapChecks ({ lookupTable, parentRange, fnRange, firstArg } as checkInfo) =
|
|
case isAlwaysMaybe lookupTable firstArg of
|
|
Determined (Just { ranges, throughLambdaFunction }) ->
|
|
if throughLambdaFunction then
|
|
[ Rule.errorWithFix
|
|
{ message = "Using List.filterMap with a function that will always return Just is the same as using List.map"
|
|
, details = [ "You can remove the `Just`s and replace the call by List.map." ]
|
|
}
|
|
fnRange
|
|
(Fix.replaceRangeBy fnRange "List.map"
|
|
:: List.map Fix.removeRange ranges
|
|
)
|
|
]
|
|
|
|
else
|
|
[ Rule.errorWithFix
|
|
{ message = "Using List.filterMap with a function that will always return Just is the same as not using List.filterMap"
|
|
, details = [ "You can remove this call and replace it by the list itself." ]
|
|
}
|
|
fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
Determined Nothing ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using 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." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix "[]" parentRange checkInfo.secondArg)
|
|
]
|
|
|
|
Undetermined ->
|
|
if isIdentity lookupTable firstArg then
|
|
case Maybe.andThen (getSpecificFunctionCall ( [ "List" ], "map" ) lookupTable) checkInfo.secondArg of
|
|
Just secondArg ->
|
|
[ Rule.errorWithFix
|
|
{ message = "List.map and List.filterMap identity can be combined using List.filterMap"
|
|
, details = [ "List.filterMap is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
{ start = fnRange.start, end = (Node.range firstArg).end }
|
|
[ removeFunctionAndFirstArg checkInfo secondArg.nodeRange
|
|
, Fix.replaceRangeBy secondArg.fnRange "List.filterMap"
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
case checkInfo.secondArg of
|
|
Just (Node listRange (Expression.ListExpr list)) ->
|
|
case collectJusts lookupTable list [] of
|
|
Just justRanges ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary use of 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 = fnRange.start, end = (Node.range firstArg).end }
|
|
((if checkInfo.usingRightPizza then
|
|
Fix.removeRange { start = listRange.end, end = (Node.range firstArg).end }
|
|
|
|
else
|
|
Fix.removeRange { start = fnRange.start, end = listRange.start }
|
|
)
|
|
:: List.map Fix.removeRange justRanges
|
|
)
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
collectJusts : ModuleNameLookupTable -> List (Node Expression) -> List Range -> Maybe (List Range)
|
|
collectJusts lookupTable list acc =
|
|
case list of
|
|
[] ->
|
|
Just acc
|
|
|
|
element :: restOfList ->
|
|
case Node.value element of
|
|
Expression.Application ((Node justRange (Expression.FunctionOrValue _ "Just")) :: justArg :: []) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Maybe" ] ->
|
|
collectJusts lookupTable restOfList ({ start = justRange.start, end = (Node.range justArg).start } :: acc)
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
filterAndMapCompositionCheck : CompositionCheckInfo -> List (Error {})
|
|
filterAndMapCompositionCheck { lookupTable, fromLeftToRight, left, right } =
|
|
if fromLeftToRight then
|
|
case Node.value (AstHelpers.removeParens right) of
|
|
Expression.Application [ rightFunction, arg ] ->
|
|
if isSpecificFunction [ "List" ] "filterMap" lookupTable rightFunction && isIdentity lookupTable arg then
|
|
case Node.value (AstHelpers.removeParens left) of
|
|
Expression.Application [ leftFunction, _ ] ->
|
|
if isSpecificFunction [ "List" ] "map" lookupTable leftFunction then
|
|
[ Rule.errorWithFix
|
|
{ message = "List.map and List.filterMap identity can be combined using List.filterMap"
|
|
, details = [ "List.filterMap is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
(Node.range right)
|
|
[ Fix.removeRange { start = (Node.range left).end, end = (Node.range right).end }
|
|
, Fix.replaceRangeBy (Node.range leftFunction) "List.filterMap"
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
case Node.value (AstHelpers.removeParens left) of
|
|
Expression.Application [ leftFunction, arg ] ->
|
|
if isSpecificFunction [ "List" ] "filterMap" lookupTable leftFunction && isIdentity lookupTable arg then
|
|
case Node.value (AstHelpers.removeParens right) of
|
|
Expression.Application [ rightFunction, _ ] ->
|
|
if isSpecificFunction [ "List" ] "map" lookupTable rightFunction then
|
|
[ Rule.errorWithFix
|
|
{ message = "List.map and List.filterMap identity can be combined using List.filterMap"
|
|
, details = [ "List.filterMap is meant for this exact purpose and will also be faster." ]
|
|
}
|
|
(Node.range left)
|
|
[ Fix.removeRange { start = (Node.range left).start, end = (Node.range right).start }
|
|
, Fix.replaceRangeBy (Node.range rightFunction) "List.filterMap"
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listRangeChecks : CheckInfo -> List (Error {})
|
|
listRangeChecks ({ parentRange, fnRange, firstArg, secondArg } as checkInfo) =
|
|
case Maybe.andThen (Evaluate.getInt checkInfo) secondArg of
|
|
Just second ->
|
|
case Evaluate.getInt checkInfo firstArg of
|
|
Just first ->
|
|
if first > second then
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to List.range will result in []"
|
|
, details = [ "The second argument to List.range is bigger than the first one, therefore you can replace this list by an empty list." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix "[]" parentRange secondArg)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
listRepeatChecks : CheckInfo -> List (Error {})
|
|
listRepeatChecks ({ parentRange, fnRange, firstArg, secondArg } as checkInfo) =
|
|
case Evaluate.getInt checkInfo firstArg of
|
|
Just intValue ->
|
|
if intValue < 1 then
|
|
[ Rule.errorWithFix
|
|
{ message = "List.repeat will result in an empty list"
|
|
, details = [ "Using List.repeat with a number less than 1 will result in an empty list. You can replace this call by an empty list." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix "[]" parentRange secondArg)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listReverseChecks : CheckInfo -> List (Error {})
|
|
listReverseChecks ({ parentRange, fnRange, firstArg } as checkInfo) =
|
|
case Node.value (AstHelpers.removeParens firstArg) of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using List.reverse on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
removeAlongWithOtherFunctionCheck
|
|
reverseReverseCompositionErrorMessage
|
|
(getSpecificFunction ( [ "List" ], "reverse" ))
|
|
checkInfo
|
|
|
|
|
|
listTakeChecks : CheckInfo -> List (Error {})
|
|
listTakeChecks { lookupTable, parentRange, fnRange, firstArg, secondArg } =
|
|
if getUncomputedNumberValue firstArg == Just 0 then
|
|
[ Rule.errorWithFix
|
|
{ message = "Taking 0 items from a list will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
fnRange
|
|
(case secondArg of
|
|
Just _ ->
|
|
[ Fix.replaceRangeBy parentRange "[]" ]
|
|
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy parentRange "(always [])" ]
|
|
)
|
|
]
|
|
|
|
else
|
|
case Maybe.andThen (determineListLength lookupTable) secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using List.take on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
listDropChecks : CheckInfo -> List (Error {})
|
|
listDropChecks { lookupTable, parentRange, fnRange, firstArg, secondArg, usingRightPizza } =
|
|
if getUncomputedNumberValue firstArg == Just 0 then
|
|
case secondArg of
|
|
Just (Node secondArgRange _) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Dropping 0 items from a list will result in the list itself"
|
|
, details = [ "You can replace this call by the list itself." ]
|
|
}
|
|
fnRange
|
|
[ if usingRightPizza then
|
|
Fix.removeRange { start = secondArgRange.end, end = parentRange.end }
|
|
|
|
else
|
|
Fix.removeRange { start = parentRange.start, end = secondArgRange.start }
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Dropping 0 items from a list will result in the list itself"
|
|
, details = [ "You can replace this function by identity." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "identity" ]
|
|
]
|
|
|
|
else
|
|
case Maybe.andThen (determineListLength lookupTable) secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using List.drop on [] will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
subAndCmdBatchChecks : String -> CheckInfo -> List (Error {})
|
|
subAndCmdBatchChecks moduleName { lookupTable, parentRange, fnRange, firstArg } =
|
|
case Node.value 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" ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange (moduleName ++ ".none") ]
|
|
]
|
|
|
|
Expression.ListExpr [ listElement ] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary " ++ moduleName ++ ".batch"
|
|
, details = [ moduleName ++ ".batch with a single element is equal to that element." ]
|
|
}
|
|
fnRange
|
|
(replaceBySubExpressionFix parentRange listElement)
|
|
]
|
|
|
|
Expression.ListExpr args ->
|
|
List.map3 (\a b c -> ( a, b, c ))
|
|
(Nothing :: List.map (Node.range >> Just) args)
|
|
args
|
|
(List.map (Node.range >> Just) (List.drop 1 args) ++ [ Nothing ])
|
|
|> List.filterMap
|
|
(\( prev, arg, next ) ->
|
|
case AstHelpers.removeParens arg of
|
|
Node batchRange (Expression.FunctionOrValue _ "none") ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable batchRange == Just [ "Platform", moduleName ] then
|
|
Just
|
|
(Rule.errorWithFix
|
|
{ message = "Unnecessary " ++ moduleName ++ ".none"
|
|
, details = [ moduleName ++ ".none will be ignored by " ++ moduleName ++ ".batch." ]
|
|
}
|
|
(Node.range arg)
|
|
(case prev of
|
|
Just prevRange ->
|
|
[ Fix.removeRange { start = prevRange.end, end = (Node.range arg).end } ]
|
|
|
|
Nothing ->
|
|
case next of
|
|
Just nextRange ->
|
|
[ Fix.removeRange { start = (Node.range arg).start, end = nextRange.start } ]
|
|
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy parentRange (moduleName ++ ".none") ]
|
|
)
|
|
)
|
|
|
|
else
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
)
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
|
|
-- PARSER
|
|
|
|
|
|
oneOfChecks : CheckInfo -> List (Error {})
|
|
oneOfChecks { parentRange, fnRange, firstArg } =
|
|
case AstHelpers.removeParens firstArg of
|
|
Node _ (Expression.ListExpr [ listElement ]) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary oneOf"
|
|
, details = [ "There is only a single element in the list of elements to try out." ]
|
|
}
|
|
fnRange
|
|
(replaceBySubExpressionFix parentRange listElement)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
type alias Collection =
|
|
{ moduleName : String
|
|
, represents : String
|
|
, emptyAsString : String
|
|
, emptyDescription : String
|
|
, isEmpty : ModuleNameLookupTable -> Node Expression -> Bool
|
|
, nameForSize : String
|
|
, determineSize : ModuleNameLookupTable -> Node Expression -> Maybe CollectionSize
|
|
}
|
|
|
|
|
|
listCollection : Collection
|
|
listCollection =
|
|
{ moduleName = "List"
|
|
, represents = "list"
|
|
, emptyAsString = "[]"
|
|
, emptyDescription = "[]"
|
|
, isEmpty = \_ -> isEmptyList
|
|
, nameForSize = "length"
|
|
, determineSize = determineListLength
|
|
}
|
|
|
|
|
|
setCollection : Collection
|
|
setCollection =
|
|
{ moduleName = "Set"
|
|
, represents = "set"
|
|
, emptyAsString = "Set.empty"
|
|
, emptyDescription = "Set.empty"
|
|
, isEmpty = isSpecificFunction [ "Set" ] "empty"
|
|
, nameForSize = "size"
|
|
, determineSize = determineIfCollectionIsEmpty [ "Set" ] 1
|
|
}
|
|
|
|
|
|
dictCollection : Collection
|
|
dictCollection =
|
|
{ moduleName = "Dict"
|
|
, represents = "Dict"
|
|
, emptyAsString = "Dict.empty"
|
|
, emptyDescription = "Dict.empty"
|
|
, isEmpty = isSpecificFunction [ "Dict" ] "empty"
|
|
, nameForSize = "size"
|
|
, determineSize = determineIfCollectionIsEmpty [ "Dict" ] 2
|
|
}
|
|
|
|
|
|
type alias Mappable =
|
|
{ moduleName : String
|
|
, represents : String
|
|
, emptyAsString : String
|
|
, emptyDescription : String
|
|
, isEmpty : ModuleNameLookupTable -> Node Expression -> Bool
|
|
}
|
|
|
|
|
|
type alias Defaultable =
|
|
{ moduleName : String
|
|
, represents : String
|
|
, emptyAsString : String
|
|
, emptyDescription : String
|
|
, isEmpty : ModuleNameLookupTable -> Node Expression -> Bool
|
|
, isSomethingConstructor : String
|
|
}
|
|
|
|
|
|
maybeCollection : Defaultable
|
|
maybeCollection =
|
|
{ moduleName = "Maybe"
|
|
, represents = "maybe"
|
|
, emptyAsString = "Nothing"
|
|
, emptyDescription = "Nothing"
|
|
, isEmpty = isSpecificFunction [ "Maybe" ] "Nothing"
|
|
, isSomethingConstructor = "Just"
|
|
}
|
|
|
|
|
|
resultCollection : Defaultable
|
|
resultCollection =
|
|
{ moduleName = "Result"
|
|
, represents = "result"
|
|
, emptyAsString = "Nothing"
|
|
, emptyDescription = "an error"
|
|
, isEmpty = isSpecificCall [ "Result" ] "Err"
|
|
, isSomethingConstructor = "Ok"
|
|
}
|
|
|
|
|
|
cmdCollection : Mappable
|
|
cmdCollection =
|
|
{ moduleName = "Cmd"
|
|
, represents = "command"
|
|
, emptyAsString = "Cmd.none"
|
|
, emptyDescription = "Cmd.none"
|
|
, isEmpty = isSpecificFunction [ "Platform", "Cmd" ] "none"
|
|
}
|
|
|
|
|
|
subCollection : Mappable
|
|
subCollection =
|
|
{ moduleName = "Sub"
|
|
, represents = "subscription"
|
|
, emptyAsString = "Sub.none"
|
|
, emptyDescription = "Sub.none"
|
|
, isEmpty = isSpecificFunction [ "Platform", "Sub" ] "none"
|
|
}
|
|
|
|
|
|
collectionMapChecks :
|
|
{ a
|
|
| moduleName : String
|
|
, represents : String
|
|
, emptyDescription : String
|
|
, emptyAsString : String
|
|
, isEmpty : ModuleNameLookupTable -> Node Expression -> Bool
|
|
}
|
|
-> CheckInfo
|
|
-> List (Error {})
|
|
collectionMapChecks collection checkInfo =
|
|
case Maybe.map (collection.isEmpty checkInfo.lookupTable) checkInfo.secondArg of
|
|
Just True ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".map on " ++ collection.emptyDescription ++ " will result in " ++ collection.emptyDescription
|
|
, details = [ "You can replace this call by " ++ collection.emptyDescription ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
if isIdentity checkInfo.lookupTable checkInfo.firstArg then
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".map with an identity function is the same as not using " ++ collection.moduleName ++ ".map"
|
|
, details = [ "You can remove this call and replace it by the " ++ collection.represents ++ " itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
maybeAndThenChecks : CheckInfo -> List (Error {})
|
|
maybeAndThenChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Match.maybeAndThen (getMaybeValues checkInfo.lookupTable) checkInfo.secondArg of
|
|
Determined (Just justRanges) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling " ++ 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.map Fix.removeRange justRanges
|
|
)
|
|
]
|
|
|
|
Determined Nothing ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ maybeCollection.moduleName ++ ".andThen on " ++ maybeCollection.emptyAsString ++ " will result in " ++ maybeCollection.emptyAsString
|
|
, details = [ "You can replace this call by " ++ maybeCollection.emptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case isAlwaysMaybe checkInfo.lookupTable checkInfo.firstArg of
|
|
Determined (Just { ranges, throughLambdaFunction }) ->
|
|
if throughLambdaFunction then
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ maybeCollection.moduleName ++ ".map instead"
|
|
, details = [ "Using " ++ maybeCollection.moduleName ++ ".andThen with a function that always returns Just is the same thing as using Maybe.map." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange (maybeCollection.moduleName ++ ".map")
|
|
:: List.map Fix.removeRange ranges
|
|
)
|
|
]
|
|
|
|
else
|
|
[ Rule.errorWithFix
|
|
{ message = "Using Maybe.andThen with a function that will always return Just is the same as not using Maybe.andThen"
|
|
, details = [ "You can remove this call and replace it by the value itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
Determined Nothing ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ 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 maybeCollection.emptyAsString checkInfo.parentRange checkInfo.secondArg)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
resultAndThenChecks : CheckInfo -> List (Error {})
|
|
resultAndThenChecks checkInfo =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case Maybe.andThen (getResultValues checkInfo.lookupTable) checkInfo.secondArg of
|
|
Just (Ok okRanges) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Calling " ++ 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.map Fix.removeRange okRanges
|
|
)
|
|
]
|
|
|
|
Just (Err _) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ resultCollection.moduleName ++ ".andThen on an error will result in the error"
|
|
, details = [ "You can replace this call by the error itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case isAlwaysResult checkInfo.lookupTable checkInfo.firstArg of
|
|
Just (Ok { ranges, throughLambdaFunction }) ->
|
|
if throughLambdaFunction then
|
|
[ Rule.errorWithFix
|
|
{ message = "Use Result.map instead"
|
|
, details = [ "Using Result.andThen with a function that always returns Ok is the same thing as using Result.map." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(Fix.replaceRangeBy checkInfo.fnRange (resultCollection.moduleName ++ ".map")
|
|
:: List.map Fix.removeRange ranges
|
|
)
|
|
]
|
|
|
|
else
|
|
[ Rule.errorWithFix
|
|
{ message = "Using Result.andThen with a function that will always return Just is the same as not using Result.andThen"
|
|
, details = [ "You can remove this call and replace it by the value itself." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
resultWithDefaultChecks : CheckInfo -> List (Error {})
|
|
resultWithDefaultChecks checkInfo =
|
|
case Maybe.andThen (getResultValues checkInfo.lookupTable) checkInfo.secondArg of
|
|
Just (Ok okRanges) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using 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.map Fix.removeRange okRanges ++ noopFix checkInfo)
|
|
]
|
|
|
|
Just (Err _) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using 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 }
|
|
]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
collectionFilterChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionFilterChecks collection ({ parentRange, fnRange, firstArg, secondArg } as checkInfo) =
|
|
case Maybe.andThen (collection.determineSize checkInfo.lookupTable) checkInfo.secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".filter on " ++ collection.emptyAsString ++ " will result in " ++ collection.emptyAsString
|
|
, details = [ "You can replace this call by " ++ collection.emptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
case Evaluate.isAlwaysBoolean checkInfo firstArg of
|
|
Determined True ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".filter with a function that will always return True is the same as not using " ++ collection.moduleName ++ ".filter"
|
|
, details = [ "You can remove this call and replace it by the " ++ collection.represents ++ " itself." ]
|
|
}
|
|
fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
Determined False ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".filter with a function that will always return False will result in " ++ collection.emptyAsString
|
|
, details = [ "You can remove this call and replace it by " ++ collection.emptyAsString ++ "." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix collection.emptyAsString parentRange secondArg)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
collectionRemoveChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionRemoveChecks collection ({ lookupTable, fnRange, secondArg } as checkInfo) =
|
|
case Maybe.andThen (collection.determineSize lookupTable) secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".remove on " ++ collection.emptyAsString ++ " will result in " ++ collection.emptyAsString
|
|
, details = [ "You can replace this call by " ++ collection.emptyAsString ++ "." ]
|
|
}
|
|
fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionIntersectChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionIntersectChecks collection { lookupTable, parentRange, fnRange, firstArg, secondArg } =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case collection.determineSize lookupTable firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".intersect on " ++ collection.emptyAsString ++ " will result in " ++ collection.emptyAsString
|
|
, details = [ "You can replace this call by " ++ collection.emptyAsString ++ "." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix collection.emptyAsString parentRange secondArg)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case Maybe.andThen (collection.determineSize lookupTable) secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".intersect on " ++ collection.emptyAsString ++ " will result in " ++ collection.emptyAsString
|
|
, details = [ "You can replace this call by " ++ collection.emptyAsString ++ "." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix collection.emptyAsString parentRange secondArg)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
collectionDiffChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionDiffChecks collection { lookupTable, parentRange, fnRange, firstArg, secondArg } =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case collection.determineSize lookupTable firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Diffing " ++ collection.emptyAsString ++ " will result in " ++ collection.emptyAsString
|
|
, details = [ "You can replace this call by " ++ collection.emptyAsString ++ "." ]
|
|
}
|
|
fnRange
|
|
(replaceByEmptyFix collection.emptyAsString parentRange secondArg)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case Maybe.andThen (collection.determineSize lookupTable) secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Diffing a " ++ collection.represents ++ " with " ++ collection.emptyAsString ++ " will result in the " ++ collection.represents ++ " itself"
|
|
, details = [ "You can replace this call by the " ++ collection.represents ++ " itself." ]
|
|
}
|
|
fnRange
|
|
[ Fix.removeRange { start = parentRange.start, end = (Node.range firstArg).start }
|
|
, Fix.removeRange { start = (Node.range firstArg).end, end = parentRange.end }
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
collectionUnionChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionUnionChecks collection ({ lookupTable, parentRange, fnRange, firstArg, secondArg } as checkInfo) =
|
|
firstThatReportsError
|
|
[ \() ->
|
|
case collection.determineSize lookupTable firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary union with Set.empty"
|
|
, details = [ "You can replace this call by the set itself." ]
|
|
}
|
|
fnRange
|
|
(noopFix checkInfo)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
, \() ->
|
|
case Maybe.andThen (collection.determineSize lookupTable) secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Unnecessary union with Set.empty"
|
|
, details = [ "You can replace this call by the set itself." ]
|
|
}
|
|
fnRange
|
|
[ Fix.removeRange { start = parentRange.start, end = (Node.range firstArg).start }
|
|
, Fix.removeRange { start = (Node.range firstArg).end, end = parentRange.end }
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
]
|
|
()
|
|
|
|
|
|
collectionInsertChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionInsertChecks collection { lookupTable, usingRightPizza, parentRange, fnRange, firstArg, secondArg } =
|
|
case Maybe.andThen (collection.determineSize lookupTable) secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Use " ++ collection.moduleName ++ ".singleton instead of inserting in " ++ collection.emptyAsString
|
|
, details = [ "You can replace this call by " ++ collection.moduleName ++ ".singleton." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy fnRange (collection.moduleName ++ ".singleton")
|
|
, if usingRightPizza then
|
|
Fix.removeRange { start = parentRange.start, end = fnRange.start }
|
|
|
|
else
|
|
Fix.removeRange { start = (Node.range firstArg).end, end = parentRange.end }
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionMemberChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionMemberChecks collection { lookupTable, parentRange, fnRange, secondArg } =
|
|
case Maybe.andThen (collection.determineSize lookupTable) secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".member on " ++ collection.emptyAsString ++ " will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
fnRange
|
|
(replaceByBoolFix parentRange secondArg False)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionIsEmptyChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionIsEmptyChecks collection { lookupTable, parentRange, fnRange, firstArg } =
|
|
case collection.determineSize lookupTable firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ collection.moduleName ++ ".isEmpty will result in True"
|
|
, details = [ "You can replace this call by True." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "True" ]
|
|
]
|
|
|
|
Just _ ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ collection.moduleName ++ ".isEmpty will result in False"
|
|
, details = [ "You can replace this call by False." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "False" ]
|
|
]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
collectionSizeChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionSizeChecks collection { lookupTable, parentRange, fnRange, firstArg } =
|
|
case collection.determineSize lookupTable 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." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange (String.fromInt size) ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionFromListChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionFromListChecks collection { parentRange, fnRange, firstArg } =
|
|
case Node.value firstArg of
|
|
Expression.ListExpr [] ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ collection.moduleName ++ ".fromList will result in " ++ collection.emptyAsString
|
|
, details = [ "You can replace this call by " ++ collection.emptyAsString ++ "." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange collection.emptyAsString ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionToListChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionToListChecks collection { lookupTable, parentRange, fnRange, firstArg } =
|
|
case collection.determineSize lookupTable firstArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "The call to " ++ collection.moduleName ++ ".toList will result in []"
|
|
, details = [ "You can replace this call by []." ]
|
|
}
|
|
fnRange
|
|
[ Fix.replaceRangeBy parentRange "[]" ]
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
collectionPartitionChecks : Collection -> CheckInfo -> List (Error {})
|
|
collectionPartitionChecks collection checkInfo =
|
|
case Maybe.andThen (collection.determineSize checkInfo.lookupTable) checkInfo.secondArg of
|
|
Just (Exactly 0) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using " ++ collection.moduleName ++ ".partition on " ++ collection.emptyAsString ++ " will result in ( " ++ collection.emptyAsString ++ ", " ++ collection.emptyAsString ++ " )"
|
|
, details = [ "You can replace this call by ( " ++ collection.emptyAsString ++ ", " ++ collection.emptyAsString ++ " )." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy checkInfo.parentRange ("( " ++ collection.emptyAsString ++ ", " ++ collection.emptyAsString ++ " )") ]
|
|
]
|
|
|
|
_ ->
|
|
case Evaluate.isAlwaysBoolean checkInfo checkInfo.firstArg of
|
|
Determined True ->
|
|
case checkInfo.secondArg of
|
|
Just listArg ->
|
|
[ 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.emptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
[ Fix.replaceRangeBy { start = checkInfo.fnRange.start, end = (Node.range listArg).start } "( "
|
|
, Fix.insertAt (Node.range listArg).end (", " ++ collection.emptyAsString ++ " )")
|
|
]
|
|
]
|
|
|
|
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.emptyAsString ++ "." ]
|
|
}
|
|
checkInfo.fnRange
|
|
(case checkInfo.secondArg of
|
|
Just listArg ->
|
|
[ Fix.replaceRangeBy { start = checkInfo.fnRange.start, end = (Node.range listArg).start } ("( " ++ collection.emptyAsString ++ ", ")
|
|
, Fix.insertAt (Node.range listArg).end " )"
|
|
]
|
|
|
|
Nothing ->
|
|
[ Fix.replaceRangeBy checkInfo.parentRange ("(Tuple.pair " ++ collection.emptyAsString ++ ")") ]
|
|
)
|
|
]
|
|
|
|
Undetermined ->
|
|
[]
|
|
|
|
|
|
maybeWithDefaultChecks : CheckInfo -> List (Error {})
|
|
maybeWithDefaultChecks checkInfo =
|
|
case Match.maybeAndThen (getMaybeValues checkInfo.lookupTable) checkInfo.secondArg of
|
|
Determined (Just justRanges) ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using 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.map Fix.removeRange justRanges ++ noopFix checkInfo)
|
|
]
|
|
|
|
Determined Nothing ->
|
|
[ Rule.errorWithFix
|
|
{ message = "Using 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 ->
|
|
[]
|
|
|
|
|
|
type CollectionSize
|
|
= Exactly Int
|
|
| NotEmpty
|
|
|
|
|
|
determineListLength : ModuleNameLookupTable -> Node Expression -> Maybe CollectionSize
|
|
determineListLength lookupTable node =
|
|
case Node.value (AstHelpers.removeParens node) 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_RENAME : ModuleNameLookupTable -> Range -> Node Expression -> Maybe (List (Error {}))
|
|
replaceSingleElementListBySingleValue_RENAME lookupTable fnRange node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.LambdaExpression { expression } ->
|
|
case replaceSingleElementListBySingleValue lookupTable expression of
|
|
Just fixes ->
|
|
Just
|
|
[ Rule.errorWithFix
|
|
{ message = "Use List.map instead"
|
|
, details = [ "The function passed to List.concatMap always returns a list with a single element." ]
|
|
}
|
|
fnRange
|
|
(Fix.replaceRangeBy fnRange "List.map" :: fixes)
|
|
]
|
|
|
|
Nothing ->
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
replaceSingleElementListBySingleValue : ModuleNameLookupTable -> Node Expression -> Maybe (List Fix)
|
|
replaceSingleElementListBySingleValue lookupTable node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.ListExpr [ listElement ] ->
|
|
Just (replaceBySubExpressionFix (Node.range node) 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 { cases } ->
|
|
combineSingleElementFixes lookupTable (List.map Tuple.second 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)
|
|
|
|
|
|
determineIfCollectionIsEmpty : ModuleName -> Int -> ModuleNameLookupTable -> Node Expression -> Maybe CollectionSize
|
|
determineIfCollectionIsEmpty moduleName singletonNumberOfArgs lookupTable node =
|
|
if isSpecificFunction moduleName "empty" lookupTable node then
|
|
Just (Exactly 0)
|
|
|
|
else
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.Application ((Node fnRange (Expression.FunctionOrValue _ "singleton")) :: args) ->
|
|
if List.length args == singletonNumberOfArgs && ModuleNameLookupTable.moduleNameAt lookupTable fnRange == Just moduleName then
|
|
Just (Exactly 1)
|
|
|
|
else
|
|
Nothing
|
|
|
|
Expression.Application ((Node fnRange (Expression.FunctionOrValue _ "fromList")) :: (Node _ (Expression.ListExpr list)) :: []) ->
|
|
if ModuleNameLookupTable.moduleNameAt lookupTable fnRange == Just moduleName then
|
|
if moduleName == [ "Set" ] then
|
|
case list of
|
|
[] ->
|
|
Just (Exactly 0)
|
|
|
|
[ _ ] ->
|
|
Just (Exactly 1)
|
|
|
|
_ ->
|
|
case traverse getComparableExpression list of
|
|
Nothing ->
|
|
Just NotEmpty
|
|
|
|
Just comparableExpressions ->
|
|
comparableExpressions |> unique |> List.length |> Exactly |> Just
|
|
|
|
else
|
|
Just (Exactly (List.length list))
|
|
|
|
else
|
|
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
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
-- 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)
|
|
[ Fix.removeRange { start = recordUpdateRange.start, end = (Node.range variable).start }
|
|
, Fix.removeRange { start = (Node.range variable).end, end = recordUpdateRange.end }
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
(Node firstRange _) :: second :: _ ->
|
|
List.filterMap
|
|
(\( Node range ( field, valueWithParens ), previousRange ) ->
|
|
let
|
|
value : Node Expression
|
|
value =
|
|
AstHelpers.removeParens valueWithParens
|
|
in
|
|
if isUnnecessaryRecordUpdateSetter variable field value then
|
|
Just
|
|
(Rule.errorWithFix
|
|
{ message = "Unnecessary field assignment"
|
|
, details = [ "The field is being set to its own value." ]
|
|
}
|
|
(Node.range value)
|
|
(case previousRange of
|
|
Just prevRange ->
|
|
[ Fix.removeRange { start = prevRange.end, end = range.end } ]
|
|
|
|
Nothing ->
|
|
-- It's the first element, so we can remove until the second element
|
|
[ Fix.removeRange { start = firstRange.start, end = (Node.range second).start } ]
|
|
)
|
|
)
|
|
|
|
else
|
|
Nothing
|
|
)
|
|
(List.map2 Tuple.pair fields (Nothing :: List.map (Node.range >> Just) fields))
|
|
|
|
|
|
isUnnecessaryRecordUpdateSetter : Node String -> Node String -> Node Expression -> Bool
|
|
isUnnecessaryRecordUpdateSetter variable field value =
|
|
case Node.value value of
|
|
Expression.RecordAccess (Node _ (Expression.FunctionOrValue [] valueHolder)) fieldName ->
|
|
Node.value field == Node.value fieldName && Node.value variable == valueHolder
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
|
|
-- IF
|
|
|
|
|
|
ifChecks :
|
|
ModuleContext
|
|
-> Range
|
|
->
|
|
{ condition : Node Expression
|
|
, trueBranch : Node Expression
|
|
, falseBranch : Node Expression
|
|
}
|
|
-> { errors : List (Error {}), rangesToIgnore : List Range, rightSidesOfPlusPlus : List Range, inferredConstants : List ( Range, Infer.Inferred ) }
|
|
ifChecks context nodeRange { condition, trueBranch, falseBranch } =
|
|
case Evaluate.getBoolean context condition of
|
|
Determined True ->
|
|
errorsAndRangesToIgnore
|
|
[ Rule.errorWithFix
|
|
{ message = "The condition will always evaluate to True"
|
|
, details = [ "The expression can be replaced by what is inside the 'then' branch." ]
|
|
}
|
|
(targetIfKeyword nodeRange)
|
|
[ Fix.removeRange
|
|
{ start = nodeRange.start
|
|
, end = (Node.range trueBranch).start
|
|
}
|
|
, Fix.removeRange
|
|
{ start = (Node.range trueBranch).end
|
|
, end = nodeRange.end
|
|
}
|
|
]
|
|
]
|
|
[ Node.range condition ]
|
|
|
|
Determined False ->
|
|
errorsAndRangesToIgnore
|
|
[ Rule.errorWithFix
|
|
{ message = "The condition will always evaluate to False"
|
|
, details = [ "The expression can be replaced by what is inside the 'else' branch." ]
|
|
}
|
|
(targetIfKeyword nodeRange)
|
|
[ Fix.removeRange
|
|
{ start = nodeRange.start
|
|
, end = (Node.range falseBranch).start
|
|
}
|
|
]
|
|
]
|
|
[ Node.range condition ]
|
|
|
|
Undetermined ->
|
|
case ( Evaluate.getBoolean context trueBranch, Evaluate.getBoolean context falseBranch ) of
|
|
( Determined True, Determined False ) ->
|
|
onlyErrors
|
|
[ Rule.errorWithFix
|
|
{ message = "The if expression's value is the same as the condition"
|
|
, details = [ "The expression can be replaced by the condition." ]
|
|
}
|
|
(targetIfKeyword nodeRange)
|
|
[ Fix.removeRange
|
|
{ start = nodeRange.start
|
|
, end = (Node.range condition).start
|
|
}
|
|
, Fix.removeRange
|
|
{ start = (Node.range condition).end
|
|
, end = nodeRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
( Determined False, Determined True ) ->
|
|
onlyErrors
|
|
[ 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 nodeRange)
|
|
[ Fix.replaceRangeBy
|
|
{ start = nodeRange.start
|
|
, end = (Node.range condition).start
|
|
}
|
|
"not ("
|
|
, Fix.replaceRangeBy
|
|
{ start = (Node.range condition).end
|
|
, end = nodeRange.end
|
|
}
|
|
")"
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
case Normalize.compare context trueBranch falseBranch of
|
|
Normalize.ConfirmedEquality ->
|
|
onlyErrors
|
|
[ Rule.errorWithFix
|
|
{ message = "The values in both branches is the same."
|
|
, details = [ "The expression can be replaced by the contents of either branch." ]
|
|
}
|
|
(targetIfKeyword nodeRange)
|
|
[ Fix.removeRange
|
|
{ start = nodeRange.start
|
|
, end = (Node.range trueBranch).start
|
|
}
|
|
, Fix.removeRange
|
|
{ start = (Node.range trueBranch).end
|
|
, end = nodeRange.end
|
|
}
|
|
]
|
|
]
|
|
|
|
_ ->
|
|
{ errors = []
|
|
, rangesToIgnore = []
|
|
, rightSidesOfPlusPlus = []
|
|
, inferredConstants =
|
|
Infer.inferForIfCondition
|
|
(Node.value (Normalize.normalize context condition))
|
|
{ trueBranchRange = Node.range trueBranch
|
|
, falseBranchRange = Node.range falseBranch
|
|
}
|
|
(Tuple.first context.inferredConstants)
|
|
}
|
|
|
|
|
|
|
|
-- CASE OF
|
|
|
|
|
|
caseOfChecks : ModuleContext -> Range -> Expression.CaseBlock -> List (Error {})
|
|
caseOfChecks context parentRange caseBlock =
|
|
firstThatReportsError
|
|
[ \() -> sameBodyForCaseOfChecks context parentRange caseBlock.cases
|
|
, \() -> booleanCaseOfChecks context.lookupTable parentRange caseBlock
|
|
, \() -> destructuringCaseOfChecks context.extractSourceCode parentRange caseBlock
|
|
]
|
|
()
|
|
|
|
|
|
sameBodyForCaseOfChecks : ModuleContext -> Range -> List ( Node Pattern, Node Expression ) -> List (Error {})
|
|
sameBodyForCaseOfChecks context parentRange cases =
|
|
case 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 parentRange)
|
|
[ Fix.removeRange { start = parentRange.start, end = firstBodyRange.start }
|
|
, Fix.removeRange { start = firstBodyRange.end, end = parentRange.end }
|
|
]
|
|
]
|
|
|
|
|
|
caseKeyWordRange : Range -> Range
|
|
caseKeyWordRange range =
|
|
{ start = range.start
|
|
, end = { row = range.start.row, column = range.start.column + 4 }
|
|
}
|
|
|
|
|
|
introducesVariableOrUsesTypeConstructor : ModuleContext -> List (Node Pattern) -> Bool
|
|
introducesVariableOrUsesTypeConstructor context 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 context (pattern :: remaining)
|
|
|
|
Pattern.TuplePattern nodes ->
|
|
introducesVariableOrUsesTypeConstructor context (nodes ++ remaining)
|
|
|
|
Pattern.UnConsPattern first rest ->
|
|
introducesVariableOrUsesTypeConstructor context (first :: rest :: remaining)
|
|
|
|
Pattern.ListPattern nodes ->
|
|
introducesVariableOrUsesTypeConstructor context (nodes ++ remaining)
|
|
|
|
Pattern.NamedPattern { name } nodes ->
|
|
case ModuleNameLookupTable.fullModuleNameFor context.lookupTable node of
|
|
Just moduleName ->
|
|
if Set.member ( moduleName, name ) context.customTypesToReportInCases then
|
|
introducesVariableOrUsesTypeConstructor context (nodes ++ remaining)
|
|
|
|
else
|
|
True
|
|
|
|
Nothing ->
|
|
True
|
|
|
|
_ ->
|
|
introducesVariableOrUsesTypeConstructor context remaining
|
|
|
|
|
|
booleanCaseOfChecks : ModuleNameLookupTable -> Range -> Expression.CaseBlock -> List (Error {})
|
|
booleanCaseOfChecks lookupTable parentRange { expression, cases } =
|
|
case cases of
|
|
[ ( firstPattern, Node firstRange _ ), ( Node secondPatternRange _, Node secondExprRange _ ) ] ->
|
|
case getBooleanPattern lookupTable firstPattern of
|
|
Just isTrueFirst ->
|
|
[ 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 = parentRange.start, end = (Node.range expression).start } "if "
|
|
, Fix.replaceRangeBy { start = (Node.range expression).end, end = firstRange.start } " then "
|
|
, Fix.replaceRangeBy { start = secondPatternRange.start, end = secondExprRange.start } "else "
|
|
]
|
|
|
|
else
|
|
[ Fix.replaceRangeBy { start = parentRange.start, end = (Node.range expression).start } "if not ("
|
|
, Fix.replaceRangeBy { start = (Node.range expression).end, end = firstRange.start } ") then "
|
|
, Fix.replaceRangeBy { start = secondPatternRange.start, end = secondExprRange.start } "else "
|
|
]
|
|
)
|
|
]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
destructuringCaseOfChecks : (Range -> String) -> Range -> Expression.CaseBlock -> List (Error {})
|
|
destructuringCaseOfChecks extractSourceCode parentRange { expression, cases } =
|
|
case cases of
|
|
[ ( rawSinglePattern, Node bodyRange _ ) ] ->
|
|
let
|
|
singlePattern : Node Pattern
|
|
singlePattern =
|
|
AstHelpers.removeParensFromPattern rawSinglePattern
|
|
in
|
|
if isSimpleDestructurePattern singlePattern then
|
|
let
|
|
exprRange : Range
|
|
exprRange =
|
|
Node.range expression
|
|
|
|
caseIndentation : String
|
|
caseIndentation =
|
|
String.repeat (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 = parentRange.start, end = exprRange.start } ("let " ++ extractSourceCode (Node.range singlePattern) ++ " = ")
|
|
, Fix.replaceRangeBy { start = exprRange.end, end = bodyRange.start } ("\n" ++ caseIndentation ++ "in\n" ++ bodyIndentation)
|
|
]
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
_ ->
|
|
[]
|
|
|
|
|
|
isSimpleDestructurePattern : Node Pattern -> Bool
|
|
isSimpleDestructurePattern pattern =
|
|
case Node.value pattern of
|
|
Pattern.TuplePattern _ ->
|
|
True
|
|
|
|
Pattern.RecordPattern _ ->
|
|
True
|
|
|
|
Pattern.VarPattern _ ->
|
|
True
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
isSpecificFunction : ModuleName -> String -> ModuleNameLookupTable -> Node Expression -> Bool
|
|
isSpecificFunction moduleName fnName lookupTable node =
|
|
case AstHelpers.removeParens node of
|
|
Node noneRange (Expression.FunctionOrValue _ foundFnName) ->
|
|
(foundFnName == fnName)
|
|
&& (ModuleNameLookupTable.moduleNameAt lookupTable noneRange == Just moduleName)
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
isSpecificCall : ModuleName -> String -> ModuleNameLookupTable -> Node Expression -> Bool
|
|
isSpecificCall moduleName fnName lookupTable node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.Application ((Node noneRange (Expression.FunctionOrValue _ foundFnName)) :: _ :: []) ->
|
|
(foundFnName == fnName)
|
|
&& (ModuleNameLookupTable.moduleNameAt lookupTable noneRange == Just moduleName)
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
getUncomputedNumberValue : Node Expression -> Maybe Float
|
|
getUncomputedNumberValue node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.Integer n ->
|
|
Just (toFloat n)
|
|
|
|
Expression.Hex n ->
|
|
Just (toFloat n)
|
|
|
|
Expression.Floatable n ->
|
|
Just n
|
|
|
|
Expression.Negation expr ->
|
|
Maybe.map negate (getUncomputedNumberValue expr)
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
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 lastElementRange letBlock.declarations of
|
|
Just 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 }
|
|
}
|
|
|
|
|
|
lastElementRange : List (Node a) -> Maybe Range
|
|
lastElementRange nodes =
|
|
case nodes of
|
|
[] ->
|
|
Nothing
|
|
|
|
last :: [] ->
|
|
Just (Node.range last)
|
|
|
|
_ :: rest ->
|
|
lastElementRange rest
|
|
|
|
|
|
|
|
-- FIX HELPERS
|
|
|
|
|
|
removeFunctionFromFunctionCall : { a | fnRange : Range, firstArg : Node b, usingRightPizza : Bool } -> Fix
|
|
removeFunctionFromFunctionCall { fnRange, firstArg, usingRightPizza } =
|
|
if usingRightPizza then
|
|
Fix.removeRange { start = (Node.range firstArg).end, end = fnRange.end }
|
|
|
|
else
|
|
Fix.removeRange { start = fnRange.start, end = (Node.range firstArg).start }
|
|
|
|
|
|
removeFunctionAndFirstArg : { a | fnRange : Range, firstArg : Node b, usingRightPizza : Bool } -> Range -> Fix
|
|
removeFunctionAndFirstArg { fnRange, firstArg, usingRightPizza } secondArgRange =
|
|
if usingRightPizza then
|
|
Fix.removeRange { start = secondArgRange.end, end = (Node.range firstArg).end }
|
|
|
|
else
|
|
Fix.removeRange { start = fnRange.start, end = secondArgRange.start }
|
|
|
|
|
|
removeBoundariesFix : Node a -> List Fix
|
|
removeBoundariesFix node =
|
|
let
|
|
{ start, end } =
|
|
Node.range node
|
|
in
|
|
[ Fix.removeRange
|
|
{ start = { row = start.row, column = start.column }
|
|
, end = { row = start.row, column = start.column + 1 }
|
|
}
|
|
, Fix.removeRange
|
|
{ start = { row = end.row, column = end.column - 1 }
|
|
, end = { row = end.row, column = end.column }
|
|
}
|
|
]
|
|
|
|
|
|
noopFix : CheckInfo -> List Fix
|
|
noopFix { fnRange, parentRange, secondArg, usingRightPizza } =
|
|
[ case secondArg of
|
|
Just listArg ->
|
|
if usingRightPizza then
|
|
Fix.removeRange { start = (Node.range listArg).end, end = parentRange.end }
|
|
|
|
else
|
|
Fix.removeRange { start = fnRange.start, end = (Node.range listArg).start }
|
|
|
|
Nothing ->
|
|
Fix.replaceRangeBy parentRange "identity"
|
|
]
|
|
|
|
|
|
replaceByEmptyFix : String -> Range -> Maybe a -> List Fix
|
|
replaceByEmptyFix empty parentRange secondArg =
|
|
[ case secondArg of
|
|
Just _ ->
|
|
Fix.replaceRangeBy parentRange empty
|
|
|
|
Nothing ->
|
|
Fix.replaceRangeBy parentRange ("(always " ++ empty ++ ")")
|
|
]
|
|
|
|
|
|
replaceByBoolFix : Range -> Maybe a -> Bool -> List Fix
|
|
replaceByBoolFix parentRange secondArg replacementValue =
|
|
[ case secondArg of
|
|
Just _ ->
|
|
Fix.replaceRangeBy parentRange (boolToString replacementValue)
|
|
|
|
Nothing ->
|
|
Fix.replaceRangeBy parentRange ("(always " ++ boolToString replacementValue ++ ")")
|
|
]
|
|
|
|
|
|
boolToString : Bool -> String
|
|
boolToString bool =
|
|
if bool then
|
|
"True"
|
|
|
|
else
|
|
"False"
|
|
|
|
|
|
|
|
-- MATCHERS
|
|
|
|
|
|
isIdentity : ModuleNameLookupTable -> Node Expression -> Bool
|
|
isIdentity lookupTable baseNode =
|
|
let
|
|
node : Node Expression
|
|
node =
|
|
AstHelpers.removeParens baseNode
|
|
in
|
|
case Node.value node of
|
|
Expression.FunctionOrValue _ "identity" ->
|
|
ModuleNameLookupTable.moduleNameFor lookupTable node == Just [ "Basics" ]
|
|
|
|
Expression.LambdaExpression { args, expression } ->
|
|
case args of
|
|
arg :: [] ->
|
|
case getVarPattern arg of
|
|
Just patternName ->
|
|
case getExpressionName expression of
|
|
Just expressionName ->
|
|
patternName == expressionName
|
|
|
|
_ ->
|
|
False
|
|
|
|
_ ->
|
|
False
|
|
|
|
_ ->
|
|
False
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
getVarPattern : Node Pattern -> Maybe String
|
|
getVarPattern node =
|
|
case Node.value node of
|
|
Pattern.VarPattern name ->
|
|
Just name
|
|
|
|
Pattern.ParenthesizedPattern pattern ->
|
|
getVarPattern pattern
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
getExpressionName : Node Expression -> Maybe String
|
|
getExpressionName node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.FunctionOrValue [] name ->
|
|
Just name
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
isListLiteral : Node Expression -> Bool
|
|
isListLiteral node =
|
|
case Node.value node of
|
|
Expression.ListExpr _ ->
|
|
True
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
getBooleanPattern : ModuleNameLookupTable -> Node Pattern -> Maybe Bool
|
|
getBooleanPattern lookupTable node =
|
|
case Node.value node of
|
|
Pattern.NamedPattern { name } _ ->
|
|
case name of
|
|
"True" ->
|
|
if ModuleNameLookupTable.moduleNameFor lookupTable node == Just [ "Basics" ] then
|
|
Just True
|
|
|
|
else
|
|
Nothing
|
|
|
|
"False" ->
|
|
if ModuleNameLookupTable.moduleNameFor lookupTable node == Just [ "Basics" ] then
|
|
Just False
|
|
|
|
else
|
|
Nothing
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Pattern.ParenthesizedPattern pattern ->
|
|
getBooleanPattern lookupTable pattern
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
isAlwaysMaybe : ModuleNameLookupTable -> Node Expression -> Match (Maybe { ranges : List Range, throughLambdaFunction : Bool })
|
|
isAlwaysMaybe lookupTable baseNode =
|
|
let
|
|
node : Node Expression
|
|
node =
|
|
AstHelpers.removeParens baseNode
|
|
in
|
|
case Node.value node of
|
|
Expression.FunctionOrValue _ "Just" ->
|
|
case ModuleNameLookupTable.moduleNameFor lookupTable node of
|
|
Just [ "Maybe" ] ->
|
|
Determined (Just { ranges = [ Node.range node ], throughLambdaFunction = False })
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
Expression.Application ((Node alwaysRange (Expression.FunctionOrValue _ "always")) :: value :: []) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable alwaysRange of
|
|
Just [ "Basics" ] ->
|
|
getMaybeValues lookupTable value
|
|
|> Match.map (Maybe.map (\ranges -> { ranges = ranges, throughLambdaFunction = False }))
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
Expression.LambdaExpression { expression } ->
|
|
getMaybeValues lookupTable expression
|
|
|> Match.map (Maybe.map (\ranges -> { ranges = ranges, throughLambdaFunction = True }))
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
|
|
getMaybeValues : ModuleNameLookupTable -> Node Expression -> Match (Maybe (List Range))
|
|
getMaybeValues lookupTable baseNode =
|
|
let
|
|
node : Node Expression
|
|
node =
|
|
AstHelpers.removeParens baseNode
|
|
in
|
|
case Node.value node of
|
|
Expression.Application ((Node justRange (Expression.FunctionOrValue _ "Just")) :: arg :: []) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Maybe" ] ->
|
|
Determined (Just [ { start = justRange.start, end = (Node.range arg).start } ])
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
Expression.OperatorApplication "|>" _ arg (Node justRange (Expression.FunctionOrValue _ "Just")) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Maybe" ] ->
|
|
Determined (Just [ { start = (Node.range arg).end, end = justRange.end } ])
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
Expression.OperatorApplication "<|" _ (Node justRange (Expression.FunctionOrValue _ "Just")) arg ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Maybe" ] ->
|
|
Determined (Just [ { start = justRange.start, end = (Node.range arg).start } ])
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
Expression.FunctionOrValue _ "Nothing" ->
|
|
case ModuleNameLookupTable.moduleNameFor lookupTable node of
|
|
Just [ "Maybe" ] ->
|
|
Determined Nothing
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
Expression.LetExpression { expression } ->
|
|
getMaybeValues lookupTable expression
|
|
|
|
Expression.ParenthesizedExpression expression ->
|
|
getMaybeValues lookupTable expression
|
|
|
|
Expression.IfBlock _ thenBranch elseBranch ->
|
|
combineMaybeValues lookupTable [ thenBranch, elseBranch ]
|
|
|
|
Expression.CaseExpression { cases } ->
|
|
combineMaybeValues lookupTable (List.map Tuple.second cases)
|
|
|
|
_ ->
|
|
Undetermined
|
|
|
|
|
|
combineMaybeValues : ModuleNameLookupTable -> List (Node Expression) -> Match (Maybe (List Range))
|
|
combineMaybeValues lookupTable nodes =
|
|
case nodes of
|
|
node :: restOfNodes ->
|
|
case getMaybeValues lookupTable node of
|
|
Undetermined ->
|
|
Undetermined
|
|
|
|
Determined nodeValue ->
|
|
combineMaybeValuesHelp lookupTable restOfNodes nodeValue
|
|
|
|
[] ->
|
|
Undetermined
|
|
|
|
|
|
combineMaybeValuesHelp : ModuleNameLookupTable -> List (Node Expression) -> Maybe (List Range) -> Match (Maybe (List Range))
|
|
combineMaybeValuesHelp lookupTable nodes soFar =
|
|
case nodes of
|
|
node :: restOfNodes ->
|
|
case getMaybeValues lookupTable node of
|
|
Undetermined ->
|
|
Undetermined
|
|
|
|
Determined nodeValue ->
|
|
case ( nodeValue, soFar ) of
|
|
( Just _, Nothing ) ->
|
|
Undetermined
|
|
|
|
( Nothing, Just _ ) ->
|
|
Undetermined
|
|
|
|
( Nothing, Nothing ) ->
|
|
combineMaybeValuesHelp lookupTable restOfNodes Nothing
|
|
|
|
( Just a, Just b ) ->
|
|
combineMaybeValuesHelp lookupTable restOfNodes (Just (a ++ b))
|
|
|
|
[] ->
|
|
Determined soFar
|
|
|
|
|
|
isAlwaysResult : ModuleNameLookupTable -> Node Expression -> Maybe (Result Range { ranges : List Range, throughLambdaFunction : Bool })
|
|
isAlwaysResult lookupTable baseNode =
|
|
let
|
|
node : Node Expression
|
|
node =
|
|
AstHelpers.removeParens baseNode
|
|
in
|
|
case Node.value node of
|
|
Expression.FunctionOrValue _ "Ok" ->
|
|
case ModuleNameLookupTable.moduleNameFor lookupTable node of
|
|
Just [ "Result" ] ->
|
|
Just (Ok { ranges = [ Node.range node ], throughLambdaFunction = False })
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.FunctionOrValue _ "Err" ->
|
|
case ModuleNameLookupTable.moduleNameFor lookupTable node of
|
|
Just [ "Result" ] ->
|
|
Just (Err (Node.range node))
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.Application ((Node alwaysRange (Expression.FunctionOrValue _ "always")) :: value :: []) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable alwaysRange of
|
|
Just [ "Basics" ] ->
|
|
getResultValues lookupTable value
|
|
|> Maybe.map (Result.map (\ranges -> { ranges = ranges, throughLambdaFunction = False }))
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.LambdaExpression { expression } ->
|
|
getResultValues lookupTable expression
|
|
|> Maybe.map (Result.map (\ranges -> { ranges = ranges, throughLambdaFunction = True }))
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
getResultValues : ModuleNameLookupTable -> Node Expression -> Maybe (Result Range (List Range))
|
|
getResultValues lookupTable baseNode =
|
|
let
|
|
node : Node Expression
|
|
node =
|
|
AstHelpers.removeParens baseNode
|
|
in
|
|
case Node.value node of
|
|
Expression.Application ((Node justRange (Expression.FunctionOrValue _ "Ok")) :: arg :: []) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Result" ] ->
|
|
Just (Ok [ { start = justRange.start, end = (Node.range arg).start } ])
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.OperatorApplication "|>" _ arg (Node justRange (Expression.FunctionOrValue _ "Ok")) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Result" ] ->
|
|
Just (Ok [ { start = (Node.range arg).end, end = justRange.end } ])
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.OperatorApplication "<|" _ (Node justRange (Expression.FunctionOrValue _ "Ok")) arg ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Result" ] ->
|
|
Just (Ok [ { start = justRange.start, end = (Node.range arg).start } ])
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.Application ((Node justRange (Expression.FunctionOrValue _ "Err")) :: arg :: []) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Result" ] ->
|
|
Just (Err { start = justRange.start, end = (Node.range arg).start })
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.OperatorApplication "|>" _ arg (Node justRange (Expression.FunctionOrValue _ "Err")) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Result" ] ->
|
|
Just (Err { start = (Node.range arg).end, end = justRange.end })
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.OperatorApplication "<|" _ (Node justRange (Expression.FunctionOrValue _ "Err")) arg ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable justRange of
|
|
Just [ "Result" ] ->
|
|
Just (Err { start = justRange.start, end = (Node.range arg).start })
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
Expression.LetExpression { expression } ->
|
|
getResultValues lookupTable expression
|
|
|
|
Expression.ParenthesizedExpression expression ->
|
|
getResultValues lookupTable expression
|
|
|
|
Expression.IfBlock _ thenBranch elseBranch ->
|
|
combineResultValues lookupTable [ thenBranch, elseBranch ]
|
|
|
|
Expression.CaseExpression { cases } ->
|
|
combineResultValues lookupTable (List.map Tuple.second cases)
|
|
|
|
_ ->
|
|
Nothing
|
|
|
|
|
|
combineResultValues : ModuleNameLookupTable -> List (Node Expression) -> Maybe (Result Range (List Range))
|
|
combineResultValues lookupTable nodes =
|
|
case nodes of
|
|
node :: restOfNodes ->
|
|
case getResultValues lookupTable node of
|
|
Nothing ->
|
|
Nothing
|
|
|
|
Just nodeValue ->
|
|
combineResultValuesHelp lookupTable restOfNodes nodeValue
|
|
|
|
[] ->
|
|
Nothing
|
|
|
|
|
|
combineResultValuesHelp : ModuleNameLookupTable -> List (Node Expression) -> Result Range (List Range) -> Maybe (Result Range (List Range))
|
|
combineResultValuesHelp lookupTable nodes soFar =
|
|
case nodes of
|
|
node :: restOfNodes ->
|
|
case getResultValues lookupTable node of
|
|
Nothing ->
|
|
Nothing
|
|
|
|
Just nodeValue ->
|
|
case ( nodeValue, soFar ) of
|
|
( Ok _, Err _ ) ->
|
|
Nothing
|
|
|
|
( Err _, Ok _ ) ->
|
|
Nothing
|
|
|
|
( Err _, Err soFarRange ) ->
|
|
combineResultValuesHelp lookupTable restOfNodes (Err soFarRange)
|
|
|
|
( Ok a, Ok b ) ->
|
|
combineResultValuesHelp lookupTable restOfNodes (Ok (a ++ b))
|
|
|
|
[] ->
|
|
Just soFar
|
|
|
|
|
|
isAlwaysEmptyList : ModuleNameLookupTable -> Node Expression -> Bool
|
|
isAlwaysEmptyList lookupTable node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.Application ((Node alwaysRange (Expression.FunctionOrValue _ "always")) :: alwaysValue :: []) ->
|
|
case ModuleNameLookupTable.moduleNameAt lookupTable alwaysRange of
|
|
Just [ "Basics" ] ->
|
|
isEmptyList alwaysValue
|
|
|
|
_ ->
|
|
False
|
|
|
|
Expression.LambdaExpression { expression } ->
|
|
isEmptyList expression
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
isEmptyList : Node Expression -> Bool
|
|
isEmptyList node =
|
|
case Node.value (AstHelpers.removeParens node) of
|
|
Expression.ListExpr [] ->
|
|
True
|
|
|
|
_ ->
|
|
False
|