2020-01-12 02:28:20 +03:00
|
|
|
module NoUnusedExports exposing (rule)
|
|
|
|
|
|
|
|
{-| Forbid the use of modules that are never used in your project.
|
|
|
|
|
|
|
|
|
|
|
|
# Rule
|
|
|
|
|
|
|
|
@docs rule
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
-- TODO Don't report type or type aliases (still `A(..)` though) if they are
|
|
|
|
-- used in exposed function arguments/return values.
|
|
|
|
|
|
|
|
import Dict exposing (Dict)
|
|
|
|
import Elm.Module
|
|
|
|
import Elm.Project exposing (Project)
|
|
|
|
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
|
|
|
|
import Elm.Syntax.Exposing as Exposing
|
2020-01-14 11:48:26 +03:00
|
|
|
import Elm.Syntax.Expression as Expression exposing (Expression)
|
2020-01-12 02:28:20 +03:00
|
|
|
import Elm.Syntax.Import exposing (Import)
|
|
|
|
import Elm.Syntax.Module as Module exposing (Module)
|
|
|
|
import Elm.Syntax.ModuleName exposing (ModuleName)
|
|
|
|
import Elm.Syntax.Node as Node exposing (Node)
|
|
|
|
import Elm.Syntax.Range exposing (Range)
|
|
|
|
import Review.Rule as Rule exposing (Error, Rule)
|
2020-01-14 11:48:26 +03:00
|
|
|
import Scope2 as Scope
|
2020-01-12 02:28:20 +03:00
|
|
|
import Set exposing (Set)
|
|
|
|
|
|
|
|
|
|
|
|
{-| Forbid the use of modules that are never used in your project.
|
|
|
|
|
|
|
|
A module is considered unused if it does not contain a `main` function
|
|
|
|
(be it exposed or not), does not import `Test` module, and is never imported in
|
|
|
|
other modules. For packages, modules listed in the `elm.json`'s
|
|
|
|
`exposed-modules` are considered used. The `ReviewConfig` is also always
|
|
|
|
considered as used.
|
|
|
|
|
|
|
|
A module will be considered as used if it gets imported, even if none of its
|
|
|
|
functions or types are used. Other rules from this package will help detect and
|
|
|
|
remove code so that the import statement is removed.
|
|
|
|
|
|
|
|
config =
|
|
|
|
[ NoUnused.Modules.rule
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# When (not) to use this rule
|
|
|
|
|
|
|
|
You may not want to enable this rule if you are not concerned about having
|
|
|
|
unused modules in your application or package.
|
|
|
|
|
|
|
|
-}
|
|
|
|
rule : Rule
|
|
|
|
rule =
|
|
|
|
Rule.newMultiSchema "NoUnused.Exports"
|
|
|
|
{ moduleVisitorSchema =
|
|
|
|
\schema ->
|
|
|
|
schema
|
2020-01-14 11:48:26 +03:00
|
|
|
|> Scope.addModuleVisitors
|
|
|
|
{ set = \scope context -> { context | scope = scope }
|
|
|
|
, get = .scope
|
|
|
|
}
|
2020-01-12 02:28:20 +03:00
|
|
|
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|
2020-01-14 11:48:26 +03:00
|
|
|
|> Rule.withExpressionVisitor expressionVisitor
|
2020-01-12 02:28:20 +03:00
|
|
|
, initGlobalContext = initGlobalContext
|
2020-01-13 00:46:29 +03:00
|
|
|
, fromGlobalToModule = fromGlobalToModule
|
2020-01-12 02:28:20 +03:00
|
|
|
, fromModuleToGlobal = fromModuleToGlobal
|
2020-01-13 00:49:23 +03:00
|
|
|
, foldGlobalContexts = foldGlobalContexts
|
2020-01-12 02:28:20 +03:00
|
|
|
}
|
2020-01-14 11:48:26 +03:00
|
|
|
|> Scope.addGlobalVisitors
|
|
|
|
{ set = \scope context -> { context | scope = scope }
|
|
|
|
, get = .scope
|
|
|
|
}
|
2020-01-12 02:28:20 +03:00
|
|
|
|> Rule.traversingImportedModulesFirst
|
2020-01-14 11:48:26 +03:00
|
|
|
|> Rule.withMultiElmJsonVisitor elmJsonVisitor
|
2020-01-12 02:28:20 +03:00
|
|
|
|> Rule.withMultiFinalEvaluation finalEvaluationForProject
|
|
|
|
|> Rule.fromMultiSchema
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- CONTEXT
|
|
|
|
|
|
|
|
|
|
|
|
type alias GlobalContext =
|
2020-01-14 11:48:26 +03:00
|
|
|
{ scope : Scope.GlobalContext
|
|
|
|
, projectType : ProjectType
|
|
|
|
, modules :
|
2020-01-12 02:28:20 +03:00
|
|
|
Dict ModuleName
|
|
|
|
{ fileKey : Rule.FileKey
|
|
|
|
, exposed : Dict String { range : Range, exposedElement : ExposedElement }
|
|
|
|
}
|
2020-01-14 11:48:26 +03:00
|
|
|
, used : Set ( ModuleName, String )
|
2020-01-12 02:28:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-01-14 11:48:26 +03:00
|
|
|
type ProjectType
|
|
|
|
= IsApplication
|
|
|
|
| IsPackage (Set (List String))
|
|
|
|
|
|
|
|
|
2020-01-12 02:28:20 +03:00
|
|
|
type ExposedElement
|
|
|
|
= Function
|
|
|
|
| TypeOrTypeAlias
|
|
|
|
| ExposedType
|
|
|
|
|
|
|
|
|
|
|
|
type alias ModuleContext =
|
2020-01-14 11:48:26 +03:00
|
|
|
{ scope : Scope.ModuleContext
|
|
|
|
, exposesEverything : Bool
|
2020-01-12 02:28:20 +03:00
|
|
|
, exposed : Dict String { range : Range, exposedElement : ExposedElement }
|
2020-01-14 11:48:26 +03:00
|
|
|
, used : Set ( ModuleName, String )
|
2020-01-12 02:28:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
initGlobalContext : GlobalContext
|
|
|
|
initGlobalContext =
|
2020-01-14 11:48:26 +03:00
|
|
|
{ scope = Scope.initGlobalContext
|
|
|
|
, projectType = IsApplication
|
|
|
|
, modules = Dict.empty
|
|
|
|
, used = Set.empty
|
2020-01-12 02:28:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-01-13 00:46:29 +03:00
|
|
|
fromGlobalToModule : Rule.FileKey -> Node ModuleName -> GlobalContext -> ModuleContext
|
2020-01-14 11:48:26 +03:00
|
|
|
fromGlobalToModule fileKey moduleName globalContext =
|
|
|
|
{ scope = Scope.fromGlobalToModule globalContext.scope
|
|
|
|
, exposesEverything = False
|
2020-01-12 02:28:20 +03:00
|
|
|
, exposed = Dict.empty
|
2020-01-14 11:48:26 +03:00
|
|
|
, used = Set.empty
|
2020-01-12 02:28:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fromModuleToGlobal : Rule.FileKey -> Node ModuleName -> ModuleContext -> GlobalContext
|
|
|
|
fromModuleToGlobal fileKey moduleName moduleContext =
|
2020-01-14 11:48:26 +03:00
|
|
|
{ scope = Scope.fromModuleToGlobal moduleName moduleContext.scope
|
|
|
|
, projectType = IsApplication
|
|
|
|
, modules =
|
2020-01-12 02:28:20 +03:00
|
|
|
Dict.singleton
|
|
|
|
(Node.value moduleName)
|
|
|
|
{ fileKey = fileKey
|
|
|
|
, exposed = moduleContext.exposed
|
|
|
|
}
|
2020-01-14 11:48:26 +03:00
|
|
|
, used = moduleContext.used
|
2020-01-12 02:28:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-01-13 00:49:23 +03:00
|
|
|
foldGlobalContexts : GlobalContext -> GlobalContext -> GlobalContext
|
2020-01-14 11:48:26 +03:00
|
|
|
foldGlobalContexts newContext previousContext =
|
|
|
|
{ scope = Scope.foldGlobalContexts previousContext.scope newContext.scope
|
|
|
|
, projectType = previousContext.projectType
|
|
|
|
, modules = Dict.union previousContext.modules newContext.modules
|
|
|
|
, used = Set.union newContext.used previousContext.used
|
2020-01-12 02:28:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
error : ( ModuleName, { fileKey : Rule.FileKey, moduleNameLocation : Range } ) -> Error
|
|
|
|
error ( moduleName, { fileKey, moduleNameLocation } ) =
|
|
|
|
Rule.errorForFile fileKey
|
|
|
|
{ message = "Module `" ++ String.join "." moduleName ++ "` is never used."
|
|
|
|
, details = [ "This module is never used. You may want to remove it to keep your project clean, and maybe detect some unused code in your project." ]
|
|
|
|
}
|
|
|
|
moduleNameLocation
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-01-14 11:48:26 +03:00
|
|
|
-- ELM JSON VISITOR
|
|
|
|
|
|
|
|
|
|
|
|
elmJsonVisitor : Maybe Project -> GlobalContext -> GlobalContext
|
|
|
|
elmJsonVisitor maybeProject globalContext =
|
|
|
|
case maybeProject of
|
|
|
|
Just (Elm.Project.Package { exposed }) ->
|
|
|
|
let
|
|
|
|
exposedModuleNames : List Elm.Module.Name
|
|
|
|
exposedModuleNames =
|
|
|
|
case exposed of
|
|
|
|
Elm.Project.ExposedList names ->
|
|
|
|
names
|
|
|
|
|
|
|
|
Elm.Project.ExposedDict fakeDict ->
|
|
|
|
List.concatMap Tuple.second fakeDict
|
|
|
|
in
|
|
|
|
{ globalContext
|
|
|
|
| projectType =
|
|
|
|
exposedModuleNames
|
|
|
|
|> List.map (Elm.Module.toString >> String.split ".")
|
|
|
|
|> Set.fromList
|
|
|
|
|> IsPackage
|
|
|
|
}
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
{ globalContext | projectType = IsApplication }
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-01-12 02:28:20 +03:00
|
|
|
-- GLOBAL EVALUATION
|
|
|
|
|
|
|
|
|
|
|
|
finalEvaluationForProject : GlobalContext -> List Error
|
|
|
|
finalEvaluationForProject globalContext =
|
|
|
|
globalContext.modules
|
2020-01-14 11:48:26 +03:00
|
|
|
|> removeExposedPackages globalContext
|
|
|
|
|> Dict.toList
|
2020-01-12 02:28:20 +03:00
|
|
|
|> List.concatMap
|
2020-01-14 11:48:26 +03:00
|
|
|
(\( moduleName, { fileKey, exposed } ) ->
|
2020-01-12 02:28:20 +03:00
|
|
|
exposed
|
2020-01-14 11:48:26 +03:00
|
|
|
|> removeApplicationExceptions globalContext moduleName
|
|
|
|
|> Dict.filter (\name _ -> not <| Set.member ( moduleName, name ) globalContext.used)
|
2020-01-12 02:28:20 +03:00
|
|
|
|> Dict.toList
|
|
|
|
|> List.map
|
|
|
|
(\( name, { range, exposedElement } ) ->
|
|
|
|
Rule.errorForFile fileKey
|
|
|
|
{ message = "Exposed function or type `" ++ name ++ "` is never used outside this module."
|
|
|
|
, details = [ "This exposed element is never used. You may want to remove it to keep your project clean, and maybe detect some unused code in your project." ]
|
|
|
|
}
|
|
|
|
range
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-01-14 11:48:26 +03:00
|
|
|
removeExposedPackages : GlobalContext -> Dict ModuleName a -> Dict ModuleName a
|
|
|
|
removeExposedPackages globalContext dict =
|
|
|
|
case globalContext.projectType of
|
|
|
|
IsApplication ->
|
|
|
|
dict
|
|
|
|
|
|
|
|
IsPackage exposedModuleNames ->
|
|
|
|
Dict.filter (\name _ -> not <| Set.member name exposedModuleNames) dict
|
|
|
|
|
|
|
|
|
|
|
|
removeApplicationExceptions : GlobalContext -> ModuleName -> Dict String a -> Dict String a
|
|
|
|
removeApplicationExceptions globalContext moduleName dict =
|
|
|
|
case globalContext.projectType of
|
|
|
|
IsApplication ->
|
|
|
|
Dict.remove "main" dict
|
|
|
|
|
|
|
|
IsPackage _ ->
|
|
|
|
dict
|
2020-01-12 02:28:20 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- MODULE DEFINITION VISITOR
|
|
|
|
|
|
|
|
|
|
|
|
moduleDefinitionVisitor : Node Module -> ModuleContext -> ( List Error, ModuleContext )
|
|
|
|
moduleDefinitionVisitor moduleNode moduleContext =
|
|
|
|
case Module.exposingList (Node.value moduleNode) of
|
|
|
|
Exposing.All _ ->
|
|
|
|
( [], { moduleContext | exposesEverything = True } )
|
|
|
|
|
|
|
|
Exposing.Explicit list ->
|
|
|
|
( [], { moduleContext | exposed = exposedElements list } )
|
|
|
|
|
|
|
|
|
|
|
|
exposedElements : List (Node Exposing.TopLevelExpose) -> Dict String { range : Range, exposedElement : ExposedElement }
|
|
|
|
exposedElements nodes =
|
|
|
|
nodes
|
|
|
|
|> List.filterMap
|
|
|
|
(\node ->
|
|
|
|
case Node.value node of
|
|
|
|
Exposing.FunctionExpose name ->
|
|
|
|
Just <| ( name, { range = Node.range node, exposedElement = Function } )
|
|
|
|
|
|
|
|
Exposing.TypeOrAliasExpose name ->
|
|
|
|
-- TODO
|
|
|
|
Nothing
|
|
|
|
|
|
|
|
Exposing.TypeExpose { name } ->
|
|
|
|
-- TODO
|
|
|
|
Nothing
|
|
|
|
|
|
|
|
Exposing.InfixExpose name ->
|
|
|
|
Nothing
|
|
|
|
)
|
|
|
|
|> Dict.fromList
|
2020-01-14 11:48:26 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- EXPRESSION VISITOR
|
|
|
|
|
|
|
|
|
|
|
|
expressionVisitor : Node Expression -> Rule.Direction -> ModuleContext -> ( List Error, ModuleContext )
|
|
|
|
expressionVisitor node direction moduleContext =
|
|
|
|
case ( direction, Node.value node ) of
|
|
|
|
( Rule.OnEnter, Expression.FunctionOrValue moduleName name ) ->
|
|
|
|
( []
|
|
|
|
, { moduleContext
|
|
|
|
| used =
|
|
|
|
Set.insert (Scope.realFunctionOrType moduleName name moduleContext.scope) moduleContext.used
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
( [], moduleContext )
|