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.Rule as Rule exposing (Error, Rule)
|
|
|
|
import Scope
|
|
|
|
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
|
|
|
|
|
|
|
|
-}
|
|
|
|
rule : Rule
|
|
|
|
rule =
|
|
|
|
Rule.newProjectRuleSchema "NoMissingSubscriptionsCall" initialProjectContext
|
|
|
|
|> Scope.addProjectVisitors
|
|
|
|
|> Rule.withModuleVisitor moduleVisitor
|
|
|
|
|> Rule.withModuleContext
|
|
|
|
{ fromProjectToModule = fromProjectToModule
|
|
|
|
, fromModuleToProject = fromModuleToProject
|
|
|
|
, foldProjectContexts = foldProjectContexts
|
|
|
|
}
|
|
|
|
|> Rule.withContextFromImportedModules
|
|
|
|
|> Rule.fromProjectRuleSchema
|
|
|
|
|
|
|
|
|
|
|
|
moduleVisitor : Rule.ModuleRuleSchema schemaState ModuleContext -> Rule.ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } ModuleContext
|
|
|
|
moduleVisitor schema =
|
|
|
|
schema
|
2020-06-20 21:09:03 +03:00
|
|
|
|> Rule.withDeclarationEnterVisitor declarationVisitor
|
|
|
|
|> Rule.withExpressionEnterVisitor expressionVisitor
|
2020-06-03 19:23:19 +03:00
|
|
|
|> Rule.withFinalModuleEvaluation finalEvaluation
|
|
|
|
|
|
|
|
|
|
|
|
type alias ProjectContext =
|
|
|
|
{ scope : Scope.ProjectContext
|
|
|
|
, modulesThatExposeSubscriptionsAndUpdate : Set ModuleName
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias ModuleContext =
|
|
|
|
{ scope : Scope.ModuleContext
|
|
|
|
, modulesThatExposeSubscriptionsAndUpdate : Set ModuleName
|
|
|
|
|
|
|
|
--, usesUpdate : Bool
|
|
|
|
--, usesSubscription : Bool
|
|
|
|
, definesUpdate : Bool
|
|
|
|
, definesSubscriptions : Bool
|
|
|
|
, usesUpdateOfModule : Dict ModuleName Range
|
|
|
|
, usesSubscriptionsOfModule : Set ModuleName
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
initialProjectContext : ProjectContext
|
|
|
|
initialProjectContext =
|
|
|
|
{ scope = Scope.initialProjectContext
|
|
|
|
, modulesThatExposeSubscriptionsAndUpdate = Set.empty
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fromProjectToModule : Rule.ModuleKey -> Node ModuleName -> ProjectContext -> ModuleContext
|
|
|
|
fromProjectToModule _ _ projectContext =
|
|
|
|
{ scope = Scope.fromProjectToModule projectContext.scope
|
|
|
|
, modulesThatExposeSubscriptionsAndUpdate = projectContext.modulesThatExposeSubscriptionsAndUpdate
|
|
|
|
, definesUpdate = False
|
|
|
|
, definesSubscriptions = False
|
|
|
|
, usesUpdateOfModule = Dict.empty
|
|
|
|
, usesSubscriptionsOfModule = Set.empty
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fromModuleToProject : Rule.ModuleKey -> Node ModuleName -> ModuleContext -> ProjectContext
|
|
|
|
fromModuleToProject _ moduleName moduleContext =
|
|
|
|
{ scope = Scope.fromModuleToProject moduleName moduleContext.scope
|
|
|
|
, modulesThatExposeSubscriptionsAndUpdate =
|
|
|
|
if moduleContext.definesSubscriptions && moduleContext.definesUpdate then
|
|
|
|
Set.singleton (Node.value moduleName)
|
|
|
|
|
|
|
|
else
|
|
|
|
Set.empty
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
|
|
|
|
foldProjectContexts newContext previousContext =
|
|
|
|
{ scope = Scope.foldProjectContexts newContext.scope previousContext.scope
|
|
|
|
, modulesThatExposeSubscriptionsAndUpdate =
|
|
|
|
Set.union
|
|
|
|
newContext.modulesThatExposeSubscriptionsAndUpdate
|
|
|
|
previousContext.modulesThatExposeSubscriptionsAndUpdate
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-06-20 21:09:03 +03:00
|
|
|
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 moduleName "update" ->
|
2020-06-03 19:23:19 +03:00
|
|
|
let
|
|
|
|
realModuleName : List String
|
|
|
|
realModuleName =
|
|
|
|
Scope.moduleNameForValue moduleContext.scope "update" moduleName
|
|
|
|
in
|
|
|
|
if Set.member realModuleName moduleContext.modulesThatExposeSubscriptionsAndUpdate then
|
|
|
|
( [], { moduleContext | usesUpdateOfModule = Dict.insert realModuleName (Node.range node) moduleContext.usesUpdateOfModule } )
|
|
|
|
|
|
|
|
else
|
|
|
|
( [], moduleContext )
|
|
|
|
|
2020-06-20 21:09:03 +03:00
|
|
|
Expression.FunctionOrValue moduleName "subscriptions" ->
|
2020-06-03 19:23:19 +03:00
|
|
|
let
|
|
|
|
realModuleName : List String
|
|
|
|
realModuleName =
|
|
|
|
Scope.moduleNameForValue moduleContext.scope "subscriptions" moduleName
|
|
|
|
in
|
|
|
|
if Set.member realModuleName moduleContext.modulesThatExposeSubscriptionsAndUpdate then
|
|
|
|
( [], { moduleContext | usesSubscriptionsOfModule = Set.insert realModuleName moduleContext.usesSubscriptionsOfModule } )
|
|
|
|
|
|
|
|
else
|
|
|
|
( [], moduleContext )
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
( [], 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
|
|
|
|
)
|