2018-11-22 21:19:19 +03:00
|
|
|
module Lint.Rule.NoUnusedVariables exposing (rule)
|
|
|
|
|
|
|
|
{-|
|
|
|
|
|
|
|
|
@docs rule
|
|
|
|
|
|
|
|
|
|
|
|
# Fail
|
|
|
|
|
|
|
|
a n =
|
|
|
|
n + 1
|
|
|
|
|
|
|
|
b =
|
|
|
|
a 2
|
|
|
|
|
|
|
|
|
|
|
|
# Success
|
|
|
|
|
|
|
|
a n =
|
|
|
|
n + 1
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
import Dict exposing (Dict)
|
|
|
|
import Elm.Syntax.Declaration exposing (Declaration(..))
|
|
|
|
import Elm.Syntax.Exposing exposing (Exposing(..), TopLevelExpose(..))
|
|
|
|
import Elm.Syntax.Expression exposing (Expression(..), Function, LetDeclaration(..))
|
|
|
|
import Elm.Syntax.Import exposing (Import)
|
|
|
|
import Elm.Syntax.Module as Module exposing (Module(..))
|
|
|
|
import Elm.Syntax.Node exposing (Node, range, value)
|
|
|
|
import Elm.Syntax.Range exposing (Range)
|
|
|
|
import Elm.Syntax.TypeAnnotation exposing (TypeAnnotation(..))
|
|
|
|
import Lint exposing (Rule, lint)
|
|
|
|
import Lint.Error exposing (Error)
|
|
|
|
import Lint.Rule exposing (Direction(..), Implementation, createRule)
|
|
|
|
import List.Nonempty as Nonempty exposing (Nonempty)
|
|
|
|
import Set exposing (Set)
|
|
|
|
|
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
{-| Reports variables that are declared but never used.
|
|
|
|
|
|
|
|
rules =
|
|
|
|
[ NoUnusedVariables.rule
|
|
|
|
]
|
|
|
|
|
|
|
|
-}
|
|
|
|
rule : Rule
|
|
|
|
rule input =
|
|
|
|
lint input implementation
|
|
|
|
|
|
|
|
|
2018-11-26 20:39:46 +03:00
|
|
|
type Value
|
|
|
|
= Variable
|
|
|
|
| ImportedModule
|
|
|
|
| ModuleAlias
|
|
|
|
| Type
|
2018-11-26 21:09:18 +03:00
|
|
|
| Port
|
2018-11-26 20:39:46 +03:00
|
|
|
|
|
|
|
|
2018-11-22 21:19:19 +03:00
|
|
|
type alias Scope =
|
2018-11-26 20:39:46 +03:00
|
|
|
{ declared : Dict String ( Value, Range )
|
2018-11-22 21:19:19 +03:00
|
|
|
, used : Set String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias Context =
|
|
|
|
{ scopes : Nonempty Scope
|
|
|
|
, exposesEverything : Bool
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
emptyScope : Scope
|
|
|
|
emptyScope =
|
|
|
|
Scope Dict.empty Set.empty
|
|
|
|
|
|
|
|
|
2018-11-26 20:39:46 +03:00
|
|
|
error : Value -> Range -> String -> Error
|
|
|
|
error variableType range_ name =
|
|
|
|
Error
|
|
|
|
"NoUnusedVariables"
|
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_
|
|
|
|
|
|
|
|
|
|
|
|
variableTypeToString : Value -> String
|
|
|
|
variableTypeToString value =
|
|
|
|
case value of
|
|
|
|
Variable ->
|
|
|
|
"Variable"
|
|
|
|
|
|
|
|
ImportedModule ->
|
|
|
|
"Imported module"
|
|
|
|
|
|
|
|
ModuleAlias ->
|
|
|
|
"Module alias"
|
|
|
|
|
|
|
|
Type ->
|
|
|
|
"Type"
|
2018-11-22 21:19:19 +03:00
|
|
|
|
2018-11-26 21:09:18 +03:00
|
|
|
Port ->
|
|
|
|
"Port"
|
|
|
|
|
|
|
|
|
|
|
|
variableTypeWarning : Value -> String
|
|
|
|
variableTypeWarning value =
|
|
|
|
case value of
|
|
|
|
Variable ->
|
|
|
|
""
|
|
|
|
|
|
|
|
ImportedModule ->
|
|
|
|
""
|
|
|
|
|
|
|
|
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
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
initialContext : Context
|
|
|
|
initialContext =
|
|
|
|
{ scopes = Nonempty.fromElement emptyScope
|
|
|
|
, exposesEverything = False
|
|
|
|
}
|
2018-11-22 21:19:19 +03:00
|
|
|
|
|
|
|
|
|
|
|
implementation : Implementation Context
|
|
|
|
implementation =
|
|
|
|
createRule
|
2018-11-26 13:22:43 +03:00
|
|
|
initialContext
|
2018-11-22 21:19:19 +03:00
|
|
|
(\v ->
|
|
|
|
{ v
|
|
|
|
| visitModuleDefinition = visitModuleDefinition
|
|
|
|
, visitImport = visitImport
|
|
|
|
, visitDeclaration = visitDeclaration
|
|
|
|
, visitExpression = visitExpression
|
|
|
|
, visitEnd = visitEnd
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
visitModuleDefinition : Context -> Node Module -> ( List Error, Context )
|
|
|
|
visitModuleDefinition ctx moduleNode =
|
|
|
|
case Module.exposingList (value moduleNode) of
|
|
|
|
All _ ->
|
|
|
|
( [], { ctx | exposesEverything = True } )
|
|
|
|
|
|
|
|
Explicit list ->
|
|
|
|
let
|
|
|
|
names =
|
|
|
|
List.filterMap
|
|
|
|
(\node ->
|
|
|
|
case value node of
|
|
|
|
FunctionExpose name ->
|
|
|
|
Just name
|
|
|
|
|
|
|
|
TypeOrAliasExpose name ->
|
|
|
|
Just name
|
|
|
|
|
|
|
|
TypeExpose { name } ->
|
|
|
|
Just name
|
|
|
|
|
|
|
|
InfixExpose name ->
|
|
|
|
-- Just name
|
|
|
|
Nothing
|
|
|
|
)
|
|
|
|
list
|
|
|
|
in
|
|
|
|
( [], markAllAsUsed names ctx )
|
|
|
|
|
|
|
|
|
|
|
|
visitImport : Context -> Node Import -> ( List Error, Context )
|
|
|
|
visitImport ctx node =
|
|
|
|
let
|
2018-11-26 13:22:43 +03:00
|
|
|
exposed =
|
|
|
|
node
|
|
|
|
|> value
|
2018-11-22 21:19:19 +03:00
|
|
|
|> .exposingList
|
|
|
|
in
|
2018-11-26 13:22:43 +03:00
|
|
|
case Maybe.map value exposed of
|
2018-11-26 20:12:36 +03:00
|
|
|
Nothing ->
|
|
|
|
let
|
2018-11-26 20:39:46 +03:00
|
|
|
( variableType, moduleName ) =
|
|
|
|
case value node |> .moduleAlias of
|
|
|
|
Just moduleAlias ->
|
|
|
|
( ModuleAlias, moduleAlias )
|
|
|
|
|
|
|
|
Nothing ->
|
|
|
|
( ImportedModule, value node |> .moduleName )
|
2018-11-26 20:12:36 +03:00
|
|
|
in
|
|
|
|
( []
|
|
|
|
, register
|
2018-11-26 20:39:46 +03:00
|
|
|
variableType
|
2018-11-26 20:12:36 +03:00
|
|
|
(range moduleName)
|
|
|
|
(value moduleName |> getModuleName)
|
|
|
|
ctx
|
|
|
|
)
|
2018-11-22 21:19:19 +03:00
|
|
|
|
2018-11-26 20:12:36 +03:00
|
|
|
Just declaredImports ->
|
|
|
|
( []
|
|
|
|
, List.foldl
|
2018-11-26 20:39:46 +03:00
|
|
|
(\( range, name ) context -> register Variable range name context)
|
2018-11-26 20:12:36 +03:00
|
|
|
ctx
|
|
|
|
(collectFromExposing declaredImports)
|
|
|
|
)
|
2018-11-22 21:19:19 +03:00
|
|
|
|
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
visitExpression : Context -> Direction -> Node Expression -> ( List Error, Context )
|
|
|
|
visitExpression ctx direction node =
|
|
|
|
case ( direction, value node ) of
|
|
|
|
( Enter, FunctionOrValue [] name ) ->
|
|
|
|
( [], markAsUsed name ctx )
|
2018-11-22 21:19:19 +03:00
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
( Enter, FunctionOrValue moduleName name ) ->
|
|
|
|
( [], markAsUsed (getModuleName moduleName) ctx )
|
2018-11-22 21:19:19 +03:00
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
( Enter, OperatorApplication name _ _ _ ) ->
|
|
|
|
( [], markAsUsed name ctx )
|
2018-11-22 21:19:19 +03:00
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
( Enter, PrefixOperator name ) ->
|
|
|
|
( [], markAsUsed name ctx )
|
2018-11-22 21:19:19 +03:00
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
( Enter, LetExpression { declarations } ) ->
|
|
|
|
let
|
|
|
|
newContext =
|
|
|
|
List.foldl
|
|
|
|
(\declaration context ->
|
|
|
|
case value declaration of
|
|
|
|
LetFunction function ->
|
|
|
|
registerFunction function context
|
2018-11-22 21:19:19 +03:00
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
LetDestructuring pattern _ ->
|
|
|
|
context
|
|
|
|
)
|
|
|
|
{ ctx | scopes = Nonempty.cons emptyScope ctx.scopes }
|
|
|
|
declarations
|
|
|
|
in
|
|
|
|
( [], newContext )
|
|
|
|
|
|
|
|
( Exit, LetExpression _ ) ->
|
|
|
|
let
|
|
|
|
( errors, remainingUsed ) =
|
|
|
|
makeReport (Nonempty.head ctx.scopes)
|
|
|
|
|
|
|
|
ctxWithPoppedScope =
|
|
|
|
{ ctx | scopes = Nonempty.pop ctx.scopes }
|
|
|
|
in
|
|
|
|
( errors
|
|
|
|
, markAllAsUsed remainingUsed ctxWithPoppedScope
|
|
|
|
)
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
( [], ctx )
|
2018-11-22 21:19:19 +03:00
|
|
|
|
|
|
|
|
|
|
|
visitDeclaration : Context -> Direction -> Node Declaration -> ( List Error, Context )
|
|
|
|
visitDeclaration ctx direction node =
|
|
|
|
case ( direction, value node ) of
|
|
|
|
( Enter, FunctionDeclaration function ) ->
|
|
|
|
let
|
|
|
|
declaration =
|
|
|
|
value function.declaration
|
|
|
|
|
|
|
|
namesUsedInSignature =
|
|
|
|
function.signature
|
|
|
|
|> Maybe.map (value >> .typeAnnotation >> collectNamesFromTypeAnnotation)
|
|
|
|
|> Maybe.withDefault []
|
|
|
|
|
|
|
|
newContext =
|
|
|
|
ctx
|
2018-11-26 20:39:46 +03:00
|
|
|
|> register Variable (range declaration.name) (value declaration.name)
|
2018-11-22 21:19:19 +03:00
|
|
|
|> markAllAsUsed namesUsedInSignature
|
|
|
|
in
|
|
|
|
( [], newContext )
|
|
|
|
|
|
|
|
( Enter, CustomTypeDeclaration { name } ) ->
|
2018-11-26 20:39:46 +03:00
|
|
|
( [], register Type (range name) (value name) ctx )
|
2018-11-22 21:19:19 +03:00
|
|
|
|
|
|
|
( Enter, AliasDeclaration { name } ) ->
|
2018-11-26 20:39:46 +03:00
|
|
|
( [], register Type (range name) (value name) ctx )
|
2018-11-22 21:19:19 +03:00
|
|
|
|
2018-11-26 21:09:18 +03:00
|
|
|
( Enter, PortDeclaration { name, typeAnnotation } ) ->
|
|
|
|
( []
|
|
|
|
, ctx
|
|
|
|
|> markAllAsUsed (collectNamesFromTypeAnnotation typeAnnotation)
|
|
|
|
|> register Port (range name) (value name)
|
|
|
|
)
|
|
|
|
|
|
|
|
( Enter, InfixDeclaration _ ) ->
|
|
|
|
( [], ctx )
|
|
|
|
|
|
|
|
( Enter, Destructuring _ _ ) ->
|
|
|
|
( [], ctx )
|
|
|
|
|
|
|
|
( Exit, _ ) ->
|
2018-11-22 21:19:19 +03:00
|
|
|
( [], ctx )
|
|
|
|
|
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
visitEnd : Context -> ( List Error, Context )
|
|
|
|
visitEnd ctx =
|
|
|
|
let
|
|
|
|
errors =
|
|
|
|
if ctx.exposesEverything then
|
|
|
|
[]
|
|
|
|
|
|
|
|
else
|
|
|
|
ctx.scopes
|
|
|
|
|> Nonempty.head
|
2018-11-26 20:12:36 +03:00
|
|
|
|> makeReport
|
|
|
|
|> Tuple.first
|
2018-11-26 13:22:43 +03:00
|
|
|
in
|
|
|
|
( errors, ctx )
|
|
|
|
|
|
|
|
|
|
|
|
registerFunction : Function -> Context -> Context
|
|
|
|
registerFunction function ctx =
|
|
|
|
let
|
|
|
|
declaration =
|
|
|
|
value function.declaration
|
|
|
|
in
|
2018-11-26 20:39:46 +03:00
|
|
|
register Variable (range declaration.name) (value declaration.name) ctx
|
2018-11-26 13:22:43 +03:00
|
|
|
|
|
|
|
|
|
|
|
collectFromExposing : Exposing -> List ( Range, String )
|
|
|
|
collectFromExposing exposing_ =
|
|
|
|
case exposing_ of
|
|
|
|
All _ ->
|
|
|
|
[]
|
|
|
|
|
|
|
|
Explicit list ->
|
|
|
|
List.filterMap
|
|
|
|
(\node ->
|
|
|
|
case value node of
|
|
|
|
FunctionExpose name ->
|
|
|
|
Just ( range node, name )
|
|
|
|
|
|
|
|
InfixExpose name ->
|
|
|
|
Just ( range node, name )
|
|
|
|
|
2018-11-26 20:12:36 +03:00
|
|
|
TypeOrAliasExpose name ->
|
|
|
|
Just ( range node, name )
|
|
|
|
|
|
|
|
TypeExpose { name, open } ->
|
|
|
|
case open of
|
|
|
|
Just openRange ->
|
|
|
|
Nothing
|
|
|
|
|
|
|
|
Nothing ->
|
|
|
|
Just ( 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 value node of
|
|
|
|
FunctionTypeAnnotation a b ->
|
|
|
|
collectNamesFromTypeAnnotation a ++ collectNamesFromTypeAnnotation b
|
|
|
|
|
|
|
|
Typed nameNode params ->
|
|
|
|
let
|
|
|
|
name =
|
2018-11-26 21:09:18 +03:00
|
|
|
case value nameNode of
|
|
|
|
( [], str ) ->
|
|
|
|
str
|
|
|
|
|
|
|
|
( moduleName, _ ) ->
|
|
|
|
getModuleName moduleName
|
2018-11-22 21:19:19 +03:00
|
|
|
in
|
|
|
|
name :: List.concatMap collectNamesFromTypeAnnotation params
|
|
|
|
|
|
|
|
Record list ->
|
|
|
|
list
|
|
|
|
|> List.map (value >> Tuple.second)
|
|
|
|
|> List.concatMap collectNamesFromTypeAnnotation
|
|
|
|
|
|
|
|
GenericRecord name list ->
|
|
|
|
list
|
|
|
|
|> value
|
|
|
|
|> List.map (value >> Tuple.second)
|
|
|
|
|> 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 20:39:46 +03:00
|
|
|
register : Value -> Range -> String -> Context -> Context
|
|
|
|
register variableType range name ctx =
|
2018-11-22 21:19:19 +03:00
|
|
|
let
|
|
|
|
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
|
|
|
)
|
|
|
|
ctx.scopes
|
|
|
|
in
|
|
|
|
{ ctx | scopes = scopes }
|
|
|
|
|
|
|
|
|
2018-11-26 13:22:43 +03:00
|
|
|
markAllAsUsed : List String -> Context -> Context
|
|
|
|
markAllAsUsed names ctx =
|
|
|
|
List.foldl markAsUsed ctx names
|
|
|
|
|
|
|
|
|
2018-11-22 21:19:19 +03:00
|
|
|
markAsUsed : String -> Context -> Context
|
|
|
|
markAsUsed name ctx =
|
|
|
|
let
|
|
|
|
scopes =
|
|
|
|
mapNonemptyHead
|
|
|
|
(\scope ->
|
|
|
|
{ scope | used = Set.insert name scope.used }
|
|
|
|
)
|
|
|
|
ctx.scopes
|
|
|
|
in
|
|
|
|
{ ctx | scopes = scopes }
|
|
|
|
|
|
|
|
|
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
|
|
|
|
nonUsedVars =
|
|
|
|
Set.diff used (Set.fromList <| Dict.keys declared)
|
|
|
|
|> Set.toList
|
|
|
|
|
|
|
|
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 =
|
|
|
|
fn (Nonempty.head nonempty)
|
|
|
|
in
|
|
|
|
Nonempty.replaceHead newHead nonempty
|