elm-review/tests/Simplify.elm
2023-09-03 11:30:45 +02:00

9192 lines
345 KiB
Elm

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