elm-review/tests/NoUnused/Exports.elm

640 lines
21 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-08-22 00:53:35 +03:00
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
2020-01-12 02:28:20 +03:00
import Review.Rule as Rule exposing (Error, Rule)
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
]
2020-08-09 19:55:15 +03:00
## Try it out
You can try this rule out by running the following command:
```bash
2020-08-22 00:53:35 +03:00
elm - review --template jfmengels/review-unused/example --rules NoUnused.Exports
2020-08-09 19:55:15 +03:00
```
2020-01-12 02:28:20 +03:00
-}
rule : Rule
rule =
Rule.newProjectRuleSchema "NoUnused.Exports" initialProjectContext
|> Rule.withModuleVisitor moduleVisitor
2020-08-22 00:53:35 +03:00
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = Rule.initContextCreator fromProjectToModule |> Rule.withModuleNameLookupTable
, fromModuleToProject = Rule.initContextCreator fromModuleToProject |> Rule.withModuleKey |> Rule.withMetadata
, 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 =
2020-08-22 00:53:35 +03:00
{ 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 =
2020-08-22 00:53:35 +03:00
{ lookupTable : ModuleNameLookupTable
, 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 =
2020-08-22 00:53:35 +03:00
{ projectType = IsApplication
, modules = Dict.empty
, used = Set.empty
2020-01-12 02:28:20 +03:00
}
2020-08-22 00:53:35 +03:00
fromProjectToModule : ModuleNameLookupTable -> ProjectContext -> ModuleContext
fromProjectToModule lookupTable _ =
{ lookupTable = lookupTable
, 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-08-22 00:53:35 +03:00
fromModuleToProject : Rule.ModuleKey -> Rule.Metadata -> ModuleContext -> ProjectContext
fromModuleToProject moduleKey metadata moduleContext =
{ projectType = IsApplication
, modules =
2020-01-12 02:28:20 +03:00
Dict.singleton
2020-08-22 00:53:35 +03:00
(Rule.moduleNameFromMetadata metadata)
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-08-22 00:53:35 +03:00
|> Set.map (Tuple.pair <| Rule.moduleNameFromMetadata metadata)
2020-01-14 14:50:07 +03:00
|> 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 =
2020-08-22 00:53:35 +03:00
{ 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
2020-08-22 00:53:35 +03:00
|> List.filterMap (testFunctionName moduleContext)
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
2020-08-22 00:53:35 +03:00
testFunctionName : ModuleContext -> Node Declaration -> Maybe String
testFunctionName moduleContext node =
case Node.value node of
Declaration.FunctionDeclaration function ->
case
function.signature
|> Maybe.map (Node.value >> .typeAnnotation >> Node.value)
of
2020-08-22 00:53:35 +03:00
Just (TypeAnnotation.Typed typeNode _) ->
if
(Tuple.second (Node.value typeNode) == "Test")
&& (ModuleNameLookupTable.moduleNameFor moduleContext.lookupTable typeNode == Just [ "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
2020-08-22 00:53:35 +03:00
|> Maybe.map (Node.value >> .typeAnnotation >> collectTypesFromTypeAnnotation moduleContext)
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)
2020-08-22 00:53:35 +03:00
|> List.concatMap (collectTypesFromTypeAnnotation moduleContext)
, 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_ ->
2020-08-22 00:53:35 +03:00
( collectTypesFromTypeAnnotation moduleContext 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 )
2020-08-22 00:53:35 +03:00
collectTypesFromTypeAnnotation : ModuleContext -> Node TypeAnnotation -> List ( ModuleName, String )
collectTypesFromTypeAnnotation moduleContext node =
2020-01-14 14:50:07 +03:00
case Node.value node of
TypeAnnotation.FunctionTypeAnnotation a b ->
2020-08-22 00:53:35 +03:00
collectTypesFromTypeAnnotation moduleContext a ++ collectTypesFromTypeAnnotation moduleContext b
2020-01-14 14:50:07 +03:00
2020-08-22 00:53:35 +03:00
TypeAnnotation.Typed (Node range ( _, name )) params ->
case ModuleNameLookupTable.moduleNameAt moduleContext.lookupTable range of
Just moduleName ->
( moduleName, name ) :: List.concatMap (collectTypesFromTypeAnnotation moduleContext) params
Nothing ->
List.concatMap (collectTypesFromTypeAnnotation moduleContext) params
2020-01-14 14:50:07 +03:00
TypeAnnotation.Record list ->
list
|> List.map (Node.value >> Tuple.second)
2020-08-22 00:53:35 +03:00
|> List.concatMap (collectTypesFromTypeAnnotation moduleContext)
2020-01-14 14:50:07 +03:00
2020-08-22 00:53:35 +03:00
TypeAnnotation.GenericRecord _ list ->
2020-01-14 14:50:07 +03:00
list
|> Node.value
|> List.map (Node.value >> Tuple.second)
2020-08-22 00:53:35 +03:00
|> List.concatMap (collectTypesFromTypeAnnotation moduleContext)
2020-01-14 14:50:07 +03:00
TypeAnnotation.Tupled list ->
2020-08-22 00:53:35 +03:00
List.concatMap (collectTypesFromTypeAnnotation moduleContext) 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
2020-08-22 00:53:35 +03:00
Expression.FunctionOrValue _ name ->
case ModuleNameLookupTable.moduleNameFor moduleContext.lookupTable node of
Just moduleName ->
( []
, registerAsUsed
( moduleName, name )
moduleContext
)
Nothing ->
( [], moduleContext )
_ ->
( [], moduleContext )