elm-review/tests/Simplify.elm
2022-08-22 21:49:08 +02:00

5551 lines
197 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
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
not (not x)
--> x
not >> not
--> identity
### 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
### 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
case value of
A _ -> x
B -> x
--> x
### Record updates
{ a | b = a.b }
--> a
{ a | b = a.b, c = 1 }
--> { a | c = 1 }
### 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 list
--> list
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
--> ( list, Set.empty )
### Dict
Dict.isEmpty Dict.empty
--> True
Dict.fromList []
--> Dict.empty
Dict.toList Dict.empty
--> []
Dict.size Dict.empty
--> 0
### 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.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Expression as Expression exposing (Expression)
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 Elm.Type
import Json.Decode as Decode
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.Match as Match exposing (Match(..))
import Simplify.Normalize as Normalize
{-| Rule to simplify Elm code.
-}
rule : Configuration -> Rule
rule (Configuration config) =
case parseTypeNames config.ignoreConstructors of
Ok [] ->
Rule.newModuleRuleSchemaUsingContextCreator "Simplify" initialModuleContext
|> moduleVisitor Set.empty
|> Rule.fromModuleRuleSchema
Ok typeNamesList ->
let
typeNames : Set ( ModuleName, String )
typeNames =
Set.fromList typeNamesList
in
Rule.newProjectRuleSchema "Simplify" initialContext
|> Rule.withDirectDependenciesProjectVisitor (dependenciesVisitor typeNames)
|> Rule.withModuleVisitor (moduleVisitor typeNames)
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.withContextFromImportedModules
|> Rule.withFinalProjectEvaluation (finalEvaluation config.ignoreConstructors)
|> Rule.fromProjectRuleSchema
Err invalidTypes ->
Rule.configurationError "Simplify"
{ message = "Invalid type names: " ++ (invalidTypes |> List.map wrapInBackticks |> String.join ", ")
, details =
[ "I expect valid type names to be passed to Simplify.ignoreCaseOfForTypes, that include the module name, like `Module.Name.TypeName`."
]
}
moduleVisitor : Set ( ModuleName, String ) -> Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor typeNames schema =
schema
|> Rule.withDeclarationEnterVisitor declarationVisitor
|> Rule.withExpressionEnterVisitor expressionVisitor
|> addDeclarationListVisitor typeNames
addDeclarationListVisitor : Set ( ModuleName, String ) -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
addDeclarationListVisitor typeNames schema =
if Set.isEmpty typeNames then
schema
else
Rule.withDeclarationListVisitor (declarationListVisitor typeNames) schema
-- 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 used in case expressions.
This rule simplifies the following construct:
module Module.Name exposing (..)
type Type = A | B
case value of
A -> x
B -> x
--> x
In some cases, you may want to disable this simplification because you expect to change or add constructors to this custom type.
Keeping the case expression as it is will make the compiler remind you to update this code when you add new variants, which can be valuable.
Using the following configuration, case of expressions where all variants of the `Type` custom type
from the `Module.Name` module appear will not be simplified.
config =
[ Simplify.defaults
|> Simplify.ignoreCaseOfForTypes [ "Module.Name.Type" ]
|> Simplify.rule
]
Note that if you use a wildcard, you will still get the simplification, since in this case the compiler will
not remind you anyway.
case value of
A -> x
_ -> x
--> x
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).
Only use it for custom types that you think will change soon. When using it, I recommend not keeping it there too long.
Come back after a while to see if this exception is still worth having. Maybe add a comment with the date and an
explanation next to each exception?
-}
ignoreCaseOfForTypes : List String -> Configuration -> Configuration
ignoreCaseOfForTypes ignoreConstructors (Configuration config) =
Configuration { config | ignoreConstructors = ignoreConstructors ++ config.ignoreConstructors }
parseTypeNames : List String -> Result (List String) (List ( ModuleName, String ))
parseTypeNames strings =
let
parsedTypeNames : List (Result String ( ModuleName, String ))
parsedTypeNames =
List.map isValidType strings
invalidTypeNames : List String
invalidTypeNames =
List.filterMap
(\result ->
case result of
Err typeName ->
Just typeName
Ok _ ->
Nothing
)
parsedTypeNames
in
if List.isEmpty invalidTypeNames then
parsedTypeNames
|> List.filterMap Result.toMaybe
|> Ok
else
Err invalidTypeNames
isValidType : String -> Result String ( ModuleName, String )
isValidType typeAsString =
case Decode.decodeString Elm.Type.decoder ("\"" ++ typeAsString ++ "\"") of
Ok (Elm.Type.Type name _) ->
case List.reverse <| String.split "." name of
functionName :: moduleName :: restOfModuleName ->
Ok ( List.reverse (moduleName :: restOfModuleName), functionName )
_ ->
Err typeAsString
_ ->
Err typeAsString
-- CONTEXT
type alias ProjectContext =
{ ignoredCustomTypes : List Constructor
}
type alias ModuleContext =
{ lookupTable : ModuleNameLookupTable
, moduleName : ModuleName
, rangesToIgnore : List Range
, rightSidesOfPlusPlus : List Range
, ignoredCustomTypes : List Constructor
, localIgnoredCustomTypes : List Constructor
, constructorsToIgnore : Set ( ModuleName, String )
}
type alias Constructor =
{ moduleName : ModuleName
, name : String
, constructors : List String
}
initialContext : ProjectContext
initialContext =
{ ignoredCustomTypes = []
}
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
fromModuleToProject =
Rule.initContextCreator
(\moduleContext ->
{ ignoredCustomTypes = moduleContext.localIgnoredCustomTypes
}
)
initialModuleContext : Rule.ContextCreator () ModuleContext
initialModuleContext =
Rule.initContextCreator
(\lookupTable moduleName () ->
{ lookupTable = lookupTable
, moduleName = moduleName
, rangesToIgnore = []
, rightSidesOfPlusPlus = []
, localIgnoredCustomTypes = []
, ignoredCustomTypes = []
, constructorsToIgnore = Set.empty
}
)
|> Rule.withModuleNameLookupTable
|> Rule.withModuleName
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
fromProjectToModule =
Rule.initContextCreator
(\lookupTable moduleName projectContext ->
{ lookupTable = lookupTable
, moduleName = moduleName
, rangesToIgnore = []
, rightSidesOfPlusPlus = []
, localIgnoredCustomTypes = []
, ignoredCustomTypes = projectContext.ignoredCustomTypes
, constructorsToIgnore = buildConstructorsToIgnore projectContext.ignoredCustomTypes
}
)
|> Rule.withModuleNameLookupTable
|> Rule.withModuleName
buildConstructorsToIgnore : List Constructor -> Set ( ModuleName, String )
buildConstructorsToIgnore constructors =
constructors
|> List.concatMap (\c -> List.map (Tuple.pair c.moduleName) c.constructors)
|> Set.fromList
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts newContext previousContext =
{ ignoredCustomTypes = newContext.ignoredCustomTypes ++ previousContext.ignoredCustomTypes
}
-- DEPENDENCIES VISITOR
dependenciesVisitor : Set ( ModuleName, String ) -> Dict String Dependency -> ProjectContext -> ( List nothing, ProjectContext )
dependenciesVisitor typeNames dict _ =
( []
, { ignoredCustomTypes =
dict
|> Dict.values
|> List.concatMap Dependency.modules
|> List.concatMap
(\mod ->
let
moduleName : ModuleName
moduleName =
String.split "." mod.name
in
mod.unions
|> List.filter (\{ name } -> Set.member ( moduleName, name ) typeNames)
|> List.map
(\union ->
{ moduleName = moduleName
, name = union.name
, constructors = List.map Tuple.first union.tags
}
)
)
}
)
-- FINAL EVALUATION
finalEvaluation : List String -> ProjectContext -> List (Error { useErrorForModule : () })
finalEvaluation ignoreConstructors projectContext =
let
list : List String
list =
projectContext.ignoredCustomTypes
|> List.map (\type_ -> String.join "." type_.moduleName ++ "." ++ type_.name)
|> Set.fromList
|> Set.diff (Set.fromList ignoreConstructors)
|> Set.toList
in
if List.isEmpty list then
[]
else
[ Rule.globalError
{ message = "Could not find type names: " ++ (String.join ", " <| List.map wrapInBackticks list)
, details =
[ "I expected to find these custom types in the code or 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."
]
}
]
wrapInBackticks : String -> String
wrapInBackticks s =
"`" ++ s ++ "`"
-- DECLARATION LIST VISITOR
declarationListVisitor : Set ( ModuleName, String ) -> List (Node Declaration) -> ModuleContext -> ( List nothing, ModuleContext )
declarationListVisitor constructorsToIgnore declarations context =
let
localIgnoredCustomTypes : List Constructor
localIgnoredCustomTypes =
List.filterMap (findCustomTypes constructorsToIgnore context) declarations
in
( []
, { context
| localIgnoredCustomTypes = localIgnoredCustomTypes
, ignoredCustomTypes = localIgnoredCustomTypes ++ context.ignoredCustomTypes
, constructorsToIgnore = Set.union (buildConstructorsToIgnore localIgnoredCustomTypes) context.constructorsToIgnore
}
)
findCustomTypes : Set ( ModuleName, String ) -> ModuleContext -> Node Declaration -> Maybe Constructor
findCustomTypes constructorsToIgnore context node =
case Node.value node of
Declaration.CustomTypeDeclaration { name, constructors } ->
if Set.member ( context.moduleName, Node.value name ) constructorsToIgnore then
Just
{ moduleName = context.moduleName
, name = Node.value name
, constructors = List.map (\constructor -> constructor |> Node.value |> .name |> Node.value) constructors
}
else
Nothing
_ ->
Nothing
-- DECLARATION VISITOR
declarationVisitor : Node a -> ModuleContext -> ( List nothing, ModuleContext )
declarationVisitor _ context =
( [], { context | rangesToIgnore = [], rightSidesOfPlusPlus = [] } )
-- EXPRESSION VISITOR
expressionVisitor : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
expressionVisitor node context =
if List.member (Node.range node) context.rangesToIgnore then
( [], context )
else
let
{ errors, rangesToIgnore, rightSidesOfPlusPlus } =
expressionVisitorHelp node context
in
( errors
, { context
| rangesToIgnore = rangesToIgnore ++ context.rangesToIgnore
, rightSidesOfPlusPlus =
rightSidesOfPlusPlus ++ context.rightSidesOfPlusPlus
}
)
errorsAndRangesToIgnore : List (Error {}) -> List Range -> { errors : List (Error {}), rangesToIgnore : List Range, rightSidesOfPlusPlus : List Range }
errorsAndRangesToIgnore errors rangesToIgnore =
{ errors = errors
, rangesToIgnore = rangesToIgnore
, rightSidesOfPlusPlus = []
}
onlyErrors : List (Error {}) -> { errors : List (Error {}), rangesToIgnore : List Range, rightSidesOfPlusPlus : List Range }
onlyErrors errors =
{ errors = errors
, rangesToIgnore = []
, rightSidesOfPlusPlus = []
}
expressionVisitorHelp : Node Expression -> ModuleContext -> { errors : List (Error {}), rangesToIgnore : List Range, rightSidesOfPlusPlus : List Range }
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
, 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 cond trueBranch falseBranch ->
case getBoolean context.lookupTable cond of
Determined True ->
onlyErrors
[ Rule.errorWithFix
{ message = "The condition will always evaluate to True"
, details = [ "The expression can be replaced by what is inside the 'then' branch." ]
}
(targetIf node)
[ Fix.removeRange
{ start = (Node.range node).start
, end = (Node.range trueBranch).start
}
, Fix.removeRange
{ start = (Node.range trueBranch).end
, end = (Node.range node).end
}
]
]
Determined False ->
onlyErrors
[ Rule.errorWithFix
{ message = "The condition will always evaluate to False"
, details = [ "The expression can be replaced by what is inside the 'else' branch." ]
}
(targetIf node)
[ Fix.removeRange
{ start = (Node.range node).start
, end = (Node.range falseBranch).start
}
]
]
Undetermined ->
case ( getBoolean context.lookupTable trueBranch, getBoolean context.lookupTable 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." ]
}
(targetIf node)
[ Fix.removeRange
{ start = (Node.range node).start
, end = (Node.range cond).start
}
, Fix.removeRange
{ start = (Node.range cond).end
, end = (Node.range node).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`." ]
}
(targetIf node)
[ Fix.replaceRangeBy
{ start = (Node.range node).start
, end = (Node.range cond).start
}
"not ("
, Fix.replaceRangeBy
{ start = (Node.range cond).end
, end = (Node.range node).end
}
")"
]
]
_ ->
case Normalize.compare context.lookupTable 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." ]
}
(targetIf node)
[ Fix.removeRange
{ start = (Node.range node).start
, end = (Node.range trueBranch).start
}
, Fix.removeRange
{ start = (Node.range trueBranch).end
, end = (Node.range node).end
}
]
]
Normalize.ConfirmedInequality ->
onlyErrors []
Normalize.Unconfirmed ->
onlyErrors []
-------------------------------
-- 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)
----------
-- (<|) --
----------
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
, 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
, 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
, 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
, 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
, 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 <| removeParens right ]
else
[]
}
Nothing ->
onlyErrors []
Expression.Negation baseExpr ->
case removeParens baseExpr of
Node range (Expression.Negation _) ->
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
[ Fix.replaceRangeBy doubleNegationRange "(" ]
]
_ ->
onlyErrors []
_ ->
onlyErrors []
type alias CheckInfo =
{ lookupTable : ModuleNameLookupTable
, 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 )
, ( ( [ "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 )
, ( ( [ "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 )
, ( ( [ "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
, 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 (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
[ 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
[]
find : (a -> Bool) -> List a -> Maybe a
find predicate nodes =
case nodes of
[] ->
Nothing
node :: rest ->
if predicate node then
Just node
else
find predicate rest
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 [ _ ], _ ) ->
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.start
, end = { row = leftRange.start.row, column = leftRange.start.column + 1 }
}
"("
, Fix.replaceRangeBy
{ start = { row = leftRange.end.row, column = leftRange.end.column - 1 }
, end = rightRange.start
}
") :: "
]
]
_ ->
[]
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 (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 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 getBoolean checkInfo.lookupTable 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 (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.lookupTable
operatorCheckInfo.operator
nodeToCompareTo
)
conditionsOnTheRight
in
operatorCheckInfo.left
|> listConditions operatorCheckInfo.operator (RemoveFrom operatorCheckInfo.leftRange.end)
|> List.concatMap (Tuple.second >> errorsForNode)
areSimilarConditionsError : ModuleNameLookupTable -> String -> Node Expression -> ( RedundantConditionResolution, Node Expression ) -> List (Error {})
areSimilarConditionsError lookupTable operator nodeToCompareTo ( redundantConditionResolution, nodeToLookAt ) =
case Normalize.compare lookupTable 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 { lookupTable, parentRange, left, leftRange, rightRange } =
case getBoolean lookupTable left of
Determined True ->
[ Rule.errorWithFix
{ message = "Condition 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 { lookupTable, parentRange, right, leftRange, rightRange } =
case getBoolean lookupTable 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 { lookupTable, parentRange, left, leftRange, rightRange } =
case getBoolean lookupTable left of
Determined True ->
[ Rule.errorWithFix
{ message = unnecessaryMessage
, details = unnecessaryDetails
}
parentRange
[ Fix.removeRange
{ start = leftRange.start
, end = rightRange.start
}
]
]
Determined False ->
[ Rule.errorWithFix
{ message = "Condition is always False"
, details = alwaysSameDetails
}
parentRange
[ Fix.removeRange
{ start = leftRange.end
, end = rightRange.end
}
]
]
Undetermined ->
[]
and_isRightSimplifiableError : OperatorCheckInfo -> List (Rule.Error {})
and_isRightSimplifiableError { lookupTable, parentRange, leftRange, right, rightRange } =
case getBoolean lookupTable right of
Determined True ->
[ Rule.errorWithFix
{ message = unnecessaryMessage
, details = unnecessaryDetails
}
parentRange
[ Fix.removeRange
{ start = leftRange.end
, end = rightRange.end
}
]
]
Determined False ->
[ Rule.errorWithFix
{ message = "Condition 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 } =
if getBoolean lookupTable 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 getBoolean lookupTable 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 ]
]
_ ->
case Normalize.compare lookupTable left right of
Normalize.ConfirmedEquality ->
[ Rule.errorWithFix
{ message = "Condition is always " ++ boolToString isEqual
, details = sameThingOnBothSidesDetails isEqual
}
parentRange
[ Fix.replaceRangeBy parentRange (boolToString isEqual)
]
]
Normalize.ConfirmedInequality ->
[ Rule.errorWithFix
{ message = "Condition is always " ++ boolToString (not isEqual)
, details = sameThingOnBothSidesDetails (not isEqual)
}
parentRange
[ Fix.replaceRangeBy parentRange (boolToString (not isEqual))
]
]
Normalize.Unconfirmed ->
[]
getNotCall : ModuleNameLookupTable -> Node Expression -> Maybe Range
getNotCall lookupTable baseNode =
case Node.value (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 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 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 (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."
]
sameThingOnBothSidesDetails : Bool -> List String
sameThingOnBothSidesDetails computedResult =
let
computedResultString : String
computedResultString =
if computedResult then
"True"
else
"False"
in
[ "The value on the left and on the right are the same. Therefore we can determine that the expression will always be " ++ computedResultString ++ "."
]
-- COMPARISONS
comparisonChecks : (Float -> Float -> Bool) -> OperatorCheckInfo -> List (Error {})
comparisonChecks operatorFunction operatorCheckInfo =
case
Maybe.map2 operatorFunction
(Normalize.getNumberValue operatorCheckInfo.left)
(Normalize.getNumberValue operatorCheckInfo.right)
|> Maybe.map boolToString
of
Just value ->
[ Rule.errorWithFix
{ message = "Comparison is always " ++ value
, details =
[ "The value on the left and on the right are the same. Therefore we can determine that the expression will always be " ++ value ++ "."
]
}
operatorCheckInfo.parentRange
[ Fix.replaceRangeBy operatorCheckInfo.parentRange value
]
]
Nothing ->
[]
-- IF EXPRESSIONS
targetIf : Node a -> Range
targetIf node =
let
{ start } =
Node.range node
in
{ 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 (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 (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 a 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 a 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 a 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 a 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 } =
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 getIntValue 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 { lookupTable, fnRange, firstArg, secondArg, thirdArg } =
case secondArg of
Just secondArg_ ->
case Normalize.compare lookupTable 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 ( removeParens left, Node.value (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 (removeParens left), 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 ( removeParens left, Node.value (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 (removeParens left), 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, usingRightPizza } =
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 [ Node singleElementRange _ ])) ->
[ 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
(if usingRightPizza then
[ Fix.replaceRangeBy { start = listRange.start, end = singleElementRange.start } "("
, Fix.replaceRangeBy { start = singleElementRange.end, end = listRange.end } ")"
, Fix.removeRange fnRange
]
else
[ Fix.removeRange fnRange
, Fix.replaceRangeBy { start = listRange.start, end = singleElementRange.start } "("
, Fix.replaceRangeBy { start = singleElementRange.end, end = listRange.end } ")"
]
)
]
_ ->
[]
concatAndMapCompositionCheck : CompositionCheckInfo -> List (Error {})
concatAndMapCompositionCheck { lookupTable, fromLeftToRight, left, right } =
if fromLeftToRight then
if isSpecificFunction [ "List" ] "concat" lookupTable right then
case Node.value (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 (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 removeParens firstArg of
Node lambdaRange (Expression.LambdaExpression { args, expression }) ->
case Maybe.map 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 { lookupTable, parentRange, fnRange, firstArg, secondArg } =
case Maybe.map (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 isAlwaysBoolean lookupTable 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 { lookupTable, parentRange, fnRange, firstArg, secondArg } =
case Maybe.map (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 isAlwaysBoolean lookupTable 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 (removeParens right) of
Expression.Application [ rightFunction, arg ] ->
if isSpecificFunction [ "List" ] "filterMap" lookupTable rightFunction && isIdentity lookupTable arg then
case Node.value (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 (removeParens left) of
Expression.Application [ leftFunction, arg ] ->
if isSpecificFunction [ "List" ] "filterMap" lookupTable leftFunction && isIdentity lookupTable arg then
case Node.value (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 } =
case Maybe.map2 Tuple.pair (getIntValue firstArg) (Maybe.andThen getIntValue secondArg) of
Just ( first, second ) ->
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 ->
[]
listRepeatChecks : CheckInfo -> List (Error {})
listRepeatChecks { parentRange, fnRange, firstArg, secondArg } =
case getIntValue 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 (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 [ arg ] ->
[ Rule.errorWithFix
{ message = "Unnecessary " ++ moduleName ++ ".batch"
, details = [ moduleName ++ ".batch with a single element is equal to that element." ]
}
fnRange
[ Fix.replaceRangeBy { start = parentRange.start, end = (Node.range arg).start } "("
, Fix.replaceRangeBy { start = (Node.range arg).end, end = parentRange.end } ")"
]
]
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 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 { fnRange, firstArg } =
case removeParens firstArg of
Node listRange (Expression.ListExpr [ Node singleElementRange _ ]) ->
[ Rule.errorWithFix
{ message = "Unnecessary oneOf"
, details = [ "There is only a single element in the list of elements to try out." ]
}
fnRange
[ Fix.replaceRangeBy { start = fnRange.start, end = singleElementRange.start } "("
, Fix.replaceRangeBy { start = singleElementRange.end, end = listRange.end } ")"
]
]
_ ->
[]
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 ({ lookupTable, 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 isAlwaysBoolean lookupTable 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 isAlwaysBoolean checkInfo.lookupTable 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 (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 (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 rawNode =
let
(Node { start, end } nodeValue) =
removeParens rawNode
in
case nodeValue of
Expression.ListExpr [ _ ] ->
Just
[ Fix.replaceRangeBy { start = start, end = { start | column = start.column + 1 } } "("
, Fix.replaceRangeBy { start = { end | column = end.column - 1 }, end = end } ")"
]
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 (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 =
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 =
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
-- CASE OF
caseOfChecks : ModuleContext -> Range -> Expression.CaseBlock -> List (Error {})
caseOfChecks context parentRange caseBlock =
firstThatReportsError
[ \() -> sameBodyForCaseOfChecks context parentRange caseBlock.cases
, \() -> booleanCaseOfChecks context.lookupTable parentRange caseBlock
]
()
sameBodyForCaseOfChecks : ModuleContext -> Range -> List ( Node Pattern, Node Expression ) -> List (Error {})
sameBodyForCaseOfChecks context parentRange cases =
case cases of
[] ->
[]
first :: rest ->
if
List.any (Tuple.first >> introducesVariable) (first :: rest)
|| not (Normalize.areAllTheSame context.lookupTable (Tuple.second first) (List.map Tuple.second rest))
then
[]
else
let
constructorsUsed : () -> List ( ModuleName, String )
constructorsUsed () =
List.concatMap (Tuple.first >> findUsedConstructors context) (first :: rest)
|> Set.fromList
|> Set.toList
in
if not (List.isEmpty context.ignoredCustomTypes) && allConstructorsWereUsedOfAType context.ignoredCustomTypes (constructorsUsed ()) then
[]
else
let
firstBodyRange : Range
firstBodyRange =
Node.range (Tuple.second first)
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 }
]
]
allConstructorsWereUsedOfAType : List Constructor -> List ( ModuleName, String ) -> Bool
allConstructorsWereUsedOfAType ignoredCustomTypes constructorsUsed =
case constructorsUsed of
[] ->
False
( moduleName, constructorName ) :: rest ->
case find (\type_ -> type_.moduleName == moduleName && List.member constructorName type_.constructors) ignoredCustomTypes of
Just customType ->
List.all
(\constructor -> List.member ( moduleName, constructor ) (( moduleName, constructorName ) :: rest))
customType.constructors
|| allConstructorsWereUsedOfAType ignoredCustomTypes rest
Nothing ->
allConstructorsWereUsedOfAType ignoredCustomTypes rest
caseKeyWordRange : Range -> Range
caseKeyWordRange range =
{ start = range.start
, end = { row = range.start.row, column = range.start.column + 4 }
}
introducesVariable : Node Pattern -> Bool
introducesVariable node =
case Node.value node of
Pattern.VarPattern _ ->
True
Pattern.RecordPattern _ ->
True
Pattern.AsPattern _ _ ->
True
Pattern.ParenthesizedPattern pattern ->
introducesVariable pattern
Pattern.TuplePattern nodes ->
List.any introducesVariable nodes
Pattern.UnConsPattern first rest ->
List.any introducesVariable [ first, rest ]
Pattern.ListPattern nodes ->
List.any introducesVariable nodes
Pattern.NamedPattern _ nodes ->
List.any introducesVariable nodes
_ ->
False
findUsedConstructors : ModuleContext -> Node Pattern -> List ( ModuleName, String )
findUsedConstructors context node =
case Node.value node of
Pattern.NamedPattern { name } nodes ->
case isIgnoredConstructor2 context (Node.range node) name of
Just moduleName ->
( moduleName, name ) :: List.concatMap (findUsedConstructors context) nodes
Nothing ->
List.concatMap (findUsedConstructors context) nodes
Pattern.AsPattern pattern _ ->
findUsedConstructors context pattern
Pattern.ParenthesizedPattern pattern ->
findUsedConstructors context pattern
Pattern.TuplePattern nodes ->
List.concatMap (findUsedConstructors context) nodes
Pattern.UnConsPattern first rest ->
List.concatMap (findUsedConstructors context) [ first, rest ]
Pattern.ListPattern nodes ->
List.concatMap (findUsedConstructors context) nodes
_ ->
[]
isIgnoredConstructor2 : ModuleContext -> Range -> String -> Maybe ModuleName
isIgnoredConstructor2 context range name =
case ModuleNameLookupTable.moduleNameAt context.lookupTable range of
Just [] ->
if Set.member ( context.moduleName, name ) context.constructorsToIgnore then
Just context.moduleName
else
Nothing
Just moduleName ->
if Set.member ( moduleName, name ) context.constructorsToIgnore then
Just moduleName
else
Nothing
Nothing ->
Nothing
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 "
]
)
]
_ ->
[]
_ ->
[]
isSpecificFunction : ModuleName -> String -> ModuleNameLookupTable -> Node Expression -> Bool
isSpecificFunction moduleName fnName lookupTable node =
case 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 (removeParens node) of
Expression.Application ((Node noneRange (Expression.FunctionOrValue _ foundFnName)) :: _ :: []) ->
(foundFnName == fnName)
&& (ModuleNameLookupTable.moduleNameAt lookupTable noneRange == Just moduleName)
_ ->
False
getIntValue : Node Expression -> Maybe Int
getIntValue node =
case Node.value (removeParens node) of
Expression.Integer n ->
Just n
Expression.Hex n ->
Just n
Expression.Negation expr ->
Maybe.map negate (getIntValue expr)
_ ->
Nothing
getUncomputedNumberValue : Node Expression -> Maybe Float
getUncomputedNumberValue node =
case Node.value (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
-- 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 =
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 (removeParens node) of
Expression.FunctionOrValue [] name ->
Just name
_ ->
Nothing
isListLiteral : Node Expression -> Bool
isListLiteral node =
case Node.value node of
Expression.ListExpr _ ->
True
_ ->
False
removeParens : Node Expression -> Node Expression
removeParens node =
case Node.value node of
Expression.ParenthesizedExpression expr ->
removeParens expr
_ ->
node
removeParensFromPattern : Node Pattern -> Node Pattern
removeParensFromPattern node =
case Node.value node of
Pattern.ParenthesizedPattern pattern ->
removeParensFromPattern pattern
_ ->
node
isAlwaysBoolean : ModuleNameLookupTable -> Node Expression -> Match Bool
isAlwaysBoolean lookupTable node =
case Node.value (removeParens node) of
Expression.Application ((Node alwaysRange (Expression.FunctionOrValue _ "always")) :: boolean :: []) ->
case ModuleNameLookupTable.moduleNameAt lookupTable alwaysRange of
Just [ "Basics" ] ->
getBoolean lookupTable boolean
_ ->
Undetermined
Expression.LambdaExpression { expression } ->
getBoolean lookupTable expression
_ ->
Undetermined
getBoolean : ModuleNameLookupTable -> Node Expression -> Match Bool
getBoolean lookupTable baseNode =
let
node : Node Expression
node =
removeParens baseNode
in
case Node.value node of
Expression.FunctionOrValue _ "True" ->
case ModuleNameLookupTable.moduleNameFor lookupTable node of
Just [ "Basics" ] ->
Determined True
_ ->
Undetermined
Expression.FunctionOrValue _ "False" ->
case ModuleNameLookupTable.moduleNameFor lookupTable node of
Just [ "Basics" ] ->
Determined False
_ ->
Undetermined
_ ->
Undetermined
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 =
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 =
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 =
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 =
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 (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 (removeParens node) of
Expression.ListExpr [] ->
True
_ ->
False