elm-review/src/NoUnusedExports.elm

512 lines
17 KiB
Elm
Raw Normal View History

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
import Elm.Syntax.Expression as Expression exposing (Expression)
2020-01-12 02:28:20 +03:00
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node(..))
2020-01-12 02:28:20 +03:00
import Elm.Syntax.Range exposing (Range)
2020-01-14 14:50:07 +03:00
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
2020-01-12 02:28:20 +03:00
import Review.Rule as Rule exposing (Error, Rule)
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 =
2020-01-19 22:37:19 +03:00
Rule.newProjectRuleSchema "NoUnused.Exports"
2020-01-12 02:28:20 +03:00
{ moduleVisitorSchema =
\schema ->
schema
|> Scope.addModuleVisitors
{ set = \scope context -> { context | scope = scope }
, get = .scope
}
2020-01-12 02:28:20 +03:00
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withDeclarationListVisitor declarationListVisitor
|> Rule.withExpressionVisitor expressionVisitor
2020-01-19 22:37:19 +03:00
, initProjectContext = initProjectContext
, fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
2020-01-19 22:37:19 +03:00
, foldProjectContexts = foldProjectContexts
2020-01-12 02:28:20 +03:00
}
2020-01-26 15:25:09 +03:00
|> Scope.addProjectVisitors
{ set = \scope context -> { context | scope = scope }
, get = .scope
}
2020-01-12 02:28:20 +03:00
|> Rule.traversingImportedModulesFirst
2020-01-19 22:37:19 +03:00
|> Rule.withProjectElmJsonVisitor elmJsonVisitor
|> Rule.withProjectFinalEvaluation finalEvaluationForProject
|> Rule.fromProjectRuleSchema
2020-01-12 02:28:20 +03:00
-- CONTEXT
2020-01-19 22:37:19 +03:00
type alias ProjectContext =
{ scope : Scope.ProjectContext
, projectType : ProjectType
, modules :
2020-01-12 02:28:20 +03:00
Dict ModuleName
{ fileKey : Rule.FileKey
, exposed : Dict String { range : Range, exposedElement : ExposedElement }
}
, used : Set ( ModuleName, String )
2020-01-12 02:28:20 +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 =
{ scope : Scope.ModuleContext
, exposesEverything : Bool
2020-01-12 02:28:20 +03:00
, exposed : Dict String { range : Range, exposedElement : ExposedElement }
, used : Set ( ModuleName, String )
, elementsNotToReport : Set String
2020-01-12 02:28:20 +03:00
}
2020-01-19 22:37:19 +03:00
initProjectContext : ProjectContext
initProjectContext =
{ scope = Scope.initProjectContext
, projectType = IsApplication
, modules = Dict.empty
, used = Set.empty
2020-01-12 02:28:20 +03:00
}
fromProjectToModule : Rule.FileKey -> Node ModuleName -> ProjectContext -> ModuleContext
fromProjectToModule fileKey moduleName projectContext =
{ scope = Scope.fromProjectToModule projectContext.scope
, exposesEverything = False
2020-01-12 02:28:20 +03:00
, exposed = Dict.empty
, used = Set.empty
, elementsNotToReport = Set.empty
2020-01-12 02:28:20 +03:00
}
fromModuleToProject : Rule.FileKey -> Node ModuleName -> ModuleContext -> ProjectContext
fromModuleToProject fileKey moduleName moduleContext =
{ scope = Scope.fromModuleToProject 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 14:50:07 +03:00
, used =
moduleContext.elementsNotToReport
2020-01-14 14:50:07 +03:00
|> Set.map (Tuple.pair <| Node.value moduleName)
|> Set.union moduleContext.used
2020-01-12 02:28:20 +03:00
}
2020-01-19 22:37:19 +03:00
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts newContext previousContext =
{ scope = Scope.foldProjectContexts 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
}
registerAsUsed : ( ModuleName, String ) -> ModuleContext -> ModuleContext
registerAsUsed ( moduleName, name ) moduleContext =
{ moduleContext | used = Set.insert ( moduleName, name ) moduleContext.used }
2020-01-14 14:50:07 +03:00
registerMultipleAsUsed : List ( ModuleName, String ) -> ModuleContext -> ModuleContext
registerMultipleAsUsed usedElements moduleContext =
{ moduleContext | used = Set.union (Set.fromList usedElements) moduleContext.used }
2020-01-12 02:28:20 +03:00
-- ELM JSON VISITOR
2020-01-19 22:37:19 +03:00
elmJsonVisitor : Maybe Project -> ProjectContext -> ProjectContext
elmJsonVisitor maybeProject projectContext =
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
2020-01-19 22:37:19 +03:00
{ projectContext
| projectType =
exposedModuleNames
|> List.map (Elm.Module.toString >> String.split ".")
|> Set.fromList
|> IsPackage
}
_ ->
2020-01-19 22:37:19 +03:00
{ projectContext | projectType = IsApplication }
2020-01-26 15:25:09 +03:00
-- PROJECT EVALUATION
2020-01-12 02:28:20 +03:00
2020-01-19 22:37:19 +03:00
finalEvaluationForProject : ProjectContext -> List Error
finalEvaluationForProject projectContext =
projectContext.modules
|> removeExposedPackages projectContext
|> Dict.toList
2020-01-12 02:28:20 +03:00
|> List.concatMap
(\( moduleName, { fileKey, exposed } ) ->
2020-01-12 02:28:20 +03:00
exposed
2020-01-19 22:37:19 +03:00
|> removeApplicationExceptions projectContext moduleName
|> removeReviewConfig moduleName
2020-01-19 22:37:19 +03:00
|> Dict.filter (\name _ -> not <| Set.member ( moduleName, name ) projectContext.used)
2020-01-12 02:28:20 +03:00
|> Dict.toList
|> List.map
(\( name, { range, exposedElement } ) ->
2020-01-14 14:50:07 +03:00
let
what : String
what =
case exposedElement of
Function ->
"Exposed function or value"
TypeOrTypeAlias ->
"Exposed type or type alias"
ExposedType ->
"Exposed type"
in
2020-01-12 02:28:20 +03:00
Rule.errorForFile fileKey
2020-01-14 14:50:07 +03:00
{ message = what ++ " `" ++ name ++ "` is never used outside this module."
2020-01-12 02:28:20 +03:00
, 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-19 22:37:19 +03:00
removeExposedPackages : ProjectContext -> Dict ModuleName a -> Dict ModuleName a
removeExposedPackages projectContext dict =
case projectContext.projectType of
IsApplication ->
dict
IsPackage exposedModuleNames ->
Dict.filter (\name _ -> not <| Set.member name exposedModuleNames) dict
2020-01-19 22:37:19 +03:00
removeApplicationExceptions : ProjectContext -> ModuleName -> Dict String a -> Dict String a
removeApplicationExceptions projectContext moduleName dict =
case projectContext.projectType of
IsApplication ->
Dict.remove "main" dict
IsPackage _ ->
dict
2020-01-12 02:28:20 +03:00
removeReviewConfig : ModuleName -> Dict String a -> Dict String a
removeReviewConfig moduleName dict =
if moduleName == [ "ReviewConfig" ] then
Dict.remove "config" dict
else
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 ->
2020-01-14 14:50:07 +03:00
Just <| ( name, { range = Node.range node, exposedElement = TypeOrTypeAlias } )
2020-01-12 02:28:20 +03:00
Exposing.TypeExpose { name } ->
Just <| ( name, { range = Node.range node, exposedElement = ExposedType } )
2020-01-12 02:28:20 +03:00
Exposing.InfixExpose name ->
Nothing
)
|> Dict.fromList
2020-01-14 14:50:07 +03:00
-- DECLARATION LIST VISITOR
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ( List Error, ModuleContext )
declarationListVisitor declarations moduleContext =
let
declaredNames : Set String
declaredNames =
declarations
|> List.filterMap (Node.value >> declarationName)
|> Set.fromList
typesUsedInDeclaration_ : List ( List ( ModuleName, String ), Bool )
typesUsedInDeclaration_ =
declarations
|> List.map (typesUsedInDeclaration moduleContext)
testFunctions : List String
testFunctions =
declarations
|> List.filterMap (testFunctionName moduleContext.scope)
allUsedTypes : List ( ModuleName, String )
allUsedTypes =
typesUsedInDeclaration_
|> List.concatMap Tuple.first
contextWithUsedElements : ModuleContext
contextWithUsedElements =
registerMultipleAsUsed allUsedTypes moduleContext
in
( []
, { contextWithUsedElements
| exposed =
contextWithUsedElements.exposed
|> (if moduleContext.exposesEverything then
identity
else
Dict.filter (\name _ -> Set.member name declaredNames)
)
, elementsNotToReport =
typesUsedInDeclaration_
|> List.concatMap
(\( list, comesFromCustomTypeWithHiddenConstructors ) ->
if comesFromCustomTypeWithHiddenConstructors then
[]
else
List.filter (\( moduleName, name ) -> isType name && moduleName == []) list
)
|> List.map Tuple.second
|> List.append testFunctions
|> Set.fromList
}
)
2020-01-14 14:50:07 +03:00
isType : String -> Bool
isType string =
case String.uncons string of
Nothing ->
False
Just ( char, _ ) ->
Char.isUpper char
declarationName : Declaration -> Maybe String
declarationName declaration =
case declaration of
Declaration.FunctionDeclaration function ->
function.declaration
|> Node.value
|> .name
|> Node.value
|> Just
Declaration.CustomTypeDeclaration type_ ->
Just <| Node.value type_.name
Declaration.AliasDeclaration alias_ ->
Just <| Node.value alias_.name
Declaration.PortDeclaration port_ ->
Just <| Node.value port_.name
Declaration.InfixDeclaration { operator } ->
Just <| Node.value operator
Declaration.Destructuring _ _ ->
Nothing
testFunctionName : Scope.ModuleContext -> Node Declaration -> Maybe String
testFunctionName scope declaration =
case Node.value declaration of
Declaration.FunctionDeclaration function ->
case
function.signature
|> Maybe.map (Node.value >> .typeAnnotation >> Node.value)
of
Just (TypeAnnotation.Typed (Node _ ( moduleName, name )) _) ->
case Scope.realFunctionOrType moduleName name scope of
( [ "Test" ], "Test" ) ->
function.declaration
|> Node.value
|> .name
|> Node.value
|> Just
_ ->
Nothing
_ ->
Nothing
_ ->
Nothing
typesUsedInDeclaration : ModuleContext -> Node Declaration -> ( List ( ModuleName, String ), Bool )
typesUsedInDeclaration moduleContext declaration =
2020-01-14 14:50:07 +03:00
case Node.value declaration of
Declaration.FunctionDeclaration function ->
( function.signature
|> Maybe.map (Node.value >> .typeAnnotation >> collectTypesFromTypeAnnotation moduleContext.scope)
2020-01-14 14:50:07 +03:00
|> Maybe.withDefault []
, False
)
2020-01-14 14:50:07 +03:00
Declaration.CustomTypeDeclaration type_ ->
( type_.constructors
|> List.concatMap (Node.value >> .arguments)
|> List.concatMap (collectTypesFromTypeAnnotation moduleContext.scope)
, not <|
case Dict.get (Node.value type_.name) moduleContext.exposed |> Maybe.map .exposedElement of
Just ExposedType ->
True
_ ->
False
)
Declaration.AliasDeclaration alias_ ->
( collectTypesFromTypeAnnotation moduleContext.scope alias_.typeAnnotation, False )
2020-01-14 14:50:07 +03:00
Declaration.PortDeclaration _ ->
( [], False )
2020-01-14 14:50:07 +03:00
Declaration.InfixDeclaration _ ->
( [], False )
Declaration.Destructuring _ _ ->
( [], False )
collectTypesFromTypeAnnotation : Scope.ModuleContext -> Node TypeAnnotation -> List ( ModuleName, String )
collectTypesFromTypeAnnotation scope node =
2020-01-14 14:50:07 +03:00
case Node.value node of
TypeAnnotation.FunctionTypeAnnotation a b ->
collectTypesFromTypeAnnotation scope a ++ collectTypesFromTypeAnnotation scope b
2020-01-14 14:50:07 +03:00
TypeAnnotation.Typed (Node _ ( moduleName, name )) params ->
Scope.realFunctionOrType moduleName name scope
:: List.concatMap (collectTypesFromTypeAnnotation scope) params
2020-01-14 14:50:07 +03:00
TypeAnnotation.Record list ->
list
|> List.map (Node.value >> Tuple.second)
|> List.concatMap (collectTypesFromTypeAnnotation scope)
2020-01-14 14:50:07 +03:00
TypeAnnotation.GenericRecord name list ->
list
|> Node.value
|> List.map (Node.value >> Tuple.second)
|> List.concatMap (collectTypesFromTypeAnnotation scope)
2020-01-14 14:50:07 +03:00
TypeAnnotation.Tupled list ->
List.concatMap (collectTypesFromTypeAnnotation scope) list
2020-01-14 14:50:07 +03:00
TypeAnnotation.GenericType _ ->
[]
TypeAnnotation.Unit ->
[]
-- 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 ) ->
( []
, registerAsUsed
(Scope.realFunctionOrType moduleName name moduleContext.scope)
moduleContext
)
_ ->
( [], moduleContext )