elm-review/tests/NoMissingSubscriptionsCall.elm

225 lines
7.1 KiB
Elm
Raw Permalink Normal View History

2020-06-03 19:23:19 +03:00
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)
2020-06-03 19:23:19 +03:00
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
2020-08-09 19:55:15 +03:00
## Try it out
You can try this rule out by running the following command:
```bash
2020-09-22 20:40:30 +03:00
elm-review --template jfmengels/elm-review-the-elm-architecture/example --rules NoMissingSubscriptionsCall
2020-08-09 19:55:15 +03:00
```
2020-06-03 19:23:19 +03:00
-}
rule : Rule
rule =
Rule.newProjectRuleSchema "NoMissingSubscriptionsCall" initialProjectContext
|> Rule.withModuleVisitor moduleVisitor
2020-06-28 19:19:58 +03:00
|> Rule.withModuleContextUsingContextCreator
2020-09-22 20:40:30 +03:00
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
2020-06-03 19:23:19 +03:00
, 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
2020-06-03 19:23:19 +03:00
|> Rule.withFinalModuleEvaluation finalEvaluation
type alias ProjectContext =
{ modulesThatExposeSubscriptionsAndUpdate : Set ModuleName
2020-06-03 19:23:19 +03:00
}
type alias ModuleContext =
2020-09-22 20:40:30 +03:00
{ lookupTable : ModuleNameLookupTable
, modulesThatExposeSubscriptionsAndUpdate : Set ModuleName
2020-06-03 19:23:19 +03:00
, definesUpdate : Bool
, definesSubscriptions : Bool
, usesUpdateOfModule : Dict ModuleName Range
, usesSubscriptionsOfModule : Set ModuleName
}
initialProjectContext : ProjectContext
initialProjectContext =
{ modulesThatExposeSubscriptionsAndUpdate = Set.empty
2020-06-03 19:23:19 +03:00
}
2020-09-22 20:40:30 +03:00
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
2020-06-03 19:23:19 +03:00
2020-09-22 20:40:30 +03:00
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
fromModuleToProject =
Rule.initContextCreator
(\moduleName moduleContext ->
2020-09-22 20:40:30 +03:00
{ modulesThatExposeSubscriptionsAndUpdate =
if moduleContext.definesSubscriptions && moduleContext.definesUpdate then
Set.singleton moduleName
2020-06-03 19:23:19 +03:00
2020-09-22 20:40:30 +03:00
else
Set.empty
}
)
|> Rule.withModuleName
2020-06-03 19:23:19 +03:00
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts newContext previousContext =
{ modulesThatExposeSubscriptionsAndUpdate =
2020-06-03 19:23:19 +03:00
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" ->
2022-04-17 09:59:11 +03:00
( [], registerUpdateFunction node moduleContext )
2022-04-17 09:59:11 +03:00
Expression.FunctionOrValue _ "subscriptions" ->
( [], registerSubscriptionsFunction node moduleContext )
2022-04-17 09:59:11 +03:00
_ ->
( [], moduleContext )
2022-04-17 09:59:11 +03:00
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 }
2022-04-17 09:59:11 +03:00
else
moduleContext
2020-06-03 19:23:19 +03:00
2022-04-17 09:59:11 +03:00
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
2020-06-03 19:23:19 +03:00
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
)