mirror of
https://github.com/jfmengels/elm-review.git
synced 2024-12-23 17:53:35 +03:00
225 lines
7.1 KiB
Elm
225 lines
7.1 KiB
Elm
module NoMissingSubscriptionsCall exposing (rule)
|
|
|
|
{-|
|
|
|
|
@docs rule
|
|
|
|
-}
|
|
|
|
import Dict exposing (Dict)
|
|
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
|
|
import Elm.Syntax.Expression as Expression exposing (Expression)
|
|
import Elm.Syntax.ModuleName exposing (ModuleName)
|
|
import Elm.Syntax.Node as Node exposing (Node)
|
|
import Elm.Syntax.Range exposing (Range)
|
|
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
|
|
import Review.Rule as Rule exposing (Error, Rule)
|
|
import Set exposing (Set)
|
|
|
|
|
|
{-| Reports likely missing calls to a `subscriptions` function.
|
|
|
|
config =
|
|
[ NoMissingSubscriptionsCall.rule
|
|
]
|
|
|
|
|
|
## Fail
|
|
|
|
import SomeModule
|
|
|
|
update msg model =
|
|
case msg of
|
|
UsedMsg subMsg ->
|
|
SomeModule.update subMsg model.used
|
|
|
|
subscriptions model =
|
|
-- We used `SomeModule.update` but not `SomeModule.subscriptions`
|
|
Sub.none
|
|
|
|
This won't fail if `SomeModule` does not define a `subscriptions` function.
|
|
|
|
|
|
## Success
|
|
|
|
import SomeModule
|
|
|
|
update msg model =
|
|
case msg of
|
|
UsedMsg subMsg ->
|
|
SomeModule.update subMsg model.used
|
|
|
|
subscriptions model =
|
|
SomeModule.subscriptions
|
|
|
|
|
|
## Try it out
|
|
|
|
You can try this rule out by running the following command:
|
|
|
|
```bash
|
|
elm-review --template jfmengels/elm-review-the-elm-architecture/example --rules NoMissingSubscriptionsCall
|
|
```
|
|
|
|
-}
|
|
rule : Rule
|
|
rule =
|
|
Rule.newProjectRuleSchema "NoMissingSubscriptionsCall" initialProjectContext
|
|
|> Rule.withModuleVisitor moduleVisitor
|
|
|> Rule.withModuleContextUsingContextCreator
|
|
{ fromProjectToModule = fromProjectToModule
|
|
, fromModuleToProject = fromModuleToProject
|
|
, foldProjectContexts = foldProjectContexts
|
|
}
|
|
|> Rule.withContextFromImportedModules
|
|
|> Rule.fromProjectRuleSchema
|
|
|
|
|
|
moduleVisitor : Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
|
|
moduleVisitor schema =
|
|
schema
|
|
|> Rule.withDeclarationEnterVisitor declarationVisitor
|
|
|> Rule.withExpressionEnterVisitor expressionVisitor
|
|
|> Rule.withFinalModuleEvaluation finalEvaluation
|
|
|
|
|
|
type alias ProjectContext =
|
|
{ modulesThatExposeSubscriptionsAndUpdate : Set ModuleName
|
|
}
|
|
|
|
|
|
type alias ModuleContext =
|
|
{ lookupTable : ModuleNameLookupTable
|
|
, modulesThatExposeSubscriptionsAndUpdate : Set ModuleName
|
|
, definesUpdate : Bool
|
|
, definesSubscriptions : Bool
|
|
, usesUpdateOfModule : Dict ModuleName Range
|
|
, usesSubscriptionsOfModule : Set ModuleName
|
|
}
|
|
|
|
|
|
initialProjectContext : ProjectContext
|
|
initialProjectContext =
|
|
{ modulesThatExposeSubscriptionsAndUpdate = Set.empty
|
|
}
|
|
|
|
|
|
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
|
|
fromProjectToModule =
|
|
Rule.initContextCreator
|
|
(\lookupTable projectContext ->
|
|
{ lookupTable = lookupTable
|
|
, modulesThatExposeSubscriptionsAndUpdate = projectContext.modulesThatExposeSubscriptionsAndUpdate
|
|
, definesUpdate = False
|
|
, definesSubscriptions = False
|
|
, usesUpdateOfModule = Dict.empty
|
|
, usesSubscriptionsOfModule = Set.empty
|
|
}
|
|
)
|
|
|> Rule.withModuleNameLookupTable
|
|
|
|
|
|
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
|
|
fromModuleToProject =
|
|
Rule.initContextCreator
|
|
(\moduleName moduleContext ->
|
|
{ modulesThatExposeSubscriptionsAndUpdate =
|
|
if moduleContext.definesSubscriptions && moduleContext.definesUpdate then
|
|
Set.singleton moduleName
|
|
|
|
else
|
|
Set.empty
|
|
}
|
|
)
|
|
|> Rule.withModuleName
|
|
|
|
|
|
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
|
|
foldProjectContexts newContext previousContext =
|
|
{ modulesThatExposeSubscriptionsAndUpdate =
|
|
Set.union
|
|
newContext.modulesThatExposeSubscriptionsAndUpdate
|
|
previousContext.modulesThatExposeSubscriptionsAndUpdate
|
|
}
|
|
|
|
|
|
declarationVisitor : Node Declaration -> ModuleContext -> ( List (Error nothing), ModuleContext )
|
|
declarationVisitor node moduleContext =
|
|
case Node.value node of
|
|
Declaration.FunctionDeclaration function ->
|
|
case
|
|
function.declaration
|
|
|> Node.value
|
|
|> .name
|
|
|> Node.value
|
|
of
|
|
"update" ->
|
|
( [], { moduleContext | definesUpdate = True } )
|
|
|
|
"subscriptions" ->
|
|
( [], { moduleContext | definesSubscriptions = True } )
|
|
|
|
_ ->
|
|
( [], moduleContext )
|
|
|
|
_ ->
|
|
( [], moduleContext )
|
|
|
|
|
|
expressionVisitor : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
|
|
expressionVisitor node moduleContext =
|
|
case Node.value node of
|
|
Expression.FunctionOrValue _ "update" ->
|
|
( [], registerUpdateFunction node moduleContext )
|
|
|
|
Expression.FunctionOrValue _ "subscriptions" ->
|
|
( [], registerSubscriptionsFunction node moduleContext )
|
|
|
|
_ ->
|
|
( [], moduleContext )
|
|
|
|
|
|
registerUpdateFunction : Node a -> ModuleContext -> ModuleContext
|
|
registerUpdateFunction node moduleContext =
|
|
case ModuleNameLookupTable.moduleNameFor moduleContext.lookupTable node of
|
|
Just moduleName ->
|
|
if Set.member moduleName moduleContext.modulesThatExposeSubscriptionsAndUpdate then
|
|
{ moduleContext | usesUpdateOfModule = Dict.insert moduleName (Node.range node) moduleContext.usesUpdateOfModule }
|
|
|
|
else
|
|
moduleContext
|
|
|
|
Nothing ->
|
|
moduleContext
|
|
|
|
|
|
registerSubscriptionsFunction : Node a -> ModuleContext -> ModuleContext
|
|
registerSubscriptionsFunction node moduleContext =
|
|
case ModuleNameLookupTable.moduleNameFor moduleContext.lookupTable node of
|
|
Just moduleName ->
|
|
if Set.member moduleName moduleContext.modulesThatExposeSubscriptionsAndUpdate then
|
|
{ moduleContext | usesSubscriptionsOfModule = Set.insert moduleName moduleContext.usesSubscriptionsOfModule }
|
|
|
|
else
|
|
moduleContext
|
|
|
|
Nothing ->
|
|
moduleContext
|
|
|
|
|
|
finalEvaluation : ModuleContext -> List (Error {})
|
|
finalEvaluation moduleContext =
|
|
moduleContext.usesUpdateOfModule
|
|
|> Dict.filter (\moduleName _ -> not <| Set.member moduleName moduleContext.usesSubscriptionsOfModule)
|
|
|> Dict.toList
|
|
|> List.map
|
|
(\( moduleName, range ) ->
|
|
Rule.error
|
|
{ message = "Missing subscriptions call to " ++ String.join "." moduleName ++ ".subscriptions"
|
|
, details =
|
|
[ "The " ++ String.join "." moduleName ++ " module defines a `subscriptions` function, which you are not using even though you are using its `update` function. This makes me think that you are not subscribing to all the things you should."
|
|
]
|
|
}
|
|
range
|
|
)
|