elm-review/tests/NoUnused/Patterns.elm

711 lines
20 KiB
Elm
Raw Normal View History

2020-06-03 19:23:19 +03:00
module NoUnused.Patterns exposing (rule)
{-| Report useless patterns and pattern values that are not used.
# Rule
@docs rule
-}
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range as Range exposing (Range)
import Elm.Writer as Writer
import NoUnused.Patterns.NameVisitor as NameVisitor
2021-01-24 18:44:12 +03:00
import NoUnused.RangeDict as RangeDict exposing (RangeDict)
2020-08-09 19:55:15 +03:00
import Review.Fix as Fix exposing (Fix)
2020-06-03 19:23:19 +03:00
import Review.Rule as Rule exposing (Rule)
import Set exposing (Set)
{-| Report useless patterns and pattern values that are not used.
2021-08-19 21:57:29 +03:00
🔧 Running with `--fix` will automatically remove all the reported errors.
2020-06-03 19:23:19 +03:00
config =
[ NoUnused.Patterns.rule
]
This rule looks within let..in blocks and case branches to find any patterns that are unused. It will report any useless patterns as well as any pattern values that are not used.
## Fail
Value `something` is not used:
case maybe of
Just something ->
True
Nothing ->
False
## Success
case maybe of
Just _ ->
True
Nothing ->
False
2020-08-09 19:55:15 +03:00
## Try it out
You can try this rule out by running the following command:
```bash
2020-09-22 20:40:30 +03:00
elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Patterns
2020-08-09 19:55:15 +03:00
```
2020-06-03 19:23:19 +03:00
-}
rule : Rule
rule =
2020-06-28 08:49:27 +03:00
Rule.newModuleRuleSchema "NoUnused.Patterns" initialContext
|> Rule.withExpressionEnterVisitor expressionEnterVisitor
|> Rule.withExpressionExitVisitor expressionExitVisitor
2020-06-28 08:53:16 +03:00
|> NameVisitor.withValueVisitor valueVisitor
2020-06-28 08:49:27 +03:00
|> Rule.fromModuleRuleSchema
2020-06-03 19:23:19 +03:00
2021-01-24 18:44:12 +03:00
type alias Context =
{ scopes : List Scope
, scopesToCreate : RangeDict (List FoundPattern)
}
type alias Scope =
{ declared : List FoundPattern
, used : Set String
}
type FoundPattern
= SingleValue
{ name : String
, range : Range
, message : String
, details : List String
, fix : List Fix
}
| RecordPattern
{ fields : List (Node String)
, recordRange : Range
}
| SimplifiablePattern (Rule.Error {})
initialContext : Context
initialContext =
{ scopes = []
, scopesToCreate = RangeDict.empty
}
-- EXPRESSION ENTER VISITOR
expressionEnterVisitor : Node Expression -> Context -> ( List nothing, Context )
expressionEnterVisitor node context =
2021-01-24 18:44:12 +03:00
let
newContext : Context
newContext =
case RangeDict.get (Node.range node) context.scopesToCreate of
Just declared ->
{ context | scopes = { declared = declared, used = Set.empty } :: context.scopes }
Nothing ->
context
in
expressionEnterVisitorHelp node newContext
expressionEnterVisitorHelp : Node Expression -> Context -> ( List nothing, Context )
expressionEnterVisitorHelp node context =
case Node.value node of
Expression.LetExpression { declarations } ->
2021-01-24 18:44:12 +03:00
let
findPatternsInLetDeclaration : Node Expression.LetDeclaration -> List FoundPattern
findPatternsInLetDeclaration letDeclaration =
case Node.value letDeclaration of
Expression.LetFunction _ ->
[]
Expression.LetDestructuring pattern _ ->
findPatterns Destructuring pattern
in
( []
, { context
| scopes =
{ declared = List.concatMap findPatternsInLetDeclaration declarations
, used = Set.empty
}
:: context.scopes
-- Will only be used to remove on exit, we are already adding the declared patterns to the scope above
, scopesToCreate = RangeDict.insert (Node.range node) [] context.scopesToCreate
}
)
2020-06-03 19:23:19 +03:00
Expression.CaseExpression { cases } ->
2021-01-24 18:44:12 +03:00
( []
, { context
2021-04-06 18:19:13 +03:00
| scopesToCreate =
2021-01-24 18:44:12 +03:00
List.foldl
(\( pattern, expr ) scopesToCreate -> RangeDict.insert (Node.range expr) (findPatterns Matching pattern) scopesToCreate)
context.scopesToCreate
cases
}
)
2020-06-03 19:23:19 +03:00
_ ->
( [], context )
expressionExitVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionExitVisitor node context =
2021-01-24 18:44:12 +03:00
case RangeDict.get (Node.range node) context.scopesToCreate of
Just _ ->
report context
2021-01-24 18:44:12 +03:00
Nothing ->
( [], context )
report : Context -> ( List (Rule.Error {}), Context )
report context =
case context.scopes of
headScope :: restOfScopes ->
let
{ singles, records, simplifiablePatterns } =
findDeclaredPatterns headScope
allDeclared : List String
allDeclared =
List.concat
[ List.map .name singles
, List.concatMap (.fields >> List.map Node.value) records
]
nonUsedVars : Set String
nonUsedVars =
allDeclared
|> Set.fromList
|> Set.diff headScope.used
errors : List (Rule.Error {})
errors =
List.concat
[ singleErrors
2021-08-19 21:57:29 +03:00
, List.concatMap (recordErrors context) records
2021-01-24 18:44:12 +03:00
, simplifiablePatterns
]
singleErrors : List (Rule.Error {})
singleErrors =
List.filter (\{ name } -> not <| Set.member name headScope.used) singles
|> List.map
(\pattern ->
Rule.errorWithFix
{ message = pattern.message
, details = pattern.details
}
pattern.range
pattern.fix
)
in
( errors
, List.foldl
useValue
{ context | scopes = restOfScopes }
(Set.toList nonUsedVars)
)
2020-06-03 19:23:19 +03:00
_ ->
( [], context )
2021-08-19 21:57:29 +03:00
recordErrors : Context -> { fields : List (Node String), recordRange : Range } -> List (Rule.Error {})
recordErrors context { fields, recordRange } =
if List.isEmpty fields then
[ Rule.errorWithFix
{ message = "Record pattern is not needed"
, details = [ "This pattern is redundant and should be replaced with '_'." ]
}
recordRange
[ Fix.replaceRangeBy recordRange "_" ]
]
else
let
( unused, used ) =
List.partition (isNodeInContext context) fields
in
case unused of
[] ->
[]
firstNode :: restNodes ->
let
first : String
first =
Node.value firstNode
rest : List String
rest =
List.map Node.value restNodes
( errorRange, fix ) =
case used of
[] ->
( recordRange, Fix.replaceRangeBy recordRange "_" )
_ ->
( Range.combine (List.map Node.range unused)
, Node Range.emptyRange (Pattern.RecordPattern used)
|> Writer.writePattern
|> Writer.write
|> Fix.replaceRangeBy recordRange
)
in
[ Rule.errorWithFix
{ message = listToMessage first rest
, details = listToDetails first rest
}
errorRange
[ fix ]
]
2021-01-24 18:44:12 +03:00
findDeclaredPatterns :
Scope
->
{ singles : List { name : String, range : Range, message : String, details : List String, fix : List Fix }
, records : List { fields : List (Node String), recordRange : Range }
, simplifiablePatterns : List (Rule.Error {})
}
findDeclaredPatterns scope =
List.foldl
(\foundPattern acc ->
case foundPattern of
SingleValue v ->
{ acc | singles = v :: acc.singles }
RecordPattern v ->
{ acc | records = v :: acc.records }
SimplifiablePattern simplifiablePatternError ->
{ acc | simplifiablePatterns = simplifiablePatternError :: acc.simplifiablePatterns }
)
{ singles = [], records = [], simplifiablePatterns = [] }
scope.declared
2020-06-03 19:23:19 +03:00
valueVisitor : Node ( ModuleName, String ) -> Context -> ( List (Rule.Error {}), Context )
valueVisitor (Node _ ( moduleName, value )) context =
case moduleName of
[] ->
( [], useValue value context )
_ ->
( [], context )
--- ON ENTER
2021-01-24 18:44:12 +03:00
findPatterns : PatternUse -> Node Pattern -> List FoundPattern
findPatterns use (Node range pattern) =
2020-06-03 19:23:19 +03:00
case pattern of
2021-01-24 18:44:12 +03:00
Pattern.VarPattern name ->
[ SingleValue
{ name = name
, message = "Value `" ++ name ++ "` is not used."
, details = singularReplaceDetails
, range = range
, fix = [ Fix.replaceRangeBy range "_" ]
}
]
2020-06-03 19:23:19 +03:00
2021-01-24 18:44:12 +03:00
Pattern.TuplePattern [ Node _ Pattern.AllPattern, Node _ Pattern.AllPattern ] ->
[ SimplifiablePattern
(Rule.errorWithFix
2021-08-19 21:57:29 +03:00
{ message = "Tuple pattern is not needed"
2021-01-24 18:44:12 +03:00
, details = redundantDetails
}
range
[ Fix.replaceRangeBy range "_" ]
)
]
Pattern.TuplePattern [ Node _ Pattern.AllPattern, Node _ Pattern.AllPattern, Node _ Pattern.AllPattern ] ->
[ SimplifiablePattern
(Rule.errorWithFix
2021-08-19 21:57:29 +03:00
{ message = "Tuple pattern is not needed"
2021-01-24 18:44:12 +03:00
, details = redundantDetails
}
range
[ Fix.replaceRangeBy range "_" ]
)
]
2020-06-03 19:23:19 +03:00
Pattern.TuplePattern patterns ->
2021-01-24 18:44:12 +03:00
List.concatMap (findPatterns use) patterns
2020-06-03 19:23:19 +03:00
2021-01-24 18:44:12 +03:00
Pattern.RecordPattern fields ->
[ RecordPattern
{ fields = fields
, recordRange = range
}
]
2020-06-03 19:23:19 +03:00
Pattern.UnConsPattern first second ->
2021-01-24 18:44:12 +03:00
findPatterns use first ++ findPatterns use second
2020-06-03 19:23:19 +03:00
Pattern.ListPattern patterns ->
2021-01-24 18:44:12 +03:00
List.concatMap (findPatterns use) patterns
2020-06-03 19:23:19 +03:00
Pattern.NamedPattern _ patterns ->
2021-01-24 18:44:12 +03:00
if use == Destructuring && List.all isAllPattern patterns then
[ SimplifiablePattern
(Rule.errorWithFix
2021-08-19 21:57:29 +03:00
{ message = "Named pattern is not needed"
2021-01-24 18:44:12 +03:00
, details = redundantDetails
}
range
[ Fix.replaceRangeBy range "_" ]
)
]
else
List.concatMap (findPatterns use) patterns
2020-06-03 19:23:19 +03:00
Pattern.AsPattern inner name ->
2021-01-24 18:44:12 +03:00
findPatternForAsPattern range inner name :: findPatterns use inner
2020-06-03 19:23:19 +03:00
Pattern.ParenthesizedPattern inner ->
2021-01-24 18:44:12 +03:00
findPatterns use inner
2020-06-03 19:23:19 +03:00
_ ->
2021-01-24 18:44:12 +03:00
[]
2020-06-03 19:23:19 +03:00
2021-01-24 18:44:12 +03:00
--- ON EXIT
2020-06-03 19:23:19 +03:00
2021-01-24 18:44:12 +03:00
singularRemoveDetails : List String
singularRemoveDetails =
[ "You should either use this value somewhere or remove it." ]
2020-06-03 19:23:19 +03:00
2021-01-24 18:44:12 +03:00
singularReplaceDetails : List String
singularReplaceDetails =
[ "You should either use this value somewhere or replace it with '_'." ]
2020-06-03 19:23:19 +03:00
pluralDetails : List String
pluralDetails =
2021-01-24 18:44:12 +03:00
[ "You should either use these values somewhere or remove them." ]
redundantDetails : List String
redundantDetails =
[ "This pattern is redundant and should be replaced with '_'." ]
2020-06-03 19:23:19 +03:00
removeDetails : List String
removeDetails =
2021-01-24 18:44:12 +03:00
[ "This pattern is redundant and should be removed." ]
2020-06-03 19:23:19 +03:00
andThen :
(value -> Context -> ( List (Rule.Error {}), Context ))
-> value
2020-06-03 19:23:19 +03:00
-> ( List (Rule.Error {}), Context )
-> ( List (Rule.Error {}), Context )
andThen function value ( errors, context ) =
2020-06-03 19:23:19 +03:00
let
( newErrors, newContext ) =
function value context
2020-06-03 19:23:19 +03:00
in
( newErrors ++ errors, newContext )
type PatternUse
= Destructuring
| Matching
errorsForPatternList : PatternUse -> List (Node Pattern) -> Context -> ( List (Rule.Error {}), Context )
errorsForPatternList use list context =
List.foldl (andThen (errorsForPattern use)) ( [], context ) list
2020-06-03 19:23:19 +03:00
errorsForPattern : PatternUse -> Node Pattern -> Context -> ( List (Rule.Error {}), Context )
errorsForPattern use (Node range pattern) context =
case pattern of
Pattern.AllPattern ->
( [], context )
Pattern.VarPattern value ->
errorsForValue value range context
Pattern.RecordPattern values ->
errorsForRecordValueList range values context
Pattern.TuplePattern [ Node _ Pattern.AllPattern, Node _ Pattern.AllPattern ] ->
errorsForUselessTuple range context
Pattern.TuplePattern [ Node _ Pattern.AllPattern, Node _ Pattern.AllPattern, Node _ Pattern.AllPattern ] ->
errorsForUselessTuple range context
Pattern.TuplePattern patterns ->
errorsForPatternList use patterns context
Pattern.UnConsPattern first second ->
errorsForPatternList use [ first, second ] context
Pattern.ListPattern patterns ->
errorsForPatternList use patterns context
Pattern.NamedPattern _ patterns ->
if use == Destructuring && List.all isAllPattern patterns then
errorsForUselessNamePattern range context
else
errorsForPatternList use patterns context
Pattern.AsPattern inner name ->
context
|> errorsForAsPattern range inner name
|> andThen (errorsForPattern use) inner
2020-06-03 19:23:19 +03:00
Pattern.ParenthesizedPattern inner ->
errorsForPattern use inner context
_ ->
( [], context )
errorsForUselessNamePattern : Range -> Context -> ( List (Rule.Error {}), Context )
errorsForUselessNamePattern range context =
( [ Rule.errorWithFix
2021-08-19 21:57:29 +03:00
{ message = "Named pattern is not needed"
2021-01-24 18:44:12 +03:00
, details = redundantDetails
2020-06-03 19:23:19 +03:00
}
range
[ Fix.replaceRangeBy range "_" ]
]
, context
)
errorsForUselessTuple : Range -> Context -> ( List (Rule.Error {}), Context )
errorsForUselessTuple range context =
( [ Rule.errorWithFix
2021-08-19 21:57:29 +03:00
{ message = "Tuple pattern is not needed"
2021-01-24 18:44:12 +03:00
, details = redundantDetails
2020-06-03 19:23:19 +03:00
}
range
[ Fix.replaceRangeBy range "_" ]
]
, context
)
errorsForRecordValueList : Range -> List (Node String) -> Context -> ( List (Rule.Error {}), Context )
errorsForRecordValueList recordRange list context =
let
( unused, used ) =
List.partition (isNodeInContext context) list
2020-06-03 19:23:19 +03:00
in
case unused of
[] ->
( [], context )
firstNode :: restNodes ->
let
2020-08-09 19:55:15 +03:00
first : String
2020-06-03 19:23:19 +03:00
first =
2020-08-09 19:55:15 +03:00
Node.value firstNode
2020-06-03 19:23:19 +03:00
2020-08-09 19:55:15 +03:00
rest : List String
2020-06-03 19:23:19 +03:00
rest =
List.map Node.value restNodes
( errorRange, fix ) =
case used of
[] ->
( recordRange, Fix.replaceRangeBy recordRange "_" )
_ ->
( Range.combine (List.map Node.range unused)
, Node Range.emptyRange (Pattern.RecordPattern used)
|> Writer.writePattern
|> Writer.write
|> Fix.replaceRangeBy recordRange
)
in
( [ Rule.errorWithFix
{ message = listToMessage first rest
, details = listToDetails first rest
}
errorRange
[ fix ]
]
, List.foldl forgetNode context unused
)
listToMessage : String -> List String -> String
listToMessage first rest =
case List.reverse rest of
[] ->
"Value `" ++ first ++ "` is not used."
last :: middle ->
"Values `" ++ String.join "`, `" (first :: middle) ++ "` and `" ++ last ++ "` are not used."
listToDetails : String -> List String -> List String
listToDetails _ rest =
case rest of
[] ->
2021-01-24 18:44:12 +03:00
singularRemoveDetails
2020-06-03 19:23:19 +03:00
_ ->
pluralDetails
errorsForAsPattern : Range -> Node Pattern -> Node String -> Context -> ( List (Rule.Error {}), Context )
errorsForAsPattern patternRange inner (Node range name) context =
2021-01-24 18:44:12 +03:00
if isUnused name context then
2020-06-03 19:23:19 +03:00
let
2020-08-09 19:55:15 +03:00
fix : List Fix
2020-06-03 19:23:19 +03:00
fix =
[ inner
|> Writer.writePattern
|> Writer.write
|> Fix.replaceRangeBy patternRange
]
in
( [ Rule.errorWithFix
{ message = "Pattern alias `" ++ name ++ "` is not used."
2021-01-24 18:44:12 +03:00
, details = singularRemoveDetails
2020-06-03 19:23:19 +03:00
}
range
fix
]
2021-01-24 18:44:12 +03:00
, useValue name context
2020-06-03 19:23:19 +03:00
)
else if isAllPattern inner then
( [ Rule.errorWithFix
2021-08-19 21:57:29 +03:00
{ message = "Pattern `_` is not needed"
2020-06-03 19:23:19 +03:00
, details = removeDetails
}
(Node.range inner)
[ Fix.replaceRangeBy patternRange name ]
]
2021-01-24 18:44:12 +03:00
, useValue name context
2020-06-03 19:23:19 +03:00
)
else
( [], context )
2021-01-24 18:44:12 +03:00
findPatternForAsPattern : Range -> Node Pattern -> Node String -> FoundPattern
findPatternForAsPattern patternRange inner (Node range name) =
if isAllPattern inner then
SimplifiablePattern
(Rule.errorWithFix
2021-08-19 21:57:29 +03:00
{ message = "Pattern `_` is not needed"
2021-01-24 18:44:12 +03:00
, details = removeDetails
}
(Node.range inner)
[ Fix.replaceRangeBy patternRange name ]
)
else
let
fix : List Fix
fix =
[ inner
|> Writer.writePattern
|> Writer.write
|> Fix.replaceRangeBy patternRange
]
in
SingleValue
{ name = name
, message = "Pattern alias `" ++ name ++ "` is not used."
, details = singularRemoveDetails
, range = range
, fix = fix
}
2020-06-03 19:23:19 +03:00
isAllPattern : Node Pattern -> Bool
isAllPattern (Node _ pattern) =
case pattern of
Pattern.AllPattern ->
True
_ ->
False
forgetNode : Node String -> Context -> Context
forgetNode (Node _ value) context =
2021-01-24 18:44:12 +03:00
useValue value context
2020-06-03 19:23:19 +03:00
errorsForValue : String -> Range -> Context -> ( List (Rule.Error {}), Context )
2021-01-24 18:44:12 +03:00
errorsForValue name range context =
if isUnused name context then
2020-06-03 19:23:19 +03:00
( [ Rule.errorWithFix
2021-01-24 18:44:12 +03:00
{ message = "Value `" ++ name ++ "` is not used."
, details = singularReplaceDetails
2020-06-03 19:23:19 +03:00
}
range
[ Fix.replaceRangeBy range "_" ]
]
2021-01-24 18:44:12 +03:00
, useValue name context
2020-06-03 19:23:19 +03:00
)
else
( [], context )
2021-01-24 18:44:12 +03:00
useValue : String -> Context -> Context
useValue name context =
case context.scopes of
[] ->
context
2020-06-03 19:23:19 +03:00
2021-01-24 18:44:12 +03:00
headScope :: restOfScopes ->
{ context | scopes = { headScope | used = Set.insert name headScope.used } :: restOfScopes }
2020-06-03 19:23:19 +03:00
2021-01-24 18:44:12 +03:00
isNodeInContext : Context -> Node String -> Bool
isNodeInContext context (Node _ value) =
isUnused value context
isUnused : String -> Context -> Bool
isUnused name context =
case context.scopes of
[] ->
False
headScope :: _ ->
not <| Set.member name headScope.used