elm-review/src/Lint/Rule/NoUnusedVariables.elm

535 lines
15 KiB
Elm
Raw Normal View History

2018-11-22 21:19:19 +03:00
module Lint.Rule.NoUnusedVariables exposing (rule)
2019-07-03 15:19:29 +03:00
{-| Forbid variables or types that are declared or imported but never used.
2018-11-22 21:19:19 +03:00
2019-07-03 15:19:29 +03:00
## Fail
2018-11-22 21:19:19 +03:00
2019-07-03 15:19:29 +03:00
-- module A exposing (a)
2018-11-22 21:19:19 +03:00
a n =
n + 1
b =
a 2
2019-07-03 15:19:29 +03:00
## Success
2018-11-22 21:19:19 +03:00
2019-07-03 15:19:29 +03:00
-- module A exposing (a)
2018-11-22 21:19:19 +03:00
a n =
n + 1
2019-07-03 15:19:29 +03:00
# Rule
@docs rule
2018-11-22 21:19:19 +03:00
-}
import Dict exposing (Dict)
import Elm.Syntax.Declaration exposing (Declaration(..))
import Elm.Syntax.Exposing exposing (Exposing(..), TopLevelExpose(..))
import Elm.Syntax.Expression exposing (Expression(..), Function, FunctionImplementation, LetDeclaration(..))
2018-11-22 21:19:19 +03:00
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Module as Module exposing (Module(..))
import Elm.Syntax.Node as Node exposing (Node)
2018-11-22 21:19:19 +03:00
import Elm.Syntax.Range exposing (Range)
import Elm.Syntax.TypeAnnotation exposing (TypeAnnotation(..))
2019-06-26 14:47:00 +03:00
import Lint.Rule as Rule exposing (Direction, Error, Rule)
2018-11-22 21:19:19 +03:00
import List.Nonempty as Nonempty exposing (Nonempty)
import Set exposing (Set)
2019-07-03 15:19:29 +03:00
{-| Forbid variables or types that are declared or imported but never used.
2018-11-26 13:22:43 +03:00
config =
[ ( Critical, NoUnusedVariables.rule )
2018-11-26 13:22:43 +03:00
]
-}
rule : Rule
2019-06-15 22:14:40 +03:00
rule =
Rule.newSchema "NoUnusedVariables"
2019-06-24 01:32:27 +03:00
|> Rule.withInitialContext initialContext
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withImportVisitor importVisitor
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.withDeclarationVisitor declarationVisitor
|> Rule.withFinalEvaluation finalEvaluation
|> Rule.fromSchema
type alias Context =
{ scopes : Nonempty Scope
, exposesEverything : Bool
, constructorNameToTypeName : Dict String String
2019-06-24 01:32:27 +03:00
}
type alias Scope =
{ declared : Dict String ( VariableType, Range )
, used : Set String
}
2018-11-26 13:22:43 +03:00
2018-11-26 21:16:04 +03:00
type VariableType
2018-11-26 20:39:46 +03:00
= Variable
| ImportedModule
2018-11-26 21:16:04 +03:00
| ImportedVariable
| ImportedType
| ImportedOperator
2018-11-26 20:39:46 +03:00
| ModuleAlias
| Type
2018-11-26 21:09:18 +03:00
| Port
2018-11-26 20:39:46 +03:00
2019-06-24 01:32:27 +03:00
initialContext : Context
initialContext =
{ scopes = Nonempty.fromElement emptyScope
, exposesEverything = False
, constructorNameToTypeName = Dict.empty
2018-11-22 21:19:19 +03:00
}
emptyScope : Scope
emptyScope =
Scope Dict.empty Set.empty
2018-11-26 21:16:04 +03:00
error : VariableType -> Range -> String -> Error
2018-11-26 20:39:46 +03:00
error variableType range_ name =
2019-06-26 14:47:00 +03:00
Rule.error
2018-11-26 21:09:18 +03:00
(variableTypeToString variableType ++ " `" ++ name ++ "` is not used" ++ variableTypeWarning variableType)
2018-11-26 20:39:46 +03:00
range_
2018-11-26 21:16:04 +03:00
variableTypeToString : VariableType -> String
2018-11-26 20:39:46 +03:00
variableTypeToString value =
case value of
Variable ->
"Variable"
ImportedModule ->
"Imported module"
2018-11-26 21:16:04 +03:00
ImportedVariable ->
"Imported variable"
ImportedType ->
"Imported type"
ImportedOperator ->
"Imported operator"
2018-11-26 20:39:46 +03:00
ModuleAlias ->
"Module alias"
Type ->
"Type"
2018-11-22 21:19:19 +03:00
2018-11-26 21:09:18 +03:00
Port ->
"Port"
2018-11-26 21:16:04 +03:00
variableTypeWarning : VariableType -> String
2018-11-26 21:09:18 +03:00
variableTypeWarning value =
case value of
Variable ->
""
ImportedModule ->
""
2018-11-26 21:16:04 +03:00
ImportedVariable ->
""
ImportedType ->
""
ImportedOperator ->
""
2018-11-26 21:09:18 +03:00
ModuleAlias ->
""
Type ->
""
Port ->
" (Warning: Removing this port may break your application if it is used in the JS code)"
2018-11-22 21:19:19 +03:00
2019-06-24 01:32:27 +03:00
moduleDefinitionVisitor : Node Module -> Context -> ( List Error, Context )
moduleDefinitionVisitor moduleNode context =
case Module.exposingList (Node.value moduleNode) of
2018-11-22 21:19:19 +03:00
All _ ->
( [], { context | exposesEverything = True } )
2018-11-22 21:19:19 +03:00
Explicit list ->
let
names =
List.filterMap
(\node ->
case Node.value node of
2018-11-22 21:19:19 +03:00
FunctionExpose name ->
Just name
TypeOrAliasExpose name ->
Just name
TypeExpose { name } ->
Just name
InfixExpose name ->
-- Just name
Nothing
)
list
in
( [], markAllAsUsed names context )
2018-11-22 21:19:19 +03:00
2019-06-24 01:32:27 +03:00
importVisitor : Node Import -> Context -> ( List Error, Context )
importVisitor node context =
2018-11-22 21:19:19 +03:00
let
exposed : Maybe Exposing
2018-11-26 13:22:43 +03:00
exposed =
node
|> Node.value
2018-11-22 21:19:19 +03:00
|> .exposingList
|> Maybe.map Node.value
2018-11-22 21:19:19 +03:00
in
case exposed of
2018-11-26 20:12:36 +03:00
Nothing ->
let
2018-11-26 20:39:46 +03:00
( variableType, moduleName ) =
case Node.value node |> .moduleAlias of
2018-11-26 20:39:46 +03:00
Just moduleAlias ->
( ModuleAlias, moduleAlias )
Nothing ->
( ImportedModule, Node.value node |> .moduleName )
2018-11-26 20:12:36 +03:00
in
( []
, register
2018-11-26 20:39:46 +03:00
variableType
(Node.range moduleName)
(Node.value moduleName |> getModuleName)
context
2018-11-26 20:12:36 +03:00
)
2018-11-22 21:19:19 +03:00
2018-11-26 20:12:36 +03:00
Just declaredImports ->
( []
, List.foldl
(\( variableType, range, name ) context_ -> register variableType range name context_)
context
2018-11-26 20:12:36 +03:00
(collectFromExposing declaredImports)
)
2018-11-22 21:19:19 +03:00
2019-06-24 01:32:27 +03:00
expressionVisitor : Node Expression -> Direction -> Context -> ( List Error, Context )
expressionVisitor node direction context =
case ( direction, Node.value node ) of
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, FunctionOrValue [] name ) ->
( [], markAsUsed name context )
2018-11-22 21:19:19 +03:00
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, FunctionOrValue moduleName name ) ->
( [], markAsUsed (getModuleName moduleName) context )
2018-11-22 21:19:19 +03:00
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, OperatorApplication name _ _ _ ) ->
( [], markAsUsed name context )
2018-11-22 21:19:19 +03:00
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, PrefixOperator name ) ->
( [], markAsUsed name context )
2018-11-22 21:19:19 +03:00
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, LetExpression { declarations } ) ->
2018-11-26 13:22:43 +03:00
let
newContext : Context
2018-11-26 13:22:43 +03:00
newContext =
List.foldl
(\declaration context_ ->
case Node.value declaration of
2018-11-26 13:22:43 +03:00
LetFunction function ->
registerFunction function context_
2018-11-22 21:19:19 +03:00
2018-11-26 13:22:43 +03:00
LetDestructuring pattern _ ->
context_
2018-11-26 13:22:43 +03:00
)
{ context | scopes = Nonempty.cons emptyScope context.scopes }
2018-11-26 13:22:43 +03:00
declarations
in
( [], newContext )
( Rule.OnExit, RecordUpdateExpression expr _ ) ->
( [], markAsUsed (Node.value expr) context )
2019-06-26 12:42:17 +03:00
( Rule.OnExit, LetExpression _ ) ->
2018-11-26 13:22:43 +03:00
let
( errors, remainingUsed ) =
makeReport (Nonempty.head context.scopes)
2018-11-26 13:22:43 +03:00
contextWithPoppedScope =
{ context | scopes = Nonempty.pop context.scopes }
2018-11-26 13:22:43 +03:00
in
( errors
, markAllAsUsed remainingUsed contextWithPoppedScope
2018-11-26 13:22:43 +03:00
)
_ ->
( [], context )
2018-11-22 21:19:19 +03:00
2019-06-24 01:32:27 +03:00
declarationVisitor : Node Declaration -> Direction -> Context -> ( List Error, Context )
declarationVisitor node direction context =
case ( direction, Node.value node ) of
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, FunctionDeclaration function ) ->
2018-11-22 21:19:19 +03:00
let
functionImplementation : FunctionImplementation
functionImplementation =
Node.value function.declaration
2018-11-22 21:19:19 +03:00
namesUsedInSignature : List String
2018-11-22 21:19:19 +03:00
namesUsedInSignature =
function.signature
|> Maybe.map (Node.value >> .typeAnnotation >> collectNamesFromTypeAnnotation)
2018-11-22 21:19:19 +03:00
|> Maybe.withDefault []
newContext : Context
2018-11-22 21:19:19 +03:00
newContext =
context
|> register Variable (Node.range functionImplementation.name) (Node.value functionImplementation.name)
2018-11-22 21:19:19 +03:00
|> markAllAsUsed namesUsedInSignature
in
( [], newContext )
( Rule.OnEnter, CustomTypeDeclaration { name, constructors } ) ->
let
variablesFromConstructorArguments : List String
variablesFromConstructorArguments =
constructors
|> List.concatMap (Node.value >> .arguments)
|> List.concatMap collectNamesFromTypeAnnotation
typeName : String
typeName =
Node.value name
constructorsForType : Dict String String
constructorsForType =
constructors
|> List.map (Node.value >> .name >> Node.value)
|> List.map (\constructorName -> ( constructorName, typeName ))
|> Dict.fromList
in
( []
, { context | constructorNameToTypeName = Dict.union constructorsForType context.constructorNameToTypeName }
|> register Type (Node.range name) (Node.value name)
|> markAllAsUsed variablesFromConstructorArguments
)
2018-11-22 21:19:19 +03:00
( Rule.OnEnter, AliasDeclaration { name, typeAnnotation } ) ->
( []
, context
|> register Type (Node.range name) (Node.value name)
|> markAllAsUsed (collectNamesFromTypeAnnotation typeAnnotation)
)
2018-11-22 21:19:19 +03:00
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, PortDeclaration { name, typeAnnotation } ) ->
2018-11-26 21:09:18 +03:00
( []
, context
2018-11-26 21:09:18 +03:00
|> markAllAsUsed (collectNamesFromTypeAnnotation typeAnnotation)
|> register Port (Node.range name) (Node.value name)
2018-11-26 21:09:18 +03:00
)
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, InfixDeclaration _ ) ->
( [], context )
2018-11-26 21:09:18 +03:00
2019-06-26 12:42:17 +03:00
( Rule.OnEnter, Destructuring _ _ ) ->
( [], context )
2018-11-26 21:09:18 +03:00
2019-06-26 12:42:17 +03:00
( Rule.OnExit, _ ) ->
( [], context )
2018-11-22 21:19:19 +03:00
finalEvaluation : Context -> List Error
finalEvaluation context =
if context.exposesEverything then
[]
else
let
rootScope : Scope
rootScope =
Nonempty.head context.scopes
namesOfCustomTypesUsedByCallingAConstructor : Set String
namesOfCustomTypesUsedByCallingAConstructor =
context.constructorNameToTypeName
|> Dict.filter (\usedName _ -> Set.member usedName rootScope.used)
|> Dict.values
|> Set.fromList
newRootScope : Scope
newRootScope =
{ rootScope | used = Set.union namesOfCustomTypesUsedByCallingAConstructor rootScope.used }
in
newRootScope
|> makeReport
|> Tuple.first
2018-11-26 13:22:43 +03:00
registerFunction : Function -> Context -> Context
registerFunction function context =
2018-11-26 13:22:43 +03:00
let
declaration : FunctionImplementation
2018-11-26 13:22:43 +03:00
declaration =
Node.value function.declaration
namesUsedInSignature : List String
namesUsedInSignature =
case Maybe.map Node.value function.signature of
Just signature ->
collectNamesFromTypeAnnotation signature.typeAnnotation
Nothing ->
[]
2018-11-26 13:22:43 +03:00
in
context
|> register Variable (Node.range declaration.name) (Node.value declaration.name)
|> markAllAsUsed namesUsedInSignature
2018-11-26 13:22:43 +03:00
2018-11-26 21:16:04 +03:00
collectFromExposing : Exposing -> List ( VariableType, Range, String )
2018-11-26 13:22:43 +03:00
collectFromExposing exposing_ =
case exposing_ of
All _ ->
[]
Explicit list ->
List.filterMap
(\node ->
case Node.value node of
2018-11-26 13:22:43 +03:00
FunctionExpose name ->
Just ( ImportedVariable, Node.range node, name )
2018-11-26 13:22:43 +03:00
InfixExpose name ->
Just ( ImportedOperator, Node.range node, name )
2018-11-26 13:22:43 +03:00
2018-11-26 20:12:36 +03:00
TypeOrAliasExpose name ->
Just ( ImportedType, Node.range node, name )
2018-11-26 20:12:36 +03:00
TypeExpose { name, open } ->
case open of
Just openRange ->
Nothing
Nothing ->
Just ( ImportedType, Node.range node, name )
2018-11-26 13:22:43 +03:00
)
list
2018-11-22 21:19:19 +03:00
collectNamesFromTypeAnnotation : Node TypeAnnotation -> List String
collectNamesFromTypeAnnotation node =
case Node.value node of
2018-11-22 21:19:19 +03:00
FunctionTypeAnnotation a b ->
collectNamesFromTypeAnnotation a ++ collectNamesFromTypeAnnotation b
Typed nameNode params ->
let
name : String
2018-11-22 21:19:19 +03:00
name =
case Node.value nameNode of
2018-11-26 21:09:18 +03:00
( [], str ) ->
str
( moduleName, _ ) ->
getModuleName moduleName
2018-11-22 21:19:19 +03:00
in
name :: List.concatMap collectNamesFromTypeAnnotation params
Record list ->
list
|> List.map (Node.value >> Tuple.second)
2018-11-22 21:19:19 +03:00
|> List.concatMap collectNamesFromTypeAnnotation
GenericRecord name list ->
list
|> Node.value
|> List.map (Node.value >> Tuple.second)
2018-11-22 21:19:19 +03:00
|> List.concatMap collectNamesFromTypeAnnotation
2018-11-26 21:09:18 +03:00
Tupled list ->
List.concatMap collectNamesFromTypeAnnotation list
GenericType _ ->
[]
Unit ->
2018-11-22 21:19:19 +03:00
[]
2018-11-26 21:16:04 +03:00
register : VariableType -> Range -> String -> Context -> Context
register variableType range name context =
2018-11-22 21:19:19 +03:00
let
scopes : Nonempty Scope
2018-11-22 21:19:19 +03:00
scopes =
mapNonemptyHead
(\scope ->
2018-11-26 20:39:46 +03:00
{ scope | declared = Dict.insert name ( variableType, range ) scope.declared }
2018-11-22 21:19:19 +03:00
)
context.scopes
2018-11-22 21:19:19 +03:00
in
{ context | scopes = scopes }
2018-11-22 21:19:19 +03:00
2018-11-26 13:22:43 +03:00
markAllAsUsed : List String -> Context -> Context
markAllAsUsed names context =
List.foldl markAsUsed context names
2018-11-26 13:22:43 +03:00
2018-11-22 21:19:19 +03:00
markAsUsed : String -> Context -> Context
markAsUsed name context =
2018-11-22 21:19:19 +03:00
let
scopes : Nonempty Scope
2018-11-22 21:19:19 +03:00
scopes =
mapNonemptyHead
(\scope ->
{ scope | used = Set.insert name scope.used }
)
context.scopes
2018-11-22 21:19:19 +03:00
in
{ context | scopes = scopes }
2018-11-22 21:19:19 +03:00
2018-11-26 13:22:43 +03:00
getModuleName : List String -> String
getModuleName name =
String.join "." name
2018-11-22 21:19:19 +03:00
makeReport : Scope -> ( List Error, List String )
makeReport { declared, used } =
let
2019-06-24 01:32:27 +03:00
nonUsedVars : List String
2018-11-22 21:19:19 +03:00
nonUsedVars =
Set.diff used (Set.fromList <| Dict.keys declared)
|> Set.toList
2019-06-24 01:32:27 +03:00
errors : List Error
2018-11-22 21:19:19 +03:00
errors =
Dict.filter (\key _ -> not <| Set.member key used) declared
|> Dict.toList
2018-11-26 20:39:46 +03:00
|> List.map (\( key, ( variableType, range ) ) -> error variableType range key)
2018-11-22 21:19:19 +03:00
in
( errors, nonUsedVars )
mapNonemptyHead : (a -> a) -> Nonempty a -> Nonempty a
mapNonemptyHead fn nonempty =
let
newHead : a
2018-11-22 21:19:19 +03:00
newHead =
fn (Nonempty.head nonempty)
in
Nonempty.replaceHead newHead nonempty