This module contains functions that are used for writing rules.
elm-review reads the modules, elm.json, dependencies and README.md from your project,
and turns each module into an Abstract Syntax Tree (AST),
a tree-like structure which represents your source code, using the
elm-syntax package.
elm-review then feeds all this data into review rules, that traverse them to report problems.
The way that review rules go through the data depends on whether it is a module rule or a project rule.
elm-review relies on the elm-syntax package,
and all the node types you'll see will be coming from there. You are likely to
need to have the documentation for that package open when writing a rule.
There are plenty of examples in this documentation, and you can also look at the source code of existing rules to better grasp how they work.
NOTE: These examples are only here to showcase how to write rules and how a function can be used. They are not necessarily good rules to enforce. See the section on whether to write a rule for more on that. Even if you think they are good ideas to enforce, they are often not complete, as there are other patterns you would want to forbid, but that are not handled by the example.
Apart from the rationale on whether a rule should be written, here are a few tips on what makes a rule helpful.
A review rule is an automated communication tool which sends messages to developers who have written patterns your rule wishes to prevent. As all communication, the message is important.
The name of the rule (NoUnusedVariables, NoDebug, ...) should try to convey
really quickly what kind of pattern we're dealing with. Ideally, a user who
encounters this pattern for the first time could guess the problem just from the
name. And a user who encountered it several times should know how to fix the
problem just from the name too.
I recommend having the name of the module containing the rule be the same as the rule name. This will make it easier to find the module in the project or on the packages website when trying to get more information.
The error message should give more information about the problem. It is split into two parts:
message: A short sentence that describes the forbidden pattern. A user
that has encountered this error multiple times should know exactly what to do.
Example: "Function foo is never used". With this information, a user who
knows the rule probably knows that a function needs to be removed from the
source code, and also knows which one.details: All the additional information that can be useful to the
user, such as the rationale behind forbidding the pattern, and suggestions
for a solution or alternative.When writing the error message that the user will see, try to make them be as helpful as the messages the compiler gives you when it encounters a problem.
When creating an error, you need to specify under which section of the code this message appears. This is where you would see squiggly lines in your editor when you have review or compiler errors.
To make the error easier to spot, it is best to make this section as small as
possible, as long as that makes sense. For instance, in a rule that would forbid
Debug.log, you would the error to appear under Debug.log, not on the whole
function which contains this piece of code.
The rule documentation should give the same information as what you would see in the error message.
If published in a package, the rule documentation should explain when not to enable the rule in the user's review configuration. For instance, for a rule that makes sure that a package is publishable by ensuring that all docs are valid, the rule might say something along the lines of "If you are writing an application, then you should not use this rule".
Additionally, it could give a few examples of patterns that will be reported and of patterns that will not be reported, so that users can have a better grasp of what to expect.
This package comes with Review.Test, which works with elm-test.
I recommend reading through the strategies for effective testing before
starting writing a rule.
elm-syntaxelm-review is heavily dependent on the types that elm-syntax
provides. If you don't understand the AST it provides, you will have a hard time
implementing the rule you wish to create.
A "module rule" looks at modules (i.e. files) one by one. When it finishes looking at a module and reporting errors, it forgets everything about the module it just analyzed before starting to look at a different module. You should create one of these if you do not need to know the contents of a different module in the project, such as what functions are exposed. If you do need that information, you should create a project rule.
If you are new to writing rules, I would recommend learning how to build a module rule first, as they are in practice a simpler version of project rules.
The traversal of a module rule is the following:
elm.json file, visited by withElmJsonModuleVisitorREADME.md file, visited by withReadmeModuleVisitorwithDependenciesModuleVisitorwithSimpleModuleDefinitionVisitor and withModuleDefinitionVisitorwithSimpleCommentsVisitor and withCommentsVisitorwithSimpleImportVisitor and withImportVisitorwithDeclarationListVisitorwithSimpleDeclarationVisitor, withDeclarationEnterVisitor and withDeclarationExitVisitor.
Before evaluating the next declaration, the expression contained in the declaration
will be visited recursively by withSimpleExpressionVisitor, withExpressionEnterVisitor and withExpressionExitVisitorwithFinalModuleEvaluationEvaluating/visiting a node means two things:
moduleContext for module rules) to have more information available in a later
node evaluation. You can only use the context and update it with "non-simple with*" visitor functions.
I recommend using the "simple with*" visitor functions if you don't need to do either, as they are simpler to useRepresents a schema for a module Rule.
Start by using newModuleRuleSchema, then add visitors to look at the parts of the code you are interested in.
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebug" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchemaCreates a schema for a module rule. Will require adding module visitors
calling fromModuleRuleSchema to create a usable
Rule. Use "with*" functions from this module, like
withSimpleExpressionVisitor or withSimpleImportVisitor
to make it report something.
The first argument is the rule name. I highly recommend naming it just like the
module name (including all the . there may be).
The second argument is the initial moduleContext, i.e. the data that the rule will
accumulate as the module will be traversed, and allows the rule to know/remember
what happens in other parts of the module. If you don't need a context, I
recommend specifying (), and using functions from this module with names
starting with "withSimple".
NOTE: Do not store functions, JSON values or regular expressions in your contexts, as they will be compared internally, which [may cause Elm to crash](https://package.elm-lang.org/packages/elm/core/latest/Basics#(==)).
module My.Rule.Name exposing (rule)
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "My.Rule.Name" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.withSimpleImportVisitor importVisitor
|> Rule.fromModuleRuleSchemaIf you do need information from other parts of the module, then you should specify
an initial context, and I recommend using "with*" functions without "Simple" in
their name, like withExpressionEnterVisitor,
withImportVisitor or withFinalModuleEvaluation.
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUnusedVariables" initialContext
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.withImportVisitor importVisitor
|> Rule.fromModuleRuleSchema
type alias Context =
{ declaredVariables : List String
, usedVariables : List String
}
initialContext : Context
initialContext =
{ declaredVariables = [], usedVariables = [] }Create a Rule from a configured ModuleRuleSchema.
Add a visitor to the ModuleRuleSchema which will visit the module's module definition (module SomeModuleName exposing (a, b)) and report patterns.
The following example forbids having _ in any part of a module name.
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUnderscoreInModuleName" ()
|> Rule.withSimpleModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.fromModuleRuleSchema
moduleDefinitionVisitor : Node Module -> List (Error {})
moduleDefinitionVisitor node =
if List.any (String.contains "") (Node.value node |> Module.moduleName) then
[ Rule.error
{ message = "Do not use `_` in a module name"
, details = [ "By convention, Elm modules names use Pascal case (like `MyModuleName`). Please rename your module using this format." ]
}
(Node.range node)
]
else
[]Note: withSimpleModuleDefinitionVisitor is a simplified version of withModuleDefinitionVisitor,
which isn't passed a context and doesn't return one. You can use withSimpleModuleDefinitionVisitor even if you use "non-simple with*" functions.
Add a visitor to the ModuleRuleSchema which will visit the module's comments.
This visitor will give you access to the list of all comments in the module all at once.
The following example forbids words like "TODO" appearing in a comment.
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoTodoComment" ()
|> Rule.withSimpleCommentsVisitor commentsVisitor
|> Rule.fromModuleRuleSchema
commentsVisitor : List (Node String) -> List (Error {})
commentsVisitor comments =
comments
|> List.concatMap
(\commentNode ->
String.indexes "TODO" (Node.value commentNode)
|> List.map (errorAtPosition (Node.range commentNode))
)
errorAtPosition : Range -> Int -> Error {}
errorAtPosition range index =
Rule.error
{ message = "TODO needs to be handled"
, details = [ "At fruits.com, we prefer not to have lingering TODO comments. Either fix the TODO now or create an issue for it." ]
}
-- Here you would ideally only target the TODO keyword
-- or the rest of the line it appears on,
-- so you would change `range` using `index`.
rangeNote: withSimpleCommentsVisitor is a simplified version of withCommentsVisitor,
which isn't passed a context and doesn't return one. You can use withCommentsVisitor even if you use "non-simple with*" functions.
Add a visitor to the ModuleRuleSchema which will visit the module's import statements (import Html as H exposing (div)) in order of their definition and report patterns.
The following example forbids using the core Html package and suggests using
elm-css instead.
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoCoreHtml" ()
|> Rule.withSimpleImportVisitor importVisitor
|> Rule.fromModuleRuleSchema
importVisitor : Node Import -> List (Error {})
importVisitor node =
let
moduleName : List String
moduleName =
node
|> Node.value
|> .moduleName
|> Node.value
in
case moduleName of
[ "Html" ] ->
[ Rule.error
{ message = "Use `elm-css` instead of the core HTML package."
, details =
[ "At fruits.com, we chose to use the `elm-css` package (https://package.elm-lang.org/packages/rtfeldman/elm-css/latest/Css) to build our HTML and CSS rather than the core Html package. To keep things simple, we think it is best to not mix these different libraries."
, "The API is very similar, but instead of using the `Html` module, use the `Html.Styled`. CSS is then defined using the Html.Styled.Attributes.css function (https://package.elm-lang.org/packages/rtfeldman/elm-css/latest/Html-Styled-Attributes#css)."
]
}
(Node.range node)
]
_ ->
[]Note: withSimpleImportVisitor is a simplified version of withImportVisitor,
which isn't passed a context and doesn't return one. You can use withSimpleImportVisitor even if you use "non-simple with*" functions.
Add a visitor to the ModuleRuleSchema which will visit the module's
declaration statements
(someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg)
and report patterns. The declarations will be visited in the order of their definition.
The following example forbids declaring a function or a value without a type annotation.
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoMissingTypeAnnotation" ()
|> Rule.withSimpleDeclarationVisitor declarationVisitor
|> Rule.fromModuleRuleSchema
declarationVisitor : Node Declaration -> List (Error {})
declarationVisitor node =
case Node.value node of
Declaration.FunctionDeclaration { signature, declaration } ->
case signature of
Just _ ->
[]
Nothing ->
let
functionName : String
functionName =
declaration |> Node.value |> .name |> Node.value
in
[ Rule.error
{ message = "Missing type annotation for `" ++ functionName ++ "`"
, details =
[ "Type annotations are very helpful for people who read your code. It can give a lot of information without having to read the contents of the function. When encountering problems, the compiler will also give much more precise and helpful information to help you solve the problem."
, "To add a type annotation, add a line like `" functionName ++ " : ()`, and replace the `()` by the type of the function. If you don't replace `()`, the compiler should give you a suggestion of what the type should be."
]
}
(Node.range node)
]
_ ->
[]Note: withSimpleDeclarationVisitor is a simplified version of withDeclarationEnterVisitor,
which isn't passed a context and doesn't return one either. You can use withSimpleDeclarationVisitor even if you use "non-simple with*" functions.
Add a visitor to the ModuleRuleSchema which will visit the module's
expressions
(1, True, add 1 2, 1 + 2). The expressions are visited in pre-order
depth-first search, meaning that an expression will be visited, then its first
child, the first child's children (and so on), then the second child (and so on).
The following example forbids using the Debug module.
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebug" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
expressionVisitor : Node Expression -> List (Error {})
expressionVisitor node =
case Node.value node of
Expression.FunctionOrValue moduleName fnName ->
if List.member "Debug" moduleName then
[ Rule.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
}
(Node.range node)
]
else
[]
_ ->
[]Note: withSimpleExpressionVisitor is a simplified version of withExpressionEnterVisitor,
which isn't passed a context and doesn't return one either. You can use withSimpleExpressionVisitor even if you use "non-simple with*" functions.
Same as newModuleRuleSchema, except that you can request for data to help initialize the context.
compared internally, which [may cause Elm to crash](https://package.elm-lang.org/packages/elm/core/latest/Basics#(==)).
module My.Rule.Name exposing (rule)
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "My.Rule.Name" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.withSimpleImportVisitor importVisitor
|> Rule.fromModuleRuleSchemaIf you do need information from other parts of the module, then you should specify
an initial context, and I recommend using "with*" functions without "Simple" in
their name, like withExpressionEnterVisitor,
withImportVisitor or withFinalModuleEvaluation.
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchemaUsingContextCreator "Rule.Name" contextCreator
-- visitors
|> Rule.fromModuleRuleSchema
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\metadata () ->
{ hasTodoBeenImported = False
, hasToStringBeenImported = False
, isInSourceDirectories = Rule.isInSourceDirectories metadata
}
)
|> Rule.withMetadataAdd a visitor to the ModuleRuleSchema which will visit the module's
module definition (module SomeModuleName exposing (a, b)), collect data in the context and/or report patterns.
The following example forbids the use of Html.button except in the "Button" module.
The example is simplified to only forbid the use of the Html.button expression.
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
type Context
= HtmlButtonIsAllowed
| HtmlButtonIsForbidden
rule : Rule
rule =
Rule.newModuleRuleSchema "NoHtmlButton" HtmlButtonIsForbidden
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
moduleDefinitionVisitor : Node Module -> Context -> ( List (Error {}), Context )
moduleDefinitionVisitor node context =
if (Node.value node |> Module.moduleName) == [ "Button" ] then
( [], HtmlButtonIsAllowed )
else
( [], HtmlButtonIsForbidden )
expressionVisitor : Node Expression -> Context -> ( List (Error {}), Context )
expressionVisitor node context =
case context of
HtmlButtonIsAllowed ->
( [], context )
HtmlButtonIsForbidden ->
case Node.value node of
Expression.FunctionOrValue [ "Html" ] "button" ->
( [ Rule.error
{ message = "Do not use `Html.button` directly"
, details = [ "At fruits.com, we've built a nice `Button` module that suits our needs better. Using this module instead of `Html.button` ensures we have a consistent button experience across the website." ]
}
(Node.range node)
]
, context
)
_ ->
( [], context )
_ ->
( [], context )Tip: If you do not need to collect data in this visitor, you may wish to use the
simpler withSimpleModuleDefinitionVisitor function.
Tip: The rule above is very brittle. What if button was imported using import Html exposing (button) or import Html exposing (..), or if Html was aliased (import Html as H)? Then the rule above would
not catch and report the use Html.button. To handle this, check out withModuleNameLookupTable.
Add a visitor to the ModuleRuleSchema which will visit the module's comments, collect data in
the context and/or report patterns.
This visitor will give you access to the list of all comments in the module all at once.
Tip: If you do not need to collect data in this visitor, you may wish to use the
simpler withSimpleCommentsVisitor function.
Add a visitor to the ModuleRuleSchema which will visit the module's
import statements
(import Html as H exposing (div)) in order of their definition, collect data
in the context and/or report patterns.
The following example forbids importing both Element (elm-ui) and
Html.Styled (elm-css).
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
type alias Context =
{ elmUiWasImported : Bool
, elmCssWasImported : Bool
}
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUsingBothHtmlAndHtmlStyled" initialContext
|> Rule.withImportVisitor importVisitor
|> Rule.fromModuleRuleSchema
initialContext : Context
initialContext =
{ elmUiWasImported = False
, elmCssWasImported = False
}
error : Node Import -> Error {}
error node =
Rule.error
{ message = "Do not use both `elm-ui` and `elm-css`"
, details = [ "At fruits.com, we use `elm-ui` in the dashboard application, and `elm-css` in the rest of the code. We want to use `elm-ui` in our new projects, but in projects using `elm-css`, we don't want to use both libraries to keep things simple." ]
}
(Node.range node)
importVisitor : Node Import -> Context -> ( List (Error {}), Context )
importVisitor node context =
case Node.value node |> .moduleName |> Node.value of
[ "Element" ] ->
if context.elmCssWasImported then
( [ error node ]
, { context | elmUiWasImported = True }
)
else
( [ error node ]
, { context | elmUiWasImported = True }
)
[ "Html", "Styled" ] ->
if context.elmUiWasImported then
( [ error node ]
, { context | elmCssWasImported = True }
)
else
( [ error node ]
, { context | elmCssWasImported = True }
)
_ ->
( [], context )This example was written in a different way in the example for withFinalModuleEvaluation.
Tip: If you do not need to collect or use the context in this visitor, you may wish to use the
simpler withSimpleImportVisitor function.
DEPRECATED
This is used in withDeclarationVisitor and withDeclarationVisitor,
which are deprecated and will be removed in the next major version. This type will be removed along with them.
To replicate the same behavior, take a look at
withDeclarationEnterVisitor and withDeclarationExitVisitor.withExpressionEnterVisitor and withExpressionExitVisitor./DEPRECATED
Represents whether a node is being traversed before having seen its children (OnEntering the node), or after (OnExiting the node).
When visiting the AST, declaration and expression nodes are visited twice: once
with OnEnter, before the children of the node are visited, and once with
OnExit, after the children of the node have been visited.
In most cases, you'll only want to handle the OnEnter case, but there are cases
where you'll want to visit a Node
after having seen its children.
For instance, if you are trying to detect the unused variables defined inside of a let expression, you will want to collect the declaration of variables, note which ones are used, and at the end of the block report the ones that weren't used.
expressionVisitor : Node Expression -> Direction -> Context -> ( List (Error {}), Context )
expressionVisitor node direction context =
case ( direction, Node.value node ) of
( Rule.OnEnter, Expression.FunctionOrValue moduleName name ) ->
( [], markVariableAsUsed context name )
-- Find variables declared in let expression
( Rule.OnEnter, Expression.LetExpression letBlock ) ->
( [], registerVariables context letBlock )
-- When exiting the let expression, report the variables that were not used.
( Rule.OnExit, Expression.LetExpression _ ) ->
( unusedVariables context |> List.map createError, context )
_ ->
( [], context )Add a visitor to the ModuleRuleSchema which will visit the module's
declaration statements
(someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg),
collect data and/or report patterns. The declarations will be visited in the order of their definition.
The following example forbids exposing a function or a value without it having a type annotation.
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
type ExposedFunctions
= All
| OnlySome (List String)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoMissingDocumentationForExposedFunctions" (OnlySome [])
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withDeclarationEnterVisitor declarationVisitor
|> Rule.fromModuleRuleSchema
moduleDefinitionVisitor : Node Module -> ExposedFunctions -> ( List (Error {}), ExposedFunctions )
moduleDefinitionVisitor node context =
case Node.value node |> Module.exposingList of
Exposing.All _ ->
( [], All )
Exposing.Explicit exposedValues ->
( [], OnlySome (List.filterMap exposedFunctionName exposedValues) )
exposedFunctionName : Node Exposing.TopLevelExpose -> Maybe String
exposedFunctionName value =
case Node.value value of
Exposing.FunctionExpose functionName ->
Just functionName
_ ->
Nothing
declarationVisitor : Node Declaration -> ExposedFunctions -> ( List (Error {}), ExposedFunctions )
declarationVisitor node direction context =
case Node.value node of
Declaration.FunctionDeclaration { documentation, declaration } ->
let
functionName : String
functionName =
Node.value declaration |> .name |> Node.value
in
if documentation == Nothing && isExposed context functionName then
( [ Rule.error
{ message = "Exposed function " ++ functionName ++ " is missing a type annotation"
, details =
[ "Type annotations are very helpful for people who use the module. It can give a lot of information without having to read the contents of the function."
, "To add a type annotation, add a line like `" functionName ++ " : ()`, and replace the `()` by the type of the function. If you don't replace `()`, the compiler should give you a suggestion of what the type should be."
]
}
(Node.range node)
]
, context
)
else
( [], context )
_ ->
( [], context )
isExposed : ExposedFunctions -> String -> Bool
isExposed exposedFunctions name =
case exposedFunctions of
All ->
True
OnlySome exposedList ->
List.member name exposedListTip: If you do not need to collect or use the context in this visitor, you may wish to use the
simpler withSimpleDeclarationVisitor function.
Add a visitor to the ModuleRuleSchema which will visit the module's
declaration statements
(someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg),
collect data and/or report patterns. The declarations will be visited in the order of their definition.
The following example reports unused parameters from top-level declarations.
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
|> Rule.withDeclarationEnterVisitor declarationEnterVisitor
|> Rule.withDeclarationExitVisitor declarationExitVisitor
-- Omitted, but this marks parameters as used
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
declarationEnterVisitor : Node Declaration -> fContext -> ( List (Error {}), Context )
declarationEnterVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration function ->
( [], registerArguments context function )
_ ->
( [], context )
declarationExitVisitor : Node Declaration -> Context -> ( List (Error {}), Context )
declarationExitVisitor node context =
case Node.value node of
-- When exiting the function expression, report the parameters that were not used.
Declaration.FunctionDeclaration function ->
( unusedParameters context |> List.map createError, removeArguments context )
_ ->
( [], context )DEPRECATED
Use withDeclarationEnterVisitor and withDeclarationExitVisitor instead.
In the next major version, this function will be removed and withDeclarationEnterVisitor will be renamed to withDeclarationVisitor.
/DEPRECATED
Add a visitor to the ModuleRuleSchema which will visit the module's
declaration statements
(someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg),
collect data and/or report patterns. The declarations will be visited in the order of their definition.
Contrary to withSimpleDeclarationVisitor, the
visitor function will be called twice with different Direction
values. It will be visited with OnEnter, then the children will be visited,
and then it will be visited again with OnExit. If you do not check the value of
the Direction parameter, you might end up with duplicate errors and/or an
unexpected moduleContext. Read more about Direction here.
The following example forbids exposing a function or a value without it having a type annotation.
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
type ExposedFunctions
= All
| OnlySome (List String)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoMissingDocumentationForExposedFunctions" (OnlySome [])
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withDeclarationVisitor declarationVisitor
|> Rule.fromModuleRuleSchema
moduleDefinitionVisitor : Node Module -> ExposedFunctions -> ( List (Error {}), ExposedFunctions )
moduleDefinitionVisitor node context =
case Node.value node |> Module.exposingList of
Exposing.All _ ->
( [], All )
Exposing.Explicit exposedValues ->
( [], OnlySome (List.filterMap exposedFunctionName exposedValues) )
exposedFunctionName : Node Exposing.TopLevelExpose -> Maybe String
exposedFunctionName value =
case Node.value value of
Exposing.FunctionExpose functionName ->
Just functionName
_ ->
Nothing
declarationVisitor : Node Declaration -> Direction -> ExposedFunctions -> ( List (Error {}), ExposedFunctions )
declarationVisitor node direction context =
case ( direction, Node.value node ) of
( Rule.OnEnter, Declaration.FunctionDeclaration { documentation, declaration } ) ->
let
functionName : String
functionName =
Node.value declaration |> .name |> Node.value
in
if documentation == Nothing && isExposed context functionName then
( [ Rule.error
{ message = "Exposed function " ++ functionName ++ " is missing a type annotation"
, details =
[ "Type annotations are very helpful for people who use the module. It can give a lot of information without having to read the contents of the function."
, "To add a type annotation, add a line like `" functionName ++ " : ()`, and replace the `()` by the type of the function. If you don't replace `()`, the compiler should give you a suggestion of what the type should be."
]
}
(Node.range node)
]
, context
)
else
( [], context )
_ ->
( [], context )
isExposed : ExposedFunctions -> String -> Bool
isExposed exposedFunctions name =
case exposedFunctions of
All ->
True
OnlySome exposedList ->
List.member name exposedListTip: If you do not need to collect or use the context in this visitor, you may wish to use the
simpler withSimpleDeclarationVisitor function.
Add a visitor to the ModuleRuleSchema which will visit the module's
declaration statements
(someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg),
collect data and/or report patterns.
It is similar to withDeclarationVisitor, but the visitor used with this function is called before the visitor added with withDeclarationVisitor. You can use this visitor in order to look ahead and add the module's types and variables into your context, before visiting the contents of the module using withDeclarationVisitor and withExpressionEnterVisitor. Otherwise, using withDeclarationVisitor is probably a simpler choice.
Add a visitor to the ModuleRuleSchema which will visit the module's
expressions
(1, True, add 1 2, 1 + 2), collect data in the context and/or report patterns.
The expressions are visited in pre-order depth-first search, meaning that an
expression will be visited, then its first child, the first child's children
(and so on), then the second child (and so on).
Contrary to withExpressionVisitor, the
visitor function will be called only once, when the expression is "entered",
meaning before its children are visited.
The following example forbids the use of Debug.log even when it is imported like
import Debug exposing (log).
import Elm.Syntax.Exposing as Exposing exposing (TopLevelExpose)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
type Context
= DebugLogWasNotImported
| DebugLogWasImported
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
|> Rule.withImportVisitor importVisitor
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
importVisitor : Node Import -> Context -> ( List (Error {}), Context )
importVisitor node context =
case ( Node.value node |> .moduleName |> Node.value, (Node.value node).exposingList |> Maybe.map Node.value ) of
( [ "Debug" ], Just (Exposing.All _) ) ->
( [], DebugLogWasImported )
( [ "Debug" ], Just (Exposing.Explicit exposedFunctions) ) ->
let
isLogFunction : Node Exposing.TopLevelExpose -> Bool
isLogFunction exposeNode =
case Node.value exposeNode of
Exposing.FunctionExpose "log" ->
True
_ ->
False
in
if List.any isLogFunction exposedFunctions then
( [], DebugLogWasImported )
else
( [], DebugLogWasNotImported )
_ ->
( [], DebugLogWasNotImported )
expressionVisitor : Node Expression -> Context -> ( List (Error {}), Context )
expressionVisitor node context =
case context of
DebugLogWasNotImported ->
( [], context )
DebugLogWasImported ->
case Node.value node of
Expression.FunctionOrValue [] "log" ->
( [ Rule.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
}
(Node.range node)
]
, context
)
_ ->
( [], context )Tip: If you do not need to collect or use the context in this visitor, you may wish to use the
simpler withSimpleExpressionVisitor function.
Add a visitor to the ModuleRuleSchema which will visit the module's
expressions
(1, True, add 1 2, 1 + 2), collect data in the context and/or report patterns.
The expressions are visited in pre-order depth-first search, meaning that an
expression will be visited, then its first child, the first child's children
(and so on), then the second child (and so on).
Contrary to withExpressionEnterVisitor, the
visitor function will be called when the expression is "exited",
meaning after its children are visited.
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
|> Rule.withExpressionEnterVisitor expressionEnterVisitor
|> Rule.withExpressionExitVisitor expressionExitVisitor
|> Rule.fromModuleRuleSchema
expressionEnterVisitor : Node Expression -> fContext -> ( List (Error {}), Context )
expressionEnterVisitor node context =
case Node.value node of
Expression.FunctionOrValue moduleName name ->
( [], markVariableAsUsed context name )
-- Find variables declared in let expression
Expression.LetExpression letBlock ->
( [], registerVariables context letBlock )
_ ->
( [], context )
expressionExitVisitor : Node Expression -> Context -> ( List (Error {}), Context )
expressionExitVisitor node context =
case Node.value node of
-- When exiting the let expression, report the variables that were not used.
Expression.LetExpression _ ->
( unusedVariables context |> List.map createError, removeVariables context )
_ ->
( [], context )DEPRECATED
Use withExpressionEnterVisitor and withExpressionExitVisitor instead.
In the next major version, this function will be removed and withExpressionEnterVisitor will be renamed to withExpressionVisitor.
/DEPRECATED
Add a visitor to the ModuleRuleSchema which will visit the module's
expressions
(1, True, add 1 2, 1 + 2), collect data in the context and/or report patterns.
The expressions are visited in pre-order depth-first search, meaning that an
expression will be visited, then its first child, the first child's children
(and so on), then the second child (and so on).
Contrary to withSimpleExpressionVisitor, the
visitor function will be called twice with different Direction
values. It will be visited with OnEnter, then the children will be visited,
and then it will be visited again with OnExit. If you do not check the value of
the Direction parameter, you might end up with duplicate errors and/or an
unexpected moduleContext. Read more about Direction here.
The following example forbids the use of Debug.log even when it is imported like
import Debug exposing (log).
import Elm.Syntax.Exposing as Exposing exposing (TopLevelExpose)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
type Context
= DebugLogWasNotImported
| DebugLogWasImported
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
|> Rule.withImportVisitor importVisitor
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
importVisitor : Node Import -> Context -> ( List (Error {}), Context )
importVisitor node context =
case ( Node.value node |> .moduleName |> Node.value, (Node.value node).exposingList |> Maybe.map Node.value ) of
( [ "Debug" ], Just (Exposing.All _) ) ->
( [], DebugLogWasImported )
( [ "Debug" ], Just (Exposing.Explicit exposedFunctions) ) ->
let
isLogFunction : Node Exposing.TopLevelExpose -> Bool
isLogFunction exposeNode =
case Node.value exposeNode of
Exposing.FunctionExpose "log" ->
True
_ ->
False
in
if List.any isLogFunction exposedFunctions then
( [], DebugLogWasImported )
else
( [], DebugLogWasNotImported )
_ ->
( [], DebugLogWasNotImported )
expressionVisitor : Node Expression -> Direction -> Context -> ( List (Error {}), Context )
expressionVisitor node direction context =
case context of
DebugLogWasNotImported ->
( [], context )
DebugLogWasImported ->
case ( direction, Node.value node ) of
( Rule.OnEnter, Expression.FunctionOrValue [] "log" ) ->
( [ Rule.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
}
(Node.range node)
]
, context
)
_ ->
( [], context )Tip: If you do not need to collect or use the context in this visitor, you may wish to use the
simpler withSimpleExpressionVisitor function.
Add a function that makes a final evaluation of the module based only on the
data that was collected in the moduleContext. This can be useful if you can't or if
it is hard to determine something as you traverse the module.
The following example forbids importing both Element (elm-ui) and
Html.Styled (elm-css). Note that this is the same one written in the example
for withImportVisitor, but using withFinalModuleEvaluation.
import Dict as Dict exposing (Dict)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.Rule as Rule exposing (Error, Rule)
type alias Context =
Dict (List String) Range
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUsingBothHtmlAndHtmlStyled" Dict.empty
|> Rule.withImportVisitor importVisitor
|> Rule.withFinalModuleEvaluation finalEvaluation
|> Rule.fromModuleRuleSchema
importVisitor : Node Import -> Context -> ( List (Error {}), Context )
importVisitor node context =
( [], Dict.insert (Node.value node |> .moduleName |> Node.value) (Node.range node) context )
finalEvaluation : Context -> List (Error {})
finalEvaluation context =
case ( Dict.get [ "Element" ] context, Dict.get [ "Html", "Styled" ] context ) of
( Just elmUiRange, Just _ ) ->
[ Rule.error
{ message = "Do not use both `elm-ui` and `elm-css`"
, details = [ "At fruits.com, we use `elm-ui` in the dashboard application, and `elm-css` in the rest of the code. We want to use `elm-ui` in our new projects, but in projects using `elm-css`, we don't want to use both libraries to keep things simple." ]
}
elmUiRange
]
_ ->
[]Add a visitor to the ModuleRuleSchema which will visit the project's
elm.json file.
The following example forbids exposing a module in an "Internal" directory in your elm.json file.
import Elm.Module
import Elm.Project
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
type alias Context =
Maybe Elm.Project.Project
rule : Rule
rule =
Rule.newModuleRuleSchema "DoNoExposeInternalModules" Nothing
|> Rule.withElmJsonModuleVisitor elmJsonVisitor
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.fromModuleRuleSchema
elmJsonVisitor : Maybe Elm.Project.Project -> Context -> Context
elmJsonVisitor elmJson context =
elmJson
moduleDefinitionVisitor : Node Module -> Context -> ( List (Error {}), Context )
moduleDefinitionVisitor node context =
let
moduleName : List String
moduleName =
Node.value node |> Module.moduleName
in
if List.member "Internal" moduleName then
case context of
Just (Elm.Project.Package { exposed }) ->
let
exposedModules : List String
exposedModules =
case exposed of
Elm.Project.ExposedList names ->
names
|> List.map Elm.Module.toString
Elm.Project.ExposedDict fakeDict ->
fakeDict
|> List.concatMap Tuple.second
|> List.map Elm.Module.toString
in
if List.member (String.join "." moduleName) exposedModules then
( [ Rule.error "Do not expose modules in `Internal` as part of the public API" (Node.range node) ], context )
else
( [], context )
_ ->
( [], context )
else
( [], context )Add a visitor to the ModuleRuleSchema which will visit
the project's README.md file.
Add a visitor to the ProjectRuleSchema which will visit the project's
dependencies.
You can use this look at the modules contained in dependencies, which can make the rule very precise when it targets specific functions.
Project rules can look at the global picture of an Elm project. Contrary to module rules, which forget everything about the module they were looking at when going from one module to another, project rules can retain information about previously analyzed modules, and use it to report errors when analyzing a different module or after all modules have been visited.
Project rules can also report errors in the elm.json or the README.md files.
If you are new to writing rules, I would recommend learning how to build a module rule first, as they are in practice a simpler version of project rules.
Represents a schema for a project Rule.
See the documentation for newProjectRuleSchema for
how to create a project rule.
Creates a schema for a project rule. Will require adding project visitors and calling
fromProjectRuleSchema to create a usable Rule.
The first argument is the rule name. I highly recommend naming it just like the
module name (including all the . there may be).
The second argument is the initial projectContext, i.e. the data that the rule will
accumulate as the project will be traversed, and allows the rule to know/remember
what happens in other parts of the project.
NOTE: Do not store functions, JSON values or regular expressions in your contexts, as they will be compared internally, which [may cause Elm to crash](https://package.elm-lang.org/packages/elm/core/latest/Basics#(==)).
Project rules traverse the project in the following order:
elm.json file, visited by withElmJsonProjectVisitorREADME.md file, visited by withReadmeProjectVisitorwithDependenciesProjectVisitorwithModuleVisitor,
following the same traversal order as for module rules but without reading the project files (elm.json, ...).withFinalProjectEvaluationEvaluating/visiting a node means two things:
projectContext or a moduleContext depending on the part of the project being visited, to have more information available in a later
part of the traversal evaluation.Create a Rule from a configured ProjectRuleSchema.
Add a visitor to the ProjectRuleSchema which will
visit the project's Elm modules.
A module visitor behaves like a module rule, except that it won't visit the
project files, as those have already been seen by other visitors for project rules (such
as withElmJsonProjectVisitor).
withModuleVisitor takes a function that takes an already initialized module
rule schema and adds visitors to it, using the same functions as for building a
ModuleRuleSchema.
When you use withModuleVisitor, you will be required to use withModuleContext,
in order to specify how to create a moduleContext from a projectContext and vice-versa.
Specify, if the project rule has a module visitor, how to:
fromProjectToModulefromModuleToProjectfoldProjectContextsNOTE: I suggest reading the section about foldProjectContexts carefully,
as it is one whose implementation you will need to do carefully.
In project rules, we separate the context related to the analysis of the project
as a whole and the context related to the analysis of a single module into a
projectContext and a moduleContext respectively. We do this because in most
project rules you won't need all the data from the projectContext to analyze a
module, and some data from the module context will not make sense inside the
project context.
When visiting modules, elm-review follows a kind of map-reduce architecture.
The idea is the following: it starts with an initial projectContext and collects data
from project-related files into it. Then, it visits every module with an initial
moduleContext derived from a projectContext. At the end of a module's visit,
the final moduleContext will be transformed ("map") to a projectContext.
All or some of the projectContexts will then be folded into a single one,
before being used in the final project evaluation or to compute another module's
initial moduleContext.
This will help make the result of the review as consistent as possible, by having the results be independent of the order the modules are visited. This also gives internal guarantees as to what needs to be re-computed when re-analyzing the project, which leads to huge performance boosts in watch mode or after fixes have been applied.
The following sections will explain each function, and will be summarized by an example.
fromProjectToModuleThe initial moduleContext of the module visitor is computed using fromProjectToModule
from a projectContext. By default, this projectContext will be the result of
visiting the project-related files (elm.json, README.md, ...).
If withContextFromImportedModules was used, then the value will be this last
projectContext, folded with each imported module's resulting projectContext,
using foldProjectContexts.
The ModuleKey will allow you to report errors for this specific module
using errorForModule from the final project evaluation or
while visiting another module. If you plan to do that, you should store this in
the moduleContext. You can also get it from fromModuleToProject, so choose
what's most convenient.
The Node containing the module name is passed for convenience, so you don't
have to visit the module definition just to get the module name. Just like what
it is in elm-syntax,
the value will be [ "My", "Module" ] if the module name is My.Module.
fromModuleToProjectWhen a module has finished being analyzed, the final moduleContext will be
converted into a projectContext, so that it can later be folded with the other
project contexts using foldProjectContexts. The resulting projectContext
will be fed into the final project evaluation and potentially into
fromProjectToModule for modules that import the current one.
Similarly to fromProjectToModule, the Node containing the module name and
the ModuleKey are passed for convenience, so you don't have to store them in
the moduleContext only to store them in the projectContext.
foldProjectContextsThis function folds two projectContext into one. This function requires a few
traits to always be true.
projectContexts should be "merged" together, not "subtracted". If for instance
you want to detect the unused exports of a module, do not remove a declared
export when you have found it used. Instead, store and accumulate the declared
and used functions (both probably as Sets or Dicts), and in the final evaluation,
filter out the declared functions if they are in the set of used functions.foldProjectContexts b (foldProjectContexts a initial)
should equal foldProjectContexts a (foldProjectContexts b initial).
List.concat.foldProjectContexts a (foldProjectContexts a initial)
should equal foldProjectContexts a initial. You will likely need to use functions
like Set.union
and Dict.union
over addition and functions like
List.concat.It is not necessary for the function to be commutative (i.e. that
foldProjectContexts a b equals foldProjectContexts b a). It is fine to take
the value from the "initial" projectContext and ignore the other one, especially
for data computed in the project-related visitors (for which you will probably
define a dummy value in the fromModuleToProject function). If it helps, imagine
that the second argument is the initial projectContext, or that it is an accumulator
just like in List.foldl.
As an example, we will write a rule that reports functions that get exported but are unused in the rest of the project.
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newProjectRuleSchema "NoUnusedExportedFunctions" initialProjectContext
-- Omitted, but this will collect the list of exposed modules for packages.
-- We don't want to report functions that are exposed
|> Rule.withElmJsonProjectVisitor elmJsonVisitor
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContext
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.withFinalProjectEvaluation finalEvaluationForProject
|> Rule.fromProjectRuleSchema
moduleVisitor :
Rule.ModuleRuleSchema {} ModuleContext
-> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema =
schema
-- Omitted, but this will collect the exposed functions
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
-- Omitted, but this will collect uses of exported functions
|> Rule.withExpressionEnterVisitor expressionVisitor
type alias ProjectContext =
{ -- Modules exposed by the package, that we should not report
exposedModules : Set ModuleName
, exposedFunctions :
-- An entry for each module
Dict ModuleName
{ -- To report errors in this module
moduleKey : Rule.ModuleKey
-- An entry for each function with its location
, exposed : Dict String Range
}
, used : Set ( ModuleName, String )
}
type alias ModuleContext =
{ isExposed : Bool
, exposed : Dict String Range
, used : Set ( ModuleName, String )
}
initialProjectContext : ProjectContext
initialProjectContext =
{ exposedModules = Set.empty
, modules = Dict.empty
, used = Set.empty
}
fromProjectToModule : Rule.ModuleKey -> Node ModuleName -> ProjectContext -> ModuleContext
fromProjectToModule moduleKey moduleName projectContext =
{ isExposed = Set.member (Node.value moduleName) projectContext.exposedModules
, exposed = Dict.empty
, used = Set.empty
}
fromModuleToProject : Rule.ModuleKey -> Node ModuleName -> ModuleContext -> ProjectContext
fromModuleToProject moduleKey moduleName moduleContext =
{ -- We don't care about this value, we'll take
-- the one from the initial context when folding
exposedModules = Set.empty
, exposedFunctions =
if moduleContext.isExposed then
-- If the module is exposed, don't collect the exported functions
Dict.empty
else
-- Create a dictionary with all the exposed functions, associated to
-- the module that was just visited
Dict.singleton
(Node.value moduleName)
{ moduleKey = moduleKey
, exposed = moduleContext.exposed
}
, used = moduleContext.used
}
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts newContext previousContext =
{ -- Always take the one from the "initial" context,
-- which is always the second argument
exposedModules = previousContext.exposedModules
-- Collect the exposed functions from the new context and the previous one.
-- We could use `Dict.merge`, but in this case, that doesn't change anything
, exposedFunctions = Dict.union previousContext.modules newContext.modules
-- Collect the used functions from the new context and the previous one
, used = Set.union newContext.used previousContext.used
}
finalEvaluationForProject : ProjectContext -> List (Error { useErrorForModule : () })
finalEvaluationForProject projectContext =
-- Implementation of `unusedFunctions` omitted, but it returns the list
-- of unused functions, along with the associated module key and range
unusedFunctions projectContext
|> List.map
(\{ moduleKey, functionName, range } ->
Rule.errorForModule moduleKey
{ message = "Function `" ++ functionName ++ "` is never used"
, details = [ "<Omitted>" ]
}
range
)Use a ContextCreator to initialize your moduleContext and projectContext. This will allow
you to request more information
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newProjectRuleSchema "NoMissingSubscriptionsCall" initialProjectContext
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = Rule.initContextCreator fromProjectToModule
, fromModuleToProject =
Rule.initContextCreator fromModuleToProject
|> Rule.withModuleKey
|> Rule.withMetadata
, foldProjectContexts = foldProjectContexts
}
|> Rule.fromProjectRuleSchema
fromProjectToModule : ProjectContext -> ModuleContext
fromProjectToModule projectContext =
{ -- something
}
fromModuleToProject : Rule.ModuleKey -> Metadata -> ModuleContext -> ProjectContext
fromModuleToProject moduleKey Metadata moduleContext =
{ moduleKeys = Dict.singleton (Rule.moduleNameFromMetadata metadata) moduleKey
}Add a visitor to the ProjectRuleSchema which will visit the project's
elm.json file.
It works exactly like withElmJsonModuleVisitor.
The visitor will be called before any module is evaluated.
Add a visitor to the ProjectRuleSchema which will visit
the project's README.md file.
It works exactly like withReadmeModuleVisitor.
The visitor will be called before any module is evaluated.
Add a visitor to the ProjectRuleSchema which will visit the project's
dependencies.
It works exactly like withDependenciesModuleVisitor. The visitor will be called before any
module is evaluated.
Add a function that makes a final evaluation of the project based only on the
data that was collected in the projectContext. This can be useful if you can't report something until you have visited
all the modules in the project.
It works similarly withFinalModuleEvaluation.
NOTE: Do not create errors using the error function using withFinalProjectEvaluation, but using errorForModule
instead. When the project is evaluated in this function, you are not in the "context" of an Elm module (the idiomatic "context", not projectContext or moduleContext).
That means that if you call error, we won't know which module to associate the error to.
Allows the rule to have access to the context of the modules imported by the currently visited module. You can use for instance to know what is exposed in a different module.
When you finish analyzing a module, the moduleContext is turned into a projectContext
through fromModuleToProject. Before analyzing a module,
the projectContexts of its imported modules get folded into a single one
starting with the initial context (that may have visited the
elm.json file and/or the project's dependencies)
using foldProjectContexts.
If there is information about another module that you wish to access, you should
therefore store it in the moduleContext, and have it persist when transitioning
to a projectContext and back to a moduleContext.
You can only access data from imported modules, not from modules that import the current module. If you need to do so, I suggest collecting all the information you need, and re-evaluate if from the final project evaluation function.
If you don't use this function, you will only be able to access the contents of the initial context. The benefit is that when re-analyzing the project, after a fix or when a file was changed in watch mode, much less work will need to be done and the analysis will be much faster, because we know other files won't influence the results of other modules' analysis.
Create a context based on some other context.
Use functions like withMetadata to request more information.
from will usually be a projectContext and to a moduleContext.
Metadata for the module being visited.
Do not store the metadata directly in your context. Prefer storing the indiviaual pieces of informa
Initialize a new context creator.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\metadata () ->
{ moduleName = Rule.moduleNameFromMetadata metadata
-- ...other fields
}
)
|> Rule.withMetadataLearn whether the current module is in the "source-directories" of the project. You can use this information to know whether the module is part of the tests or of the production code.
Get the module name of the current module.
Get the Node to the module name of the current module.
Request metadata about the module.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\metadata () ->
{ moduleName = Rule.moduleNameFromMetadata metadata
, moduleNameNode = Rule.moduleNameNodeFromMetadata metadata
, isInSourceDirectories = Rule.isInSourceDirectories metadata
-- ...other fields
}
)
|> Rule.withMetadataRequests the module name lookup table for the types and functions inside a module.
When encountering a Expression.FunctionOrValue ModuleName String (among other nodes where we refer to a function or value),
the module name available represents the module name that is in the source code. But that module name can be an alias to
a different import, or it can be empty, meaning that it refers to a local value or one that has been imported explicitly
or implicitly. Resolving which module the type or function comes from can be a bit tricky sometimes, and I recommend against
doing it yourself.
elm-review computes this for you already. Store this value inside your module context, then use
ModuleNameLookupTable.moduleNameFor or
ModuleNameLookupTable.moduleNameAt to get the name of the module the
type or value comes from.
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
type alias Context =
{ lookupTable : ModuleNameLookupTable }
rule : Rule
rule =
Rule.newModuleRuleSchemaUsingContextCreator "NoHtmlButton" contextCreator
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
|> Rule.ignoreErrorsForFiles [ "src/Colors.elm" ]
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\lookupTable () -> { lookupTable = lookupTable })
|> Rule.withModuleNameLookupTable
expressionVisitor : Node Expression -> Context -> ( List (Error {}), Context )
expressionVisitor node context =
case Node.value node of
Expression.FunctionOrValue _ "color" ->
if ModuleNameLookupTable.moduleNameFor context.lookupTable node == Just [ "Css" ] then
( [ Rule.error
{ message = "Do not use `Css.color` directly, use the Colors module instead"
, details = [ "We made a module which contains all the available colors of our design system. Use the functions in there instead." ]
}
(Node.range node)
]
, context
)
else
( [], context )
_ ->
( [], context )Note: If you have been using elm-review-scope before, you should use this instead.
Request the module key for this module.
rule =
Rule.newProjectRuleSchema "NoMissingSubscriptionsCall" initialProjectContext
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = Rule.initContextCreator fromProjectToModule
, fromModuleToProject = Rule.initContextCreator fromModuleToProject |> Rule.withModuleKey
, foldProjectContexts = foldProjectContexts
}Create an Error. Use it when you find a pattern that the rule should forbid.
The message and details represent the message you want to display to the user.
The details is a list of paragraphs, and each item will be visually separated
when shown to the user. The details may not be empty, and this will be enforced
by the tests automatically.
error : Node a -> Error
error node =
Rule.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
}
(Node.range node)The Range corresponds to the location where the error should be shown, i.e. where to put the squiggly lines in an editor.
In most cases, you can get it using Node.range.
Creates an Error, just like the error function, but
provides an automatic fix that the user can apply.
import Review.Fix as Fix
error : Node a -> Error
error node =
Rule.errorWithFix
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
}
(Node.range node)
[ Fix.removeRange (Node.range node) ]Take a look at Review.Fix to know more on how to makes fixes.
If the list of fixes is empty, then it will give the same error as if you had
called error instead.
Note: Each fix applies on a location in the code, defined by a range. To avoid an unpredictable result, those ranges may not overlap. The order of the fixes does not matter.
A key to be able to report an error for a specific module. You need such a
key in order to use the errorForModule function. This is to
prevent creating errors for modules you have not visited, or files that do not exist.
You can get a ModuleKey from the fromProjectToModule and fromModuleToProject
functions that you define when using newProjectRuleSchema.
Just like error, create an Error but for a specific module, instead of the module that is being
visited.
You will need a ModuleKey, which you can get from the fromProjectToModule and fromModuleToProject
functions that you define when using newProjectRuleSchema.
Just like errorForModule, create an Error for a specific module, but
provides an automatic fix that the user can apply.
Take a look at Review.Fix to know more on how to makes fixes.
If the list of fixes is empty, then it will give the same error as if you had
called errorForModule instead.
Note: Each fix applies on a location in the code, defined by a range. To avoid an unpredictable result, those ranges may not overlap. The order of the fixes does not matter.
A key to be able to report an error for the elm.json file. You need this
key in order to use the errorForElmJson function. This is
to prevent creating errors for it if you have not visited it.
You can get a ElmJsonKey using the withElmJsonProjectVisitor function.
Create an Error for the elm.json file.
You will need an ElmJsonKey, which you can get from the withElmJsonProjectVisitor
function.
The second argument is a function that takes the elm.json content as a raw string,
and returns the error details. Using the raw string, you should try and find the
most fitting Range
possible for the error.
A key to be able to report an error for the README.md file. You need this
key in order to use the errorForReadme function. This is
to prevent creating errors for it if you have not visited it.
You can get a ReadmeKey using the withReadmeProjectVisitor function.
Create an Error for the README.md file.
You will need an ReadmeKey, which you can get from the withReadmeProjectVisitor
function.
Just like errorForReadme, create an Error for the README.md file, but
provides an automatic fix that the user can apply.
Take a look at Review.Fix to know more on how to makes fixes.
If the list of fixes is empty, then it will give the same error as if you had
called errorForReadme instead.
Note: Each fix applies on a location in the code, defined by a range. To avoid an unpredictable result, those ranges may not overlap. The order of the fixes does not matter.
Get the name of the rule that triggered this Error.
Get the error message of an Error.
Get the error details of an Error.
Get the file path of an Error.
Get the file path of an Error.
There are situations where you don't want review rules to report errors:
You can use the following functions to ignore errors in directories or files.
NOTE: Even though they can be used to disable any errors, I strongly recommend against doing so if you are not in the situations listed above. I highly recommend you leave a comment explaining the reason why you use these functions, or to communicate with your colleagues if you see them adding exceptions without reason or seemingly inappropriately.
Ignore the errors reported for modules in specific directories of the project.
Use it when you don't want to get review errors for generated source code or for libraries that you forked and copied over to your project.
config : List Rule
config =
[ Some.Rule.rule
|> Rule.ignoreErrorsForDirectories [ "generated-source/", "vendor/" ]
, Some.Other.Rule.rule
]If you want to ignore some directories for all of your rules, you can apply
ignoreErrorsForDirectories like this:
config : List Rule
config =
[ Some.Rule.rule
, Some.Other.Rule.rule
]
|> List.map (Rule.ignoreErrorsForDirectories [ "generated-source/", "vendor/" ])The paths should be relative to the elm.json file, just like the ones for the
elm.json's source-directories.
You can apply ignoreErrorsForDirectoriesseveral times for a rule, to add more
ignored directories.
You can also use it when writing a rule. We can hardcode in the rule that a rule
is not applicable to a folder, like tests/ for instance. The following example
forbids using Debug.todo anywhere in the code, except in tests.
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
|> Rule.ignoreErrorsForDirectories [ "tests/" ]
expressionVisitor : Node Expression -> List (Error {})
expressionVisitor node =
case Node.value node of
Expression.FunctionOrValue [ "Debug" ] "todo" ->
[ Rule.error
{ message = "Remove the use of `Debug.todo` before shipping to production"
, details = [ "`Debug.todo` is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
}
(Node.range node)
]
_ ->
[]Ignore the errors reported for specific file paths. Use it when you don't want to review generated source code or files from external sources that you copied over to your project and don't want to be touched.
config : List Rule
config =
[ Some.Rule.rule
|> Rule.ignoreErrorsForFiles [ "src/Some/File.elm" ]
, Some.Other.Rule.rule
]If you want to ignore some files for all of your rules, you can apply
ignoreErrorsForFiles like this:
config : List Rule
config =
[ Some.Rule.rule
, Some.Other.Rule.rule
]
|> List.map (Rule.ignoreErrorsForFiles [ "src/Some/File.elm" ])The paths should be relative to the elm.json file, just like the ones for the
elm.json's source-directories.
You can apply ignoreErrorsForFiles several times for a rule, to add more
ignored files.
You can also use it when writing a rule. We can simplify the example from withModuleDefinitionVisitor
by hardcoding an exception into the rule (that forbids the use of Html.button except in the "Button" module).
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoHtmlButton"
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
|> Rule.ignoreErrorsForFiles [ "src/Button.elm" ]
expressionVisitor : Node Expression -> List (Error {})
expressionVisitor node context =
case Node.value node of
Expression.FunctionOrValue [ "Html" ] "button" ->
[ Rule.error
{ message = "Do not use `Html.button` directly"
, details = [ "At fruits.com, we've built a nice `Button` module that suits our needs better. Using this module instead of `Html.button` ensures we have a consistent button experience across the website." ]
}
(Node.range node)
]
_ ->
[]DEPRECATED: Use reviewV2 instead.
Review a project and gives back the errors raised by the given rules.
Note that you won't need to use this function when writing a rule. You should
only need it if you try to make elm-review run in a new environment.
import Review.Project as Project exposing (Project, ProjectModule)
import Review.Rule as Rule exposing (Rule)
config : List Rule
config =
[ Some.Rule.rule
, Some.Other.Rule.rule
]
project : Project
project =
Project.new
|> Project.addModule { path = "src/A.elm", source = "module A exposing (a)\na = 1" }
|> Project.addModule { path = "src/B.elm", source = "module B exposing (b)\nb = 1" }
doReview =
let
( errors, rulesWithCachedValues ) =
Rule.review rules project
in
doSomethingWithTheseValuesThe resulting List Rule is the same list of rules given as input, but with an
updated internal cache to make it faster to re-run the rules on the same project.
If you plan on re-reviewing with the same rules and project, for instance to
review the project after a file has changed, you may want to store the rules in
your Model.
The rules are functions, so doing so will make your model unable to be
exported/imported with elm/browser's debugger, and may cause a crash if you try
to compare them or the model that holds them.
Review a project and gives back the errors raised by the given rules.
Note that you won't need to use this function when writing a rule. You should
only need it if you try to make elm-review run in a new environment.
import Review.Project as Project exposing (Project, ProjectModule)
import Review.Rule as Rule exposing (Rule)
config : List Rule
config =
[ Some.Rule.rule
, Some.Other.Rule.rule
]
project : Project
project =
Project.new
|> Project.addModule { path = "src/A.elm", source = "module A exposing (a)\na = 1" }
|> Project.addModule { path = "src/B.elm", source = "module B exposing (b)\nb = 1" }
doReview =
let
{ errors, rules, projectData } =
-- Replace `config` by `rules` next time you call reviewV2
-- Replace `Nothing` by `projectData` next time you call reviewV2
Rule.reviewV2 config Nothing project
in
doSomethingWithTheseValuesThe resulting List Rule is the same list of rules given as input, but with an
updated internal cache to make it faster to re-run the rules on the same project.
If you plan on re-reviewing with the same rules and project, for instance to
review the project after a file has changed, you may want to store the rules in
your Model.
The rules are functions, so doing so will make your model unable to be
exported/imported with elm/browser's debugger, and may cause a crash if you try
to compare them or the model that holds them.
Internal cache about the project.
Get the name of a rule.
You should not have to use this when writing a rule.
Used for phantom type constraints. You can safely ignore this type.
Used for phantom type constraints. You can safely ignore this type.
Represents a construct able to analyze a project and report unwanted patterns.
You can create module rules or project rules.