mirror of
https://github.com/jfmengels/elm-review.git
synced 2024-11-23 14:55:35 +03:00
972 lines
34 KiB
Elm
972 lines
34 KiB
Elm
module NoUnused.Exports exposing (rule)
|
|
|
|
{-| Forbid the use of exposed elements that are never used in your project.
|
|
|
|
@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
|
|
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
|
|
import Elm.Syntax.Exposing as Exposing exposing (Exposing, TopLevelExpose)
|
|
import Elm.Syntax.Expression as Expression exposing (Expression)
|
|
import Elm.Syntax.Import exposing (Import)
|
|
import Elm.Syntax.Module as Module
|
|
import Elm.Syntax.ModuleName exposing (ModuleName)
|
|
import Elm.Syntax.Node as Node exposing (Node(..))
|
|
import Elm.Syntax.Pattern as Pattern exposing (Pattern)
|
|
import Elm.Syntax.Range as Range exposing (Range)
|
|
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
|
|
import List.Extra
|
|
import NoUnused.LamderaSupport as LamderaSupport
|
|
import Review.Fix as Fix
|
|
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
|
|
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. Also reports when a module is entirely unused.
|
|
|
|
🔧 Running with `--fix` will automatically remove all the reported errors.
|
|
It won't automatically remove unused modules though.
|
|
|
|
If the project is a package and the module that declared the element is exposed,
|
|
then nothing will be reported.
|
|
|
|
config =
|
|
[ NoUnused.Exports.rule
|
|
]
|
|
|
|
|
|
## Try it out
|
|
|
|
You can try this rule out by running the following command:
|
|
|
|
```bash
|
|
elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Exports
|
|
```
|
|
|
|
-}
|
|
rule : Rule
|
|
rule =
|
|
Rule.newProjectRuleSchema "NoUnused.Exports" initialProjectContext
|
|
|> Rule.withModuleVisitor moduleVisitor
|
|
|> Rule.withModuleContextUsingContextCreator
|
|
{ fromProjectToModule = fromProjectToModule
|
|
, fromModuleToProject = fromModuleToProject
|
|
, foldProjectContexts = foldProjectContexts
|
|
}
|
|
|> Rule.withElmJsonProjectVisitor (\elmJson context -> ( [], elmJsonVisitor elmJson context ))
|
|
|> Rule.withFinalProjectEvaluation finalEvaluationForProject
|
|
|> Rule.providesFixesForProjectRule
|
|
|> Rule.fromProjectRuleSchema
|
|
|
|
|
|
moduleVisitor : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext
|
|
moduleVisitor schema =
|
|
schema
|
|
|> Rule.withImportVisitor (\node context -> ( [], importVisitor node context ))
|
|
|> Rule.withDeclarationEnterVisitor (\node context -> ( [], declarationVisitor node context ))
|
|
|> Rule.withExpressionEnterVisitor (\node context -> ( [], expressionVisitor node context ))
|
|
|
|
|
|
|
|
-- CONTEXT
|
|
|
|
|
|
type alias ProjectContext =
|
|
{ projectType : ProjectType
|
|
, modules :
|
|
Dict
|
|
ModuleName
|
|
{ moduleKey : Rule.ModuleKey
|
|
, exposed : Dict String ExposedElement
|
|
, moduleNameLocation : Range
|
|
}
|
|
, usedModules : Set ModuleName
|
|
, used : Set ( ModuleName, String )
|
|
, constructors : Dict ( ModuleName, String ) String
|
|
}
|
|
|
|
|
|
type alias ExposedElement =
|
|
{ range : Range
|
|
, rangesToRemove : List Range
|
|
, elementType : ExposedElementType
|
|
}
|
|
|
|
|
|
type ProjectType
|
|
= IsApplication ElmApplicationType
|
|
| IsPackage (Set (List String))
|
|
|
|
|
|
type ElmApplicationType
|
|
= ElmApplication
|
|
| LamderaApplication
|
|
|
|
|
|
type ExposedElementType
|
|
= Function
|
|
| TypeOrTypeAlias
|
|
| ExposedType (List String)
|
|
|
|
|
|
type alias ModuleContext =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, exposed : Dict String ExposedElement
|
|
, used : Set ( ModuleName, String )
|
|
, elementsNotToReport : Set String
|
|
, importedModules : Set ModuleName
|
|
, containsMainFunction : Bool
|
|
, projectType : ProjectType
|
|
}
|
|
|
|
|
|
initialProjectContext : ProjectContext
|
|
initialProjectContext =
|
|
{ projectType = IsApplication ElmApplication
|
|
, modules = Dict.empty
|
|
, usedModules = Set.singleton [ "ReviewConfig" ]
|
|
, used = Set.empty
|
|
, constructors = Dict.empty
|
|
}
|
|
|
|
|
|
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
|
|
fromProjectToModule =
|
|
Rule.initContextCreator
|
|
(\lookupTable ast moduleDocumentation projectContext ->
|
|
let
|
|
exposed : Dict String ExposedElement
|
|
exposed =
|
|
case Module.exposingList (Node.value ast.moduleDefinition) of
|
|
Exposing.All _ ->
|
|
Dict.empty
|
|
|
|
Exposing.Explicit explicitlyExposed ->
|
|
collectExposedElements moduleDocumentation explicitlyExposed ast.declarations
|
|
in
|
|
{ lookupTable = lookupTable
|
|
, exposed = exposed
|
|
, used = Set.empty
|
|
, elementsNotToReport = Set.empty
|
|
, importedModules = Set.empty
|
|
, containsMainFunction = False
|
|
, projectType = projectContext.projectType
|
|
}
|
|
)
|
|
|> Rule.withModuleNameLookupTable
|
|
|> Rule.withFullAst
|
|
|> Rule.withModuleDocumentation
|
|
|
|
|
|
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
|
|
fromModuleToProject =
|
|
Rule.initContextCreator
|
|
(\moduleKey (Node moduleNameRange moduleName) moduleContext ->
|
|
{ projectType = IsApplication ElmApplication
|
|
, modules =
|
|
Dict.singleton
|
|
moduleName
|
|
{ moduleKey = moduleKey
|
|
, exposed = moduleContext.exposed
|
|
, moduleNameLocation = moduleNameRange
|
|
}
|
|
, used =
|
|
Set.foldl
|
|
(\element acc -> Set.insert ( moduleName, element ) acc)
|
|
moduleContext.used
|
|
moduleContext.elementsNotToReport
|
|
, usedModules =
|
|
if Set.member [ "Test" ] moduleContext.importedModules || moduleContext.containsMainFunction then
|
|
Set.insert moduleName moduleContext.importedModules
|
|
|
|
else
|
|
moduleContext.importedModules
|
|
, constructors =
|
|
Dict.foldl
|
|
(\name element acc ->
|
|
case element.elementType of
|
|
ExposedType constructorNames ->
|
|
List.foldl
|
|
(\constructorName listAcc -> Dict.insert ( moduleName, constructorName ) name listAcc)
|
|
acc
|
|
constructorNames
|
|
|
|
_ ->
|
|
acc
|
|
)
|
|
Dict.empty
|
|
moduleContext.exposed
|
|
}
|
|
)
|
|
|> Rule.withModuleKey
|
|
|> Rule.withModuleNameNode
|
|
|
|
|
|
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
|
|
foldProjectContexts newContext previousContext =
|
|
{ projectType = previousContext.projectType
|
|
, modules = Dict.union newContext.modules previousContext.modules
|
|
, usedModules = Set.union newContext.usedModules previousContext.usedModules
|
|
, used = Set.union newContext.used previousContext.used
|
|
, constructors = Dict.union newContext.constructors previousContext.constructors
|
|
}
|
|
|
|
|
|
registerAsUsed : ( ModuleName, String ) -> ModuleContext -> ModuleContext
|
|
registerAsUsed ( moduleName, name ) moduleContext =
|
|
{ moduleContext | used = Set.insert ( moduleName, name ) moduleContext.used }
|
|
|
|
|
|
|
|
-- ELM JSON VISITOR
|
|
|
|
|
|
elmJsonVisitor : Maybe { a | project : Elm.Project.Project } -> ProjectContext -> ProjectContext
|
|
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.foldr
|
|
(\moduleName acc ->
|
|
Set.insert (Elm.Module.toString moduleName |> String.split ".") acc
|
|
)
|
|
Set.empty
|
|
|> IsPackage
|
|
}
|
|
|
|
Just (Elm.Project.Application { depsDirect }) ->
|
|
let
|
|
elmApplicationType : ElmApplicationType
|
|
elmApplicationType =
|
|
if LamderaSupport.isLamderaApplication depsDirect then
|
|
LamderaApplication
|
|
|
|
else
|
|
ElmApplication
|
|
in
|
|
{ projectContext | projectType = IsApplication elmApplicationType }
|
|
|
|
Nothing ->
|
|
{ projectContext | projectType = IsApplication ElmApplication }
|
|
|
|
|
|
|
|
-- PROJECT EVALUATION
|
|
|
|
|
|
finalEvaluationForProject : ProjectContext -> List (Error { useErrorForModule : () })
|
|
finalEvaluationForProject projectContext =
|
|
let
|
|
used : Set ( ModuleName, String )
|
|
used =
|
|
Set.foldl
|
|
(\(( moduleName, _ ) as key) acc ->
|
|
case Dict.get key projectContext.constructors of
|
|
Just typeName ->
|
|
Set.insert ( moduleName, typeName ) acc
|
|
|
|
Nothing ->
|
|
acc
|
|
)
|
|
projectContext.used
|
|
projectContext.used
|
|
|
|
( usedModules, unusedModules ) =
|
|
projectContext.modules
|
|
|> removeExposedPackages projectContext
|
|
|> Dict.partition (\moduleName _ -> Set.member moduleName projectContext.usedModules)
|
|
in
|
|
List.concat
|
|
[ usedModules
|
|
|> Dict.toList
|
|
|> List.concatMap (errorsForModule projectContext used)
|
|
, unusedModules
|
|
|> Dict.toList
|
|
|> List.map unusedModuleError
|
|
]
|
|
|
|
|
|
unusedModuleError : ( ModuleName, { a | moduleKey : Rule.ModuleKey, moduleNameLocation : Range } ) -> Error scope
|
|
unusedModuleError ( moduleName, { moduleKey, moduleNameLocation } ) =
|
|
Rule.errorForModule moduleKey
|
|
{ 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
|
|
|
|
|
|
errorsForModule : ProjectContext -> Set ( ModuleName, String ) -> ( ModuleName, { a | moduleKey : Rule.ModuleKey, exposed : Dict String ExposedElement } ) -> List (Error scope)
|
|
errorsForModule projectContext used ( moduleName, { moduleKey, exposed } ) =
|
|
exposed
|
|
|> removeApplicationExceptions projectContext
|
|
|> removeReviewConfig moduleName
|
|
|> Dict.filter (\name _ -> not <| Set.member ( moduleName, name ) used)
|
|
|> Dict.toList
|
|
|> List.concatMap
|
|
(\( name, element ) ->
|
|
let
|
|
what : String
|
|
what =
|
|
case element.elementType of
|
|
Function ->
|
|
"Exposed function or value"
|
|
|
|
TypeOrTypeAlias ->
|
|
"Exposed type or type alias"
|
|
|
|
ExposedType _ ->
|
|
"Exposed type"
|
|
in
|
|
[ 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." ]
|
|
}
|
|
element.range
|
|
(List.map Fix.removeRange element.rangesToRemove)
|
|
]
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
removeApplicationExceptions : ProjectContext -> Dict String a -> Dict String a
|
|
removeApplicationExceptions projectContext dict =
|
|
case projectContext.projectType of
|
|
IsPackage _ ->
|
|
dict
|
|
|
|
IsApplication ElmApplication ->
|
|
Dict.remove "main" dict
|
|
|
|
IsApplication LamderaApplication ->
|
|
dict
|
|
|> Dict.remove "main"
|
|
|> Dict.remove "app"
|
|
|
|
|
|
removeReviewConfig : ModuleName -> Dict String a -> Dict String a
|
|
removeReviewConfig moduleName dict =
|
|
if moduleName == [ "ReviewConfig" ] then
|
|
Dict.remove "config" dict
|
|
|
|
else
|
|
dict
|
|
|
|
|
|
getRangesToRemove : List ( Int, String ) -> Bool -> String -> Int -> Maybe Range -> Range -> Range -> List Range
|
|
getRangesToRemove comments canRemoveExposed name index maybePreviousRange range nextRange =
|
|
if canRemoveExposed then
|
|
let
|
|
exposeRemoval : Range
|
|
exposeRemoval =
|
|
if index == 0 then
|
|
{ range | end = nextRange.start }
|
|
|
|
else
|
|
case maybePreviousRange of
|
|
Nothing ->
|
|
range
|
|
|
|
Just previousRange ->
|
|
{ range | start = previousRange.end }
|
|
in
|
|
List.filterMap identity
|
|
[ Just exposeRemoval
|
|
, findMap (findDocsRangeToRemove name) comments
|
|
]
|
|
|
|
else
|
|
[]
|
|
|
|
|
|
findDocsRangeToRemove : String -> ( Int, String ) -> Maybe Range
|
|
findDocsRangeToRemove name fullComment =
|
|
case findCommentInMiddle name fullComment of
|
|
Just range ->
|
|
Just range
|
|
|
|
Nothing ->
|
|
findCommentAtEnd name fullComment
|
|
|
|
|
|
findCommentInMiddle : String -> ( Int, String ) -> Maybe Range
|
|
findCommentInMiddle name ( row, comment ) =
|
|
String.indexes (" " ++ name ++ ", ") comment
|
|
|> List.head
|
|
|> Maybe.map
|
|
(\index ->
|
|
{ start = { row = row, column = index + 2 }
|
|
, end = { row = row, column = index + String.length name + 4 }
|
|
}
|
|
)
|
|
|
|
|
|
findCommentAtEnd : String -> ( Int, String ) -> Maybe Range
|
|
findCommentAtEnd name ( row, comment ) =
|
|
if comment == "@docs " ++ name then
|
|
Just
|
|
{ start = { row = row, column = 1 }
|
|
, end = { row = row + 1, column = 1 }
|
|
}
|
|
|
|
else
|
|
String.indexes (", " ++ name) comment
|
|
|> List.head
|
|
|> Maybe.map
|
|
(\index ->
|
|
{ start = { row = row, column = index + 1 }
|
|
, end = { row = row, column = index + String.length name + 3 }
|
|
}
|
|
)
|
|
|
|
|
|
findMap : (a -> Maybe b) -> List a -> Maybe b
|
|
findMap mapper list =
|
|
case list of
|
|
[] ->
|
|
Nothing
|
|
|
|
first :: rest ->
|
|
case mapper first of
|
|
Just value ->
|
|
Just value
|
|
|
|
Nothing ->
|
|
findMap mapper rest
|
|
|
|
|
|
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 } }
|
|
|
|
|
|
|
|
-- IMPORT VISITOR
|
|
|
|
|
|
importVisitor : Node Import -> ModuleContext -> ModuleContext
|
|
importVisitor (Node _ import_) moduleContext =
|
|
let
|
|
moduleName : ModuleName
|
|
moduleName =
|
|
Node.value import_.moduleName
|
|
in
|
|
{ moduleContext
|
|
| used = collectUsedFromImport moduleName import_.exposingList moduleContext.used
|
|
, importedModules = Set.insert moduleName moduleContext.importedModules
|
|
}
|
|
|
|
|
|
collectUsedFromImport : ModuleName -> Maybe (Node Exposing) -> Set ( ModuleName, String ) -> Set ( ModuleName, String )
|
|
collectUsedFromImport moduleName exposingList used =
|
|
case Maybe.map Node.value exposingList of
|
|
Just (Exposing.Explicit list) ->
|
|
List.foldl
|
|
(\(Node _ element) acc ->
|
|
case element of
|
|
Exposing.FunctionExpose name ->
|
|
Set.insert ( moduleName, name ) acc
|
|
|
|
Exposing.TypeOrAliasExpose name ->
|
|
Set.insert ( moduleName, name ) acc
|
|
|
|
Exposing.TypeExpose { name } ->
|
|
Set.insert ( moduleName, name ) acc
|
|
|
|
Exposing.InfixExpose _ ->
|
|
acc
|
|
)
|
|
used
|
|
list
|
|
|
|
Just (Exposing.All _) ->
|
|
used
|
|
|
|
Nothing ->
|
|
used
|
|
|
|
|
|
collectDocsReferences : Maybe (Node String) -> List ( Int, String )
|
|
collectDocsReferences maybeModuleDocumentation =
|
|
case maybeModuleDocumentation of
|
|
Just (Node range moduleDocumentation) ->
|
|
let
|
|
lines : List String
|
|
lines =
|
|
moduleDocumentation
|
|
|> String.lines
|
|
|> List.drop 1
|
|
in
|
|
List.Extra.indexedFilterMap
|
|
(\lineNumber line ->
|
|
if String.startsWith "@docs " line then
|
|
Just ( lineNumber, line )
|
|
|
|
else
|
|
Nothing
|
|
)
|
|
(range.start.row + 1)
|
|
lines
|
|
[]
|
|
|
|
Nothing ->
|
|
[]
|
|
|
|
|
|
collectExposedElements : Maybe (Node String) -> List (Node Exposing.TopLevelExpose) -> List (Node Declaration) -> Dict String ExposedElement
|
|
collectExposedElements moduleDocumentation exposingNodes declarations =
|
|
let
|
|
docsReferences : List ( Int, String )
|
|
docsReferences =
|
|
collectDocsReferences moduleDocumentation
|
|
|
|
declaredNames : Set String
|
|
declaredNames =
|
|
List.foldl
|
|
(\(Node _ declaration) acc ->
|
|
case declarationName declaration of
|
|
Just name ->
|
|
Set.insert name acc
|
|
|
|
Nothing ->
|
|
acc
|
|
)
|
|
Set.empty
|
|
declarations
|
|
in
|
|
collectExposedElementsHelp docsReferences declarations declaredNames (List.length exposingNodes /= 1) Nothing exposingNodes 0 Dict.empty
|
|
|
|
|
|
collectExposedElementsHelp : List ( Int, String ) -> List (Node Declaration) -> Set String -> Bool -> Maybe Range -> List (Node TopLevelExpose) -> Int -> Dict String ExposedElement -> Dict String ExposedElement
|
|
collectExposedElementsHelp docsReferences declarations declaredNames canRemoveExposed maybePreviousRange exposingNodes index acc =
|
|
case exposingNodes of
|
|
[] ->
|
|
acc
|
|
|
|
(Node range value) :: rest ->
|
|
let
|
|
nextRange : Range
|
|
nextRange =
|
|
case List.head rest of
|
|
Just nextNode ->
|
|
Node.range nextNode
|
|
|
|
Nothing ->
|
|
Range.emptyRange
|
|
|
|
newAcc : Dict String ExposedElement
|
|
newAcc =
|
|
case value of
|
|
Exposing.FunctionExpose name ->
|
|
if Set.member name declaredNames then
|
|
Dict.insert name
|
|
{ range = untilEndOfVariable name range
|
|
, rangesToRemove = getRangesToRemove docsReferences canRemoveExposed name index maybePreviousRange range nextRange
|
|
, elementType = Function
|
|
}
|
|
acc
|
|
|
|
else
|
|
acc
|
|
|
|
Exposing.TypeOrAliasExpose name ->
|
|
if Set.member name declaredNames then
|
|
Dict.insert name
|
|
{ range = untilEndOfVariable name range
|
|
, rangesToRemove = getRangesToRemove docsReferences canRemoveExposed name index maybePreviousRange range nextRange
|
|
, elementType = TypeOrTypeAlias
|
|
}
|
|
acc
|
|
|
|
else
|
|
acc
|
|
|
|
Exposing.TypeExpose { name } ->
|
|
if Set.member name declaredNames then
|
|
Dict.insert name
|
|
{ range = untilEndOfVariable name range
|
|
, rangesToRemove = []
|
|
, elementType = ExposedType (findConstructorsForExposedCustomType name declarations)
|
|
}
|
|
acc
|
|
|
|
else
|
|
acc
|
|
|
|
Exposing.InfixExpose _ ->
|
|
acc
|
|
in
|
|
collectExposedElementsHelp
|
|
docsReferences
|
|
declarations
|
|
declaredNames
|
|
canRemoveExposed
|
|
(Just range)
|
|
rest
|
|
(index + 1)
|
|
newAcc
|
|
|
|
|
|
declarationVisitor : Node Declaration -> ModuleContext -> ModuleContext
|
|
declarationVisitor node moduleContext =
|
|
let
|
|
( allUsedTypes, comesFromCustomTypeWithHiddenConstructors ) =
|
|
typesUsedInDeclaration moduleContext node
|
|
|
|
elementsNotToReport : Set String
|
|
elementsNotToReport =
|
|
(if comesFromCustomTypeWithHiddenConstructors then
|
|
moduleContext.elementsNotToReport
|
|
|
|
else
|
|
List.foldl (\( _, name ) acc -> Set.insert name acc) moduleContext.elementsNotToReport allUsedTypes
|
|
)
|
|
|> maybeSetInsert (testFunctionName moduleContext node)
|
|
|
|
used : Set ( ModuleName, String )
|
|
used =
|
|
List.foldl Set.insert moduleContext.used allUsedTypes
|
|
in
|
|
{ moduleContext
|
|
| elementsNotToReport = elementsNotToReport
|
|
, used = used
|
|
, containsMainFunction =
|
|
moduleContext.containsMainFunction
|
|
|| doesModuleContainMainFunction moduleContext.projectType node
|
|
}
|
|
|
|
|
|
doesModuleContainMainFunction : ProjectType -> Node Declaration -> Bool
|
|
doesModuleContainMainFunction projectType declaration =
|
|
case projectType of
|
|
IsPackage _ ->
|
|
False
|
|
|
|
IsApplication elmApplicationType ->
|
|
case Node.value declaration of
|
|
Declaration.FunctionDeclaration function ->
|
|
isMainFunction elmApplicationType (function.declaration |> Node.value |> .name |> Node.value)
|
|
|
|
_ ->
|
|
False
|
|
|
|
|
|
isMainFunction : ElmApplicationType -> String -> Bool
|
|
isMainFunction elmApplicationType name =
|
|
case elmApplicationType of
|
|
ElmApplication ->
|
|
name == "main"
|
|
|
|
LamderaApplication ->
|
|
name == "main" || name == "app"
|
|
|
|
|
|
maybeSetInsert : Maybe comparable -> Set comparable -> Set comparable
|
|
maybeSetInsert maybeValue set =
|
|
case maybeValue of
|
|
Just value ->
|
|
Set.insert value set
|
|
|
|
Nothing ->
|
|
set
|
|
|
|
|
|
findConstructorsForExposedCustomType : String -> List (Node Declaration) -> List String
|
|
findConstructorsForExposedCustomType typeName declarations =
|
|
findMap
|
|
(\node ->
|
|
case Node.value node of
|
|
Declaration.CustomTypeDeclaration type_ ->
|
|
if Node.value type_.name /= typeName then
|
|
Nothing
|
|
|
|
else
|
|
List.map (\c -> c |> Node.value |> .name |> Node.value) type_.constructors
|
|
|> Just
|
|
|
|
_ ->
|
|
Nothing
|
|
)
|
|
declarations
|
|
|> Maybe.withDefault []
|
|
|
|
|
|
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 : ModuleContext -> Node Declaration -> Maybe String
|
|
testFunctionName moduleContext node =
|
|
case Node.value node of
|
|
Declaration.FunctionDeclaration function ->
|
|
case Maybe.map (\(Node _ value) -> Node.value value.typeAnnotation) function.signature of
|
|
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 =
|
|
case Node.value declaration of
|
|
Declaration.FunctionDeclaration function ->
|
|
( case function.signature of
|
|
Just signature ->
|
|
[]
|
|
|> collectTypesFromTypeAnnotation moduleContext [ (Node.value signature).typeAnnotation ]
|
|
|> findUsedConstructors moduleContext.lookupTable (Node.value function.declaration).arguments
|
|
|
|
Nothing ->
|
|
findUsedConstructors moduleContext.lookupTable (Node.value function.declaration).arguments []
|
|
, False
|
|
)
|
|
|
|
Declaration.CustomTypeDeclaration type_ ->
|
|
let
|
|
typesUsedInArguments : List ( ModuleName, String )
|
|
typesUsedInArguments =
|
|
List.foldl
|
|
(\constructor acc -> collectTypesFromTypeAnnotation moduleContext (Node.value constructor).arguments acc)
|
|
[]
|
|
type_.constructors
|
|
in
|
|
( typesUsedInArguments
|
|
, case Dict.get (Node.value type_.name) moduleContext.exposed |> Maybe.map .elementType of
|
|
Just (ExposedType _) ->
|
|
False
|
|
|
|
_ ->
|
|
True
|
|
)
|
|
|
|
Declaration.AliasDeclaration alias_ ->
|
|
( collectTypesFromTypeAnnotation moduleContext [ alias_.typeAnnotation ] [], False )
|
|
|
|
Declaration.PortDeclaration signature ->
|
|
( collectTypesFromTypeAnnotation moduleContext [ signature.typeAnnotation ] [], False )
|
|
|
|
Declaration.InfixDeclaration _ ->
|
|
( [], False )
|
|
|
|
Declaration.Destructuring _ _ ->
|
|
( [], False )
|
|
|
|
|
|
collectTypesFromTypeAnnotation : ModuleContext -> List (Node TypeAnnotation) -> List ( ModuleName, String ) -> List ( ModuleName, String )
|
|
collectTypesFromTypeAnnotation moduleContext nodes acc =
|
|
case nodes of
|
|
[] ->
|
|
acc
|
|
|
|
node :: restOfNodes ->
|
|
case Node.value node of
|
|
TypeAnnotation.FunctionTypeAnnotation left right ->
|
|
collectTypesFromTypeAnnotation moduleContext (left :: right :: restOfNodes) acc
|
|
|
|
TypeAnnotation.Typed (Node range ( _, name )) params ->
|
|
case ModuleNameLookupTable.moduleNameAt moduleContext.lookupTable range of
|
|
Just moduleName ->
|
|
collectTypesFromTypeAnnotation moduleContext (params ++ restOfNodes) (( moduleName, name ) :: acc)
|
|
|
|
Nothing ->
|
|
collectTypesFromTypeAnnotation moduleContext (params ++ restOfNodes) acc
|
|
|
|
TypeAnnotation.Record fields ->
|
|
let
|
|
subNodes : List (Node TypeAnnotation)
|
|
subNodes =
|
|
List.map (\(Node _ ( _, value )) -> value) fields
|
|
in
|
|
collectTypesFromTypeAnnotation moduleContext (subNodes ++ restOfNodes) acc
|
|
|
|
TypeAnnotation.GenericRecord _ (Node _ fields) ->
|
|
let
|
|
subNodes : List (Node TypeAnnotation)
|
|
subNodes =
|
|
List.map (\(Node _ ( _, value )) -> value) fields
|
|
in
|
|
collectTypesFromTypeAnnotation moduleContext (subNodes ++ restOfNodes) acc
|
|
|
|
TypeAnnotation.Tupled list ->
|
|
collectTypesFromTypeAnnotation moduleContext (list ++ restOfNodes) acc
|
|
|
|
_ ->
|
|
collectTypesFromTypeAnnotation moduleContext restOfNodes acc
|
|
|
|
|
|
|
|
-- EXPRESSION VISITOR
|
|
|
|
|
|
expressionVisitor : Node Expression -> ModuleContext -> ModuleContext
|
|
expressionVisitor node moduleContext =
|
|
case Node.value node of
|
|
Expression.FunctionOrValue _ name ->
|
|
case ModuleNameLookupTable.moduleNameFor moduleContext.lookupTable node of
|
|
Just moduleName ->
|
|
registerAsUsed
|
|
( moduleName, name )
|
|
moduleContext
|
|
|
|
Nothing ->
|
|
moduleContext
|
|
|
|
Expression.RecordUpdateExpression (Node range name) _ ->
|
|
case ModuleNameLookupTable.moduleNameAt moduleContext.lookupTable range of
|
|
Just moduleName ->
|
|
registerAsUsed
|
|
( moduleName, name )
|
|
moduleContext
|
|
|
|
Nothing ->
|
|
moduleContext
|
|
|
|
Expression.LetExpression { declarations } ->
|
|
let
|
|
used : List ( ModuleName, String )
|
|
used =
|
|
List.foldl
|
|
(\declaration acc ->
|
|
case Node.value declaration of
|
|
Expression.LetFunction function ->
|
|
case function.signature of
|
|
Just signature ->
|
|
acc
|
|
|> collectTypesFromTypeAnnotation moduleContext [ (Node.value signature).typeAnnotation ]
|
|
|> findUsedConstructors moduleContext.lookupTable (Node.value function.declaration).arguments
|
|
|
|
Nothing ->
|
|
findUsedConstructors moduleContext.lookupTable (Node.value function.declaration).arguments acc
|
|
|
|
Expression.LetDestructuring pattern _ ->
|
|
findUsedConstructors moduleContext.lookupTable [ pattern ] acc
|
|
)
|
|
[]
|
|
declarations
|
|
in
|
|
{ moduleContext | used = List.foldl Set.insert moduleContext.used used }
|
|
|
|
Expression.CaseExpression { cases } ->
|
|
let
|
|
usedConstructors : List ( ModuleName, String )
|
|
usedConstructors =
|
|
findUsedConstructors
|
|
moduleContext.lookupTable
|
|
(List.map Tuple.first cases)
|
|
[]
|
|
in
|
|
{ moduleContext | used = List.foldl Set.insert moduleContext.used usedConstructors }
|
|
|
|
_ ->
|
|
moduleContext
|
|
|
|
|
|
findUsedConstructors : ModuleNameLookupTable -> List (Node Pattern) -> List ( ModuleName, String ) -> List ( ModuleName, String )
|
|
findUsedConstructors lookupTable patterns acc =
|
|
case patterns of
|
|
[] ->
|
|
acc
|
|
|
|
pattern :: restOfPatterns ->
|
|
case Node.value pattern of
|
|
Pattern.NamedPattern qualifiedNameRef newPatterns ->
|
|
let
|
|
newAcc : List ( ModuleName, String )
|
|
newAcc =
|
|
case ModuleNameLookupTable.moduleNameFor lookupTable pattern of
|
|
Just moduleName ->
|
|
( moduleName, qualifiedNameRef.name ) :: acc
|
|
|
|
Nothing ->
|
|
acc
|
|
in
|
|
findUsedConstructors lookupTable (newPatterns ++ restOfPatterns) newAcc
|
|
|
|
Pattern.TuplePattern newPatterns ->
|
|
findUsedConstructors lookupTable (newPatterns ++ restOfPatterns) acc
|
|
|
|
Pattern.UnConsPattern left right ->
|
|
findUsedConstructors lookupTable (left :: right :: restOfPatterns) acc
|
|
|
|
Pattern.ListPattern newPatterns ->
|
|
findUsedConstructors lookupTable (newPatterns ++ restOfPatterns) acc
|
|
|
|
Pattern.AsPattern node _ ->
|
|
findUsedConstructors lookupTable (node :: restOfPatterns) acc
|
|
|
|
Pattern.ParenthesizedPattern node ->
|
|
findUsedConstructors lookupTable (node :: restOfPatterns) acc
|
|
|
|
_ ->
|
|
findUsedConstructors lookupTable restOfPatterns acc
|