elm-review/tests/NoUnused/Exports.elm

624 lines
20 KiB
Elm
Raw Normal View History

module NoUnused.Exports exposing (rule)
2020-01-12 02:28:20 +03:00
{-| Forbid the use of exposed elements that are never used in your project.
2020-01-12 02:28:20 +03:00
# 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
2020-01-12 02:28:20 +03:00
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Expression as Expression exposing (Expression)
2020-06-03 19:23:19 +03:00
import Elm.Syntax.Import exposing (Import)
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-06-03 19:23:19 +03:00
import Review.Fix as Fix exposing (Fix)
2020-01-12 02:28:20 +03:00
import Review.Rule as Rule exposing (Error, Rule)
import Scope
2020-01-12 02:28:20 +03:00
import Set exposing (Set)
{-| Report functions and types that are exposed from a module but that are never
used in other modules.
2020-01-12 02:28:20 +03:00
If the project is a package and the module that declared the element is exposed,
then nothing will be reported.
2020-01-12 02:28:20 +03:00
config =
[ NoUnused.Exports.rule
2020-01-12 02:28:20 +03:00
]
-}
rule : Rule
rule =
Rule.newProjectRuleSchema "NoUnused.Exports" initialProjectContext
2020-03-19 21:28:01 +03:00
|> Scope.addProjectVisitors
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContext
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.withContextFromImportedModules
|> Rule.withElmJsonProjectVisitor elmJsonVisitor
|> Rule.withFinalProjectEvaluation finalEvaluationForProject
2020-01-19 22:37:19 +03:00
|> Rule.fromProjectRuleSchema
2020-01-12 02:28:20 +03:00
moduleVisitor : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema =
schema
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
2020-06-03 19:23:19 +03:00
|> Rule.withImportVisitor importVisitor
|> Rule.withDeclarationListVisitor declarationListVisitor
|> Rule.withExpressionEnterVisitor expressionVisitor
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
2020-02-16 23:54:05 +03:00
{ moduleKey : Rule.ModuleKey
2020-06-03 19:23:19 +03:00
, exposed : Dict String ExposedElement
2020-01-12 02:28:20 +03:00
}
, used : Set ( ModuleName, String )
2020-01-12 02:28:20 +03:00
}
2020-06-03 19:23:19 +03:00
type alias ExposedElement =
{ range : Range
, rangeToRemove : Maybe Range
, elementType : ExposedElementType
}
type ProjectType
= IsApplication
| IsPackage (Set (List String))
2020-06-03 19:23:19 +03:00
type ExposedElementType
2020-01-12 02:28:20 +03:00
= Function
| TypeOrTypeAlias
| ExposedType
type alias ModuleContext =
{ scope : Scope.ModuleContext
, exposesEverything : Bool
2020-06-03 19:23:19 +03:00
, exposed : Dict String ExposedElement
, used : Set ( ModuleName, String )
, elementsNotToReport : Set String
2020-01-12 02:28:20 +03:00
}
initialProjectContext : ProjectContext
initialProjectContext =
{ scope = Scope.initialProjectContext
, projectType = IsApplication
, modules = Dict.empty
, used = Set.empty
2020-01-12 02:28:20 +03:00
}
2020-02-16 23:54:05 +03:00
fromProjectToModule : Rule.ModuleKey -> Node ModuleName -> ProjectContext -> ModuleContext
2020-06-03 19:23:19 +03:00
fromProjectToModule _ _ 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
}
2020-02-16 23:54:05 +03:00
fromModuleToProject : Rule.ModuleKey -> Node ModuleName -> ModuleContext -> ProjectContext
fromModuleToProject moduleKey moduleName moduleContext =
{ scope = Scope.fromModuleToProject moduleName moduleContext.scope
, projectType = IsApplication
, modules =
2020-01-12 02:28:20 +03:00
Dict.singleton
(Node.value moduleName)
2020-02-16 23:54:05 +03:00
{ moduleKey = moduleKey
2020-01-12 02:28:20 +03:00
, 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 newContext.scope previousContext.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
elmJsonVisitor : Maybe { a | project : Elm.Project.Project } -> ProjectContext -> ( List nothing, ProjectContext )
2020-01-19 22:37:19 +03:00
elmJsonVisitor maybeProject projectContext =
case maybeProject |> Maybe.map .project 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
( []
, { projectContext
| projectType =
exposedModuleNames
|> List.map (Elm.Module.toString >> String.split ".")
|> Set.fromList
|> IsPackage
}
)
_ ->
( [], { projectContext | projectType = IsApplication } )
2020-01-26 15:25:09 +03:00
-- PROJECT EVALUATION
2020-01-12 02:28:20 +03:00
finalEvaluationForProject : ProjectContext -> List (Error { useErrorForModule : () })
2020-01-19 22:37:19 +03:00
finalEvaluationForProject projectContext =
projectContext.modules
|> removeExposedPackages projectContext
|> Dict.toList
2020-01-12 02:28:20 +03:00
|> List.concatMap
2020-02-16 23:54:05 +03:00
(\( moduleName, { moduleKey, exposed } ) ->
2020-01-12 02:28:20 +03:00
exposed
2020-06-03 19:23:19 +03:00
|> removeApplicationExceptions projectContext
|> 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.concatMap
2020-06-03 19:23:19 +03:00
(\( name, element ) ->
2020-01-14 14:50:07 +03:00
let
what : String
what =
2020-06-03 19:23:19 +03:00
case element.elementType of
2020-01-14 14:50:07 +03:00
Function ->
"Exposed function or value"
TypeOrTypeAlias ->
"Exposed type or type alias"
ExposedType ->
"Exposed type"
2020-06-03 19:23:19 +03:00
fixes : List Fix
fixes =
case element.rangeToRemove of
Just rangeToRemove ->
[ Fix.removeRange rangeToRemove ]
Nothing ->
[]
2020-01-14 14:50:07 +03:00
in
2020-06-03 19:23:19 +03:00
[ Rule.errorForModuleWithFix moduleKey
{ message = what ++ " `" ++ 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." ]
}
2020-06-03 19:23:19 +03:00
element.range
fixes
]
2020-01-12 02:28:20 +03:00
)
)
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-06-03 19:23:19 +03:00
removeApplicationExceptions : ProjectContext -> Dict String a -> Dict String a
removeApplicationExceptions projectContext dict =
2020-01-19 22:37:19 +03:00
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 nothing, ModuleContext )
2020-01-12 02:28:20 +03:00
moduleDefinitionVisitor moduleNode moduleContext =
case Module.exposingList (Node.value moduleNode) of
Exposing.All _ ->
( [], { moduleContext | exposesEverything = True } )
Exposing.Explicit list ->
2020-06-03 19:23:19 +03:00
( [], { moduleContext | exposed = collectExposedElements list } )
2020-01-12 02:28:20 +03:00
2020-06-03 19:23:19 +03:00
collectExposedElements : List (Node Exposing.TopLevelExpose) -> Dict String ExposedElement
collectExposedElements nodes =
let
listWithPreviousRange : List (Maybe Range)
listWithPreviousRange =
Nothing
:: (nodes
|> List.map (Node.range >> Just)
|> List.take (List.length nodes - 1)
)
2020-01-12 02:28:20 +03:00
2020-06-03 19:23:19 +03:00
listWithNextRange : List Range
listWithNextRange =
(nodes
|> List.map Node.range
|> List.drop 1
)
++ [ { start = { row = 0, column = 0 }, end = { row = 0, column = 0 } } ]
in
2020-01-12 02:28:20 +03:00
nodes
2020-06-03 19:23:19 +03:00
|> List.map3 (\prev next current -> ( prev, current, next )) listWithPreviousRange listWithNextRange
|> List.indexedMap
(\index ( maybePreviousRange, Node range value, nextRange ) ->
let
rangeToRemove : Maybe Range
rangeToRemove =
if List.length nodes == 1 then
Nothing
else if index == 0 then
Just { range | end = nextRange.start }
else
case maybePreviousRange of
Nothing ->
Just range
Just previousRange ->
Just { range | start = previousRange.end }
in
case value of
2020-01-12 02:28:20 +03:00
Exposing.FunctionExpose name ->
2020-06-03 19:23:19 +03:00
Just
( name
, { range = untilEndOfVariable name range
, rangeToRemove = rangeToRemove
, elementType = Function
}
)
2020-01-12 02:28:20 +03:00
Exposing.TypeOrAliasExpose name ->
2020-06-03 19:23:19 +03:00
Just
( name
, { range = untilEndOfVariable name range
, rangeToRemove = rangeToRemove
, elementType = TypeOrTypeAlias
}
)
2020-01-12 02:28:20 +03:00
Exposing.TypeExpose { name } ->
2020-06-03 19:23:19 +03:00
Just
( name
, { range = untilEndOfVariable name range
, rangeToRemove = Nothing
, elementType = ExposedType
}
)
2020-01-12 02:28:20 +03:00
2020-05-14 22:01:53 +03:00
Exposing.InfixExpose _ ->
2020-01-12 02:28:20 +03:00
Nothing
)
2020-06-03 19:23:19 +03:00
|> List.filterMap identity
2020-01-12 02:28:20 +03:00
|> Dict.fromList
2020-05-14 22:01:53 +03:00
untilEndOfVariable : String -> Range -> Range
untilEndOfVariable name range =
if range.start.row == range.end.row then
range
else
{ range | end = { row = range.start.row, column = range.start.column + String.length name } }
2020-06-03 19:23:19 +03:00
-- IMPORT VISITOR
importVisitor : Node Import -> ModuleContext -> ( List nothing, ModuleContext )
importVisitor node moduleContext =
case (Node.value node).exposingList |> Maybe.map Node.value of
Just (Exposing.Explicit list) ->
let
moduleName : ModuleName
moduleName =
Node.value (Node.value node).moduleName
usedElements : List ( ModuleName, String )
usedElements =
list
|> List.filterMap
(Node.value
>> (\element ->
case element of
Exposing.FunctionExpose name ->
Just ( moduleName, name )
Exposing.TypeOrAliasExpose name ->
Just ( moduleName, name )
Exposing.TypeExpose { name } ->
Just ( moduleName, name )
Exposing.InfixExpose _ ->
Nothing
)
)
in
( [], registerMultipleAsUsed usedElements moduleContext )
Just (Exposing.All _) ->
( [], moduleContext )
Nothing ->
( [], moduleContext )
2020-01-14 14:50:07 +03:00
-- DECLARATION LIST VISITOR
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ( List nothing, ModuleContext )
2020-01-14 14:50:07 +03:00
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 )) _) ->
2020-05-16 23:33:36 +03:00
if name == "Test" && Scope.moduleNameForType scope name moduleName == [ "Test" ] then
function.declaration
|> Node.value
|> .name
|> Node.value
|> Just
else
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 <|
2020-06-03 19:23:19 +03:00
case Dict.get (Node.value type_.name) moduleContext.exposed |> Maybe.map .elementType 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 ->
2020-05-16 23:33:36 +03:00
( Scope.moduleNameForType scope name moduleName, name )
:: 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 -> ModuleContext -> ( List nothing, ModuleContext )
expressionVisitor node moduleContext =
case Node.value node of
Expression.FunctionOrValue moduleName name ->
( []
, registerAsUsed
2020-05-16 23:33:36 +03:00
( Scope.moduleNameForValue moduleContext.scope name moduleName, name )
moduleContext
)
_ ->
( [], moduleContext )