elm-review/tests/NoUnused/Modules.elm

228 lines
6.5 KiB
Elm
Raw Normal View History

module NoUnused.Modules exposing (rule)
{-| Forbid the use of modules that are never used in your project.
# Rule
@docs rule
-}
import Dict exposing (Dict)
import Elm.Module
import Elm.Project exposing (Project)
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Import exposing (Import)
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)
import Set exposing (Set)
{-| Forbid the use of modules that are never used in your project.
A module is considered used if
- it contains a `main` function (be it exposed or not)
- it imports the `Test` module
- it is imported in any other modules, even if it is not used.
- the project is a package and the module is part of the `elm.json`'s `exposed-modules`
- it is named `ReviewConfig`
```elm
config =
[ NoUnused.Modules.rule
]
```
2020-08-09 19:55:15 +03:00
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-unused/example --rules NoUnused.Modules
```
-}
rule : Rule
rule =
Rule.newProjectRuleSchema "NoUnused.Modules" initialProjectContext
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContext
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.withElmJsonProjectVisitor elmJsonVisitor
|> Rule.withFinalProjectEvaluation finalEvaluationForProject
2020-01-19 22:37:19 +03:00
|> Rule.fromProjectRuleSchema
moduleVisitor : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema =
schema
|> Rule.withImportVisitor importVisitor
|> Rule.withDeclarationListVisitor declarationListVisitor
-- CONTEXT
2019-10-21 11:26:11 +03:00
2020-01-19 22:37:19 +03:00
type alias ProjectContext =
{ modules :
Dict ModuleName
2020-02-16 23:54:05 +03:00
{ moduleKey : Rule.ModuleKey
, moduleNameLocation : Range
}
, usedModules : Set ModuleName
, isPackage : Bool
}
type alias ModuleContext =
2020-01-01 22:18:04 +03:00
{ importedModules : Set ModuleName
, containsMainFunction : Bool
, isPackage : Bool
}
initialProjectContext : ProjectContext
initialProjectContext =
{ modules = Dict.empty
, usedModules = Set.singleton [ "ReviewConfig" ]
, isPackage = False
}
2020-02-16 23:54:05 +03:00
fromProjectToModule : Rule.ModuleKey -> Node ModuleName -> ProjectContext -> ModuleContext
fromProjectToModule _ _ projectContext =
2020-01-01 22:18:04 +03:00
{ importedModules = Set.empty
, containsMainFunction = False
2020-01-19 22:37:19 +03:00
, isPackage = projectContext.isPackage
}
2020-02-16 23:54:05 +03:00
fromModuleToProject : Rule.ModuleKey -> Node ModuleName -> ModuleContext -> ProjectContext
fromModuleToProject moduleKey moduleName moduleContext =
2020-01-01 22:18:04 +03:00
{ modules =
Dict.singleton
(Node.value moduleName)
2020-02-16 23:54:05 +03:00
{ moduleKey = moduleKey, moduleNameLocation = Node.range moduleName }
, usedModules =
if Set.member [ "Test" ] moduleContext.importedModules || moduleContext.containsMainFunction then
Set.insert (Node.value moduleName) moduleContext.importedModules
else
moduleContext.importedModules
, isPackage = moduleContext.isPackage
}
2020-01-19 22:37:19 +03:00
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts newContext previousContext =
{ modules = Dict.union previousContext.modules newContext.modules
, usedModules = Set.union previousContext.usedModules newContext.usedModules
, isPackage = previousContext.isPackage
}
2020-01-26 15:25:09 +03:00
-- PROJECT VISITORS
elmJsonVisitor : Maybe { a | project : Project } -> ProjectContext -> ( List nothing, ProjectContext )
2020-01-26 15:25:09 +03:00
elmJsonVisitor maybeProject projectContext =
let
( exposedModules, isPackage ) =
case maybeProject |> Maybe.map .project of
Just (Elm.Project.Package { exposed }) ->
case exposed of
Elm.Project.ExposedList names ->
( names, True )
Elm.Project.ExposedDict fakeDict ->
( List.concatMap Tuple.second fakeDict, True )
_ ->
( [], False )
in
( []
, { projectContext
2019-10-22 00:33:41 +03:00
| usedModules =
exposedModules
|> List.map (Elm.Module.toString >> String.split ".")
|> Set.fromList
2020-01-26 15:25:09 +03:00
|> Set.union projectContext.usedModules
, isPackage = isPackage
}
)
finalEvaluationForProject : ProjectContext -> List (Error scope)
finalEvaluationForProject { modules, usedModules } =
modules
|> Dict.filter (\moduleName _ -> not <| Set.member moduleName usedModules)
|> Dict.toList
|> List.map error
error : ( ModuleName, { moduleKey : Rule.ModuleKey, moduleNameLocation : Range } ) -> Error scope
2020-02-16 23:54:05 +03:00
error ( 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
-- IMPORT VISITOR
importVisitor : Node Import -> ModuleContext -> ( List nothing, ModuleContext )
importVisitor node context =
( []
2020-01-01 22:18:04 +03:00
, { context | importedModules = Set.insert (moduleNameForImport node) context.importedModules }
)
2020-01-01 22:18:04 +03:00
moduleNameForImport : Node Import -> ModuleName
moduleNameForImport node =
node
|> Node.value
|> .moduleName
|> Node.value
-- DECLARATION LIST VISITOR
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ( List nothing, ModuleContext )
declarationListVisitor list context =
if context.isPackage then
( [], context )
else
let
containsMainFunction : Bool
containsMainFunction =
List.any
2020-03-24 20:59:24 +03:00
(\declaration ->
case Node.value declaration of
Declaration.FunctionDeclaration function ->
(function.declaration |> Node.value |> .name |> Node.value) == "main"
_ ->
False
)
list
in
( []
, { context | containsMainFunction = containsMainFunction }
)