This module contains functions that are used for writing rules.
NOTE: If you want to create a package containing elm-review
rules, I highly recommend using the
CLI's elm-review new-package
subcommand. This will create a new package that will help you use the best practices and give you helpful tools like easy auto-publishing. More information is available in the maintenance file generated along with it.
If you want to add/create a rule for the package or for your local configuration, then I recommend using elm-review new-rule
, which will create a source and test file which you can use as a starting point. For packages, it will add the rule everywhere it should be present (exposed-modules
, README, ...).
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-syntax
elm-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.
Represents a construct able to analyze a project and report unwanted patterns.
You can create module rules or project rules.
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 withElmJsonModuleVisitor
README.md
file, visited by withReadmeModuleVisitor
withDirectDependenciesModuleVisitor
and withDependenciesModuleVisitor
withSimpleModuleDefinitionVisitor
and withModuleDefinitionVisitor
withModuleDocumentationVisitor
withSimpleCommentsVisitor
and withCommentsVisitor
withSimpleImportVisitor
and withImportVisitor
withDeclarationListVisitor
withSimpleDeclarationVisitor
and withDeclarationEnterVisitor
withSimpleExpressionVisitor
, withExpressionEnterVisitor
and withExpressionExitVisitor
.withDeclarationExitVisitor
withFinalModuleEvaluation
Evaluating/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.fromModuleRuleSchema
newModuleRuleSchema : String -> moduleContext -> ModuleRuleSchema { canCollectProjectData : () } moduleContext
Creates 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".
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.fromModuleRuleSchema
If 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 = [] }
fromModuleRuleSchema : ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext -> Rule
Create a Rule
from a configured ModuleRuleSchema
.
withSimpleModuleDefinitionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Module.Module -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUnderscoreInModuleName" ()
|> Rule.withSimpleModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.fromModuleRuleSchema
moduleDefinitionVisitor : Node Module -> List (Rule.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.
withSimpleCommentsVisitor : (List (Elm.Syntax.Node.Node String) -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will visit the module's comments.
This visitor will give you access to the list of comments (in source order) in
the module all at once. Note that comments that are parsed as documentation comments by
elm-syntax
are not included in this list.
As such, the following comments are included (✅) / excluded (❌):
{-| -}
){-| -}
){-| -}
)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 (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoTodoComment" ()
|> Rule.withSimpleCommentsVisitor commentsVisitor
|> Rule.fromModuleRuleSchema
commentsVisitor : List (Node String) -> List (Rule.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`.
range
Note: 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.
withSimpleImportVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Import.Import -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoCoreHtml" ()
|> Rule.withSimpleImportVisitor importVisitor
|> Rule.fromModuleRuleSchema
importVisitor : Node Import -> List (Rule.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.
withSimpleDeclarationVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoMissingTypeAnnotation" ()
|> Rule.withSimpleDeclarationVisitor declarationVisitor
|> Rule.fromModuleRuleSchema
declarationVisitor : Node Declaration -> List (Rule.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.
withSimpleExpressionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebug" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
expressionVisitor : Node Expression -> List (Rule.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.
newModuleRuleSchemaUsingContextCreator : String -> ContextCreator () moduleContext -> ModuleRuleSchema {} moduleContext
Same as newModuleRuleSchema
, except that you can request for data to help initialize the context.
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.fromModuleRuleSchema
If 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
(\isInSourceDirectories () ->
{ hasTodoBeenImported = False
, hasToStringBeenImported = False
, isInSourceDirectories = isInSourceDirectories
}
)
|> Rule.withIsInSourceDirectories
withModuleDefinitionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Module.Module -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
Add 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 (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 (Rule.Error {}), Context )
moduleDefinitionVisitor node context =
if (Node.value node |> Module.moduleName) == [ "Button" ] then
( [], HtmlButtonIsAllowed )
else
( [], HtmlButtonIsForbidden )
expressionVisitor : Node Expression -> Context -> ( List (Rule.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
.
withModuleDocumentationVisitor : (Maybe (Elm.Syntax.Node.Node String) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will visit the module's documentation, collect data in
the context
and/or report patterns.
This visitor will give you access to the module documentation comment. Modules don't always have a documentation.
When that is the case, the visitor will be called with the Nothing
as the module documentation.
withCommentsVisitor : (List (Elm.Syntax.Node.Node String) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 comments (in source order) in
the module all at once. Note that comments that are parsed as documentation comments by
elm-syntax
are not included in this list.
As such, the following comments are included (✅) / excluded (❌):
{-| -}
){-| -}
){-| -}
)Tip: If you do not need to collect data in this visitor, you may wish to use the
simpler withSimpleCommentsVisitor
function.
Tip: If you only need to access the module documentation, you should use
withModuleDocumentationVisitor
instead.
withImportVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Import.Import -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (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 (Rule.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 (OnEnter
ing the node), or after (OnExit
ing 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 (Rule.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 )
withDeclarationEnterVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (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 (Rule.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 (Rule.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 exposedList
Tip: If you do not need to collect or use the context
in this visitor, you may wish to use the
simpler withSimpleDeclarationVisitor
function.
withDeclarationExitVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (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 -> Context -> ( List (Rule.Error {}), Context )
declarationEnterVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration function ->
( [], registerArguments context function )
_ ->
( [], context )
declarationExitVisitor : Node Declaration -> Context -> ( List (Rule.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 )
withDeclarationVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> Direction -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
@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 (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 (Rule.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 -> Rule.Direction -> ExposedFunctions -> ( List (Rule.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 exposedList
Tip: If you do not need to collect or use the context
in this visitor, you may wish to use the
simpler withSimpleDeclarationVisitor
function.
withDeclarationListVisitor : (List (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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
),
to collect data and/or report patterns. The declarations will be in the same
order that they appear in the source code.
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.
withExpressionEnterVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (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 (Rule.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 (Rule.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.
withExpressionExitVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
|> Rule.withExpressionEnterVisitor expressionEnterVisitor
|> Rule.withExpressionExitVisitor expressionExitVisitor
|> Rule.fromModuleRuleSchema
expressionEnterVisitor : Node Expression -> Context -> ( List (Rule.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 (Rule.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 )
withExpressionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> Direction -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
@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 (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 (Rule.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 -> Rule.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.
withCaseBranchEnterVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.CaseBlock -> ( Elm.Syntax.Node.Node Elm.Syntax.Pattern.Pattern, Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression ) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will visit the module's
case branches when entering the branch.
The visitor can be very useful if you need to change the context when inside a case branch.
The visitors would be called in the following order (ignore the expression visitor if you don't have one):
x =
case evaluated of
Pattern1 ->
expression1
Pattern2 ->
expression2
evaluated
( Pattern1, expression1 )
expression1
( Pattern1, expression1 )
( Pattern2, expression2 )
expression2
( Pattern2, expression2 )
You can use withCaseBranchExitVisitor
to visit the node on exit.
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Pattern exposing (Pattern)
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUnusedCaseVariables" ( [], [] )
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.withCaseBranchEnterVisitor caseBranchEnterVisitor
|> Rule.withCaseBranchExitVisitor caseBranchExitVisitor
|> Rule.fromModuleRuleSchema
type alias Context =
( List String, List (List String) )
expressionVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionVisitor node (( scope, parentScopes ) as context) =
case context of
Expression.FunctionOrValue [] name ->
( [], ( name :: used, parentScopes ) )
_ ->
( [], context )
caseBranchEnterVisitor : Node Expression.LetBlock -> ( Node Pattern, Node Expression ) -> Context -> List ( Rule.Error {}, Context )
caseBranchEnterVisitor _ _ ( scope, parentScopes ) =
-- Entering a new scope every time we enter a new branch
( [], ( [], scope :: parentScopes ) )
caseBranchExitVisitor : Node Expression.LetBlock -> ( Node Pattern, Node Expression ) -> Context -> List ( Rule.Error {}, Context )
caseBranchExitVisitor _ ( pattern, _ ) ( scope, parentScopes ) =
-- Exiting the current scope every time we enter a new branch, and reporting the patterns that weren't used
let
namesFromPattern =
findNamesFromPattern pattern
( unusedPatterns, unmatchedUsed ) =
findUnused namesFromPattern scope
newScopes =
case parentScopes of
head :: tail ->
( unmatchedUsed ++ head, tail )
[] ->
( unmatched, [] )
in
( List.map errorForUnused unusedPatterns, newScopes )
For convenience, the entire case expression is passed as the first argument.
withCaseBranchExitVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.CaseBlock -> ( Elm.Syntax.Node.Node Elm.Syntax.Pattern.Pattern, Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression ) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will visit the module's
case branches when exiting the branch.
See the documentation for withCaseBranchEnterVisitor
for explanations and an example.
withLetDeclarationEnterVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.LetBlock -> Elm.Syntax.Node.Node Elm.Syntax.Expression.LetDeclaration -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will visit the module's
let declarations branches when entering the declaration.
The visitor can be very useful if you need to change the context when inside a let declaration.
The visitors would be called in the following order (ignore the expression visitor if you don't have one):
x =
let
declaration1 =
expression1
declaration2 =
expression2
in
letInValue
( declaration1, expression1 )
expression1
( declaration1, expression1 )
( declaration2, expression2 )
expression2
( declaration2, expression2 )
letInValue
You can use withLetDeclarationExitVisitor
to visit the node on exit.
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoUnusedLetFunctionParameters" ( [], [] )
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.withLetDeclarationEnterVisitor letDeclarationEnterVisitor
|> Rule.withLetDeclarationExitVisitor letDeclarationExitVisitor
|> Rule.fromModuleRuleSchema
type alias Context =
( List String, List (List String) )
expressionVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionVisitor node (( scope, parentScopes ) as context) =
case context of
Expression.FunctionOrValue [] name ->
( [], ( name :: used, parentScopes ) )
_ ->
( [], context )
letDeclarationEnterVisitor : Node Expression.LetBlock -> Node Expression.LetDeclaration -> Context -> List ( Rule.Error {}, Context )
letDeclarationEnterVisitor _ letDeclaration (( scope, parentScopes ) as context) =
case Node.value letDeclaration of
Expression.LetFunction _ ->
( [], ( [], scope :: parentScopes ) )
Expression.LetDestructuring _ ->
( [], context )
letDeclarationExitVisitor : Node Expression.LetBlock -> Node Expression.LetDeclaration -> Context -> List ( Rule.Error {}, Context )
letDeclarationExitVisitor _ letDeclaration (( scope, parentScopes ) as context) =
case Node.value letDeclaration of
Expression.LetFunction _ ->
let
namesFromPattern =
findNamesFromArguments letFunction
( unusedArguments, unmatchedUsed ) =
findUnused namesFromPattern scope
newScopes =
case parentScopes of
head :: tail ->
( unmatchedUsed ++ head, tail )
[] ->
( unmatched, [] )
in
( List.map errorForUnused unusedArguments, newScopes )
Expression.LetDestructuring _ ->
( [], context )
For convenience, the entire let expression is passed as the first argument.
withLetDeclarationExitVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.LetBlock -> Elm.Syntax.Node.Node Elm.Syntax.Expression.LetDeclaration -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will visit the module's
let declarations branches when entering the declaration.
See the documentation for withLetDeclarationEnterVisitor
for explanations and an example.
providesFixesForModuleRule : ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema schemaState moduleContext
Let elm-review
know that this rule may provide fixes in the reported errors.
This information is hard for elm-review
to deduce on its own, but can be very useful for improving the performance of
the tool while running in fix mode.
If your rule is a project rule, then you should use providesFixesForProjectRule
instead.
withFinalModuleEvaluation : (moduleContext -> List (Error {})) -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext
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 (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 (Rule.Error {}), Context )
importVisitor node context =
( [], Dict.insert (Node.value node |> .moduleName |> Node.value) (Node.range node) context )
finalEvaluation : Context -> List (Rule.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
]
_ ->
[]
withElmJsonModuleVisitor : (Maybe Elm.Project.Project -> moduleContext -> moduleContext) -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext
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 (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 (Rule.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 )
withReadmeModuleVisitor : (Maybe String -> moduleContext -> moduleContext) -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will visit
the project's README.md
file.
withDirectDependenciesModuleVisitor : (Dict String Review.Project.Dependency.Dependency -> moduleContext -> moduleContext) -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will examine the project's
direct dependencies.
You can use this look at the modules contained in dependencies, which can make the rule very precise when it targets specific functions.
withDependenciesModuleVisitor : (Dict String Review.Project.Dependency.Dependency -> moduleContext -> moduleContext) -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext
Add a visitor to the ModuleRuleSchema
which will examine 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.
newProjectRuleSchema : String -> projectContext -> ProjectRuleSchema { canAddModuleVisitor : (), withModuleContext : Forbidden } projectContext moduleContext
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 project context, as they will be compared internally, which may cause Elm to crash.
Project rules traverse the project in the following order:
elm.json
file, visited by withElmJsonProjectVisitor
README.md
file, visited by withReadmeProjectVisitor
withDependenciesProjectVisitor
withModuleVisitor
,
following the same traversal order as for module rules but without reading the project files (elm.json
, ...).withFinalProjectEvaluation
Evaluating/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.fromProjectRuleSchema : ProjectRuleSchema { schemaState | withModuleContext : Forbidden, hasAtLeastOneVisitor : () } projectContext moduleContext -> Rule
Create a Rule
from a configured ProjectRuleSchema
.
withModuleVisitor : (ModuleRuleSchema {} moduleContext -> ModuleRuleSchema { moduleSchemaState | hasAtLeastOneVisitor : () } moduleContext) -> ProjectRuleSchema { projectSchemaState | canAddModuleVisitor : () } projectContext moduleContext -> ProjectRuleSchema { projectSchemaState | canAddModuleVisitor : (), withModuleContext : Required } projectContext moduleContext
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.
withModuleContext : { fromProjectToModule : ModuleKey -> Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName -> projectContext -> moduleContext, fromModuleToProject : ModuleKey -> Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName -> moduleContext -> projectContext, foldProjectContexts : projectContext -> projectContext -> projectContext } -> ProjectRuleSchema { schemaState | canAddModuleVisitor : (), withModuleContext : Required } projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : (), withModuleContext : Forbidden } projectContext moduleContext
Specify, if the project rule has a module visitor, how to:
fromProjectToModule
]fromModuleToProject
]foldProjectContexts
]NOTE: 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 projectContext
s 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.
fromProjectToModule
The 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
.
fromModuleToProject
When 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
.
foldProjectContexts
This function folds two projectContext
into one. This function requires a few
traits to always be true.
projectContext
s 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 Set
s or Dict
s), 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 newContext.modules previousContext.modules
-- Collect the used functions from the new context and the previous one
, used = Set.union newContext.used previousContext.used
}
finalEvaluationForProject : ProjectContext -> List (Rule.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
)
withModuleContextUsingContextCreator : { fromProjectToModule : ContextCreator projectContext moduleContext, fromModuleToProject : ContextCreator moduleContext projectContext, foldProjectContexts : projectContext -> projectContext -> projectContext } -> ProjectRuleSchema { schemaState | canAddModuleVisitor : (), withModuleContext : Required } projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : (), withModuleContext : Forbidden } projectContext moduleContext
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 = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
|> Rule.fromProjectRuleSchema
fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
fromProjectToModule =
Rule.initContextCreator
(\projectContext ->
{ -- something
}
)
fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
fromModuleToProject =
Rule.initContextCreator
(\moduleKey moduleName moduleContext ->
{ moduleKeys = Dict.singleton moduleName moduleKey
}
)
|> Rule.withModuleKey
|> Rule.withModuleName
withElmJsonProjectVisitor : (Maybe { elmJsonKey : ElmJsonKey, project : Elm.Project.Project } -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext )) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext
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.
withReadmeProjectVisitor : (Maybe { readmeKey : ReadmeKey, content : String } -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext )) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext
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.
withDirectDependenciesProjectVisitor : (Dict String Review.Project.Dependency.Dependency -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext )) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext
Add a visitor to the ProjectRuleSchema
which will examine the project's
direct dependencies.
It works exactly like withDependenciesModuleVisitor
. The visitor will be called before any
module is evaluated.
withDependenciesProjectVisitor : (Dict String Review.Project.Dependency.Dependency -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext )) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext
Add a visitor to the ProjectRuleSchema
which will examine the project's
dependencies.
It works exactly like withDependenciesModuleVisitor
. The visitor will be called before any
module is evaluated.
withFinalProjectEvaluation : (projectContext -> List (Error { useErrorForModule : () })) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext
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.
withContextFromImportedModules : ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext
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 projectContext
s 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.
providesFixesForProjectRule : ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext
Let elm-review
know that this rule may provide fixes in the reported errors.
This information is hard for elm-review
to deduce on its own, but can be very useful for improving the performance of
the tool while running in fix mode.
If your rule is a module rule, then you should use providesFixesForModuleRule
instead.
Create a module context from a project context or the other way around.
Use functions like withModuleName
to request more information.
initContextCreator : (from -> to) -> ContextCreator from to
Initialize a new context creator.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\moduleName () ->
{ moduleName = moduleName
-- ...other fields
}
)
|> Rule.withModuleName
withModuleName : ContextCreator Elm.Syntax.ModuleName.ModuleName (from -> to) -> ContextCreator from to
Request the name of the module.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\moduleName () ->
{ moduleName = moduleName
-- ...other fields
}
)
|> Rule.withModuleName
withModuleNameNode : ContextCreator (Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName) (from -> to) -> ContextCreator from to
Request the node corresponding to the name of the module.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\moduleNameNode () ->
{ moduleNameNode = moduleNameNode
-- ...other fields
}
)
|> Rule.withModuleNameNode
withIsInSourceDirectories : ContextCreator Basics.Bool (from -> to) -> ContextCreator from to
Request to know 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.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\isInSourceDirectories () ->
{ isInSourceDirectories = isInSourceDirectories
-- ...other fields
}
)
|> Rule.withIsInSourceDirectories
withFilePath : ContextCreator String (from -> to) -> ContextCreator from to
Request the file path for this module, relative to the project's elm.json
.
Using newModuleRuleSchemaUsingContextCreator
:
rule : Rule
rule =
Rule.newModuleRuleSchemaUsingContextCreator "YourRuleName" initialContext
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
initialContext : Rule.ContextCreator () Context
initialContext =
Rule.initContextCreator
(\filePath () -> { filePath = filePath })
|> Rule.withFilePath
Using withModuleContextUsingContextCreator
in a project rule:
rule : Rule
rule =
Rule.newProjectRuleSchema "YourRuleName" initialProjectContext
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
fromModuleToProject : Rule.ContextCreator () Context
fromModuleToProject =
Rule.initContextCreator
(\filePath () -> { filePath = filePath })
|> Rule.withFilePath
withIsFileIgnored : ContextCreator Basics.Bool (from -> to) -> ContextCreator from to
Request to know whether the errors for the current module has been ignored for this particular rule. This may be useful to reduce the amount of work related to ignored files — like collecting unnecessary data or reporting errors — when that will ignored anyway.
Note that for module rules, ignored files will be skipped automatically anyway.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\isFileIgnored () ->
{ isFileIgnored = isFileIgnored
-- ...other fields
}
)
|> Rule.withIsFileIgnored
withModuleNameLookupTable : ContextCreator Review.ModuleNameLookupTable.ModuleNameLookupTable (from -> to) -> ContextCreator from to
Requests 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" initialContext
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
|> Rule.ignoreErrorsForFiles [ "src/Colors.elm" ]
initialContext : Rule.ContextCreator () Context
initialContext =
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.
withModuleKey : ContextCreator ModuleKey (from -> to) -> ContextCreator from to
Request the module key for this module.
rule : Rule
rule =
Rule.newProjectRuleSchema "NoMissingSubscriptionsCall" initialProjectContext
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContextUsingContextCreator
{ fromProjectToModule = fromProjectToModule
, fromModuleToProject = fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
fromModuleToProject : Rule.ContextCreator () Context
fromModuleToProject =
Rule.initContextCreator
(\moduleKey () -> { moduleKey = moduleKey })
|> Rule.withModuleKey
withSourceCodeExtractor : ContextCreator (Elm.Syntax.Range.Range -> String) (from -> to) -> ContextCreator from to
Requests access to a function that gives you the source code at a given range.
rule : Rule
rule =
Rule.newModuleRuleSchemaUsingContextCreator "YourRuleName" initialContext
|> Rule.withExpressionEnterVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
type alias Context =
{ extractSourceCode : Range -> String
}
initialContext : Rule.ContextCreator () Context
initialContext =
Rule.initContextCreator
(\extractSourceCode () -> { extractSourceCode = extractSourceCode })
|> Rule.withSourceCodeExtractor
The motivation for this capability was for allowing to provide higher-quality fixes, especially where you'd need to move or copy code from one place to another (example: when switching the branches of an if expression).
I discourage using this functionality to explore the source code, as the different visitor functions make for a nicer experience.
withFullAst : ContextCreator Elm.Syntax.File.File (from -> to) -> ContextCreator from to
Request the full AST for the current module.
This can be useful if you wish to avoid initializing the module context with dummy data future node visits can replace them.
For instance, if you wish to know what is exposed from a module, you may need to visit the module definition and then the list of declarations. If you need this information earlier on, you will have to provide dummy data at context initialization and store some intermediary data.
Using the full AST, you can simplify the implementation by computing the data in the context creator, without the use of visitors.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\ast () ->
{ exposed = collectExposed ast.moduleDefinition ast.declarations
-- ...other fields
}
)
|> Rule.withFullAst
withModuleDocumentation : ContextCreator (Maybe (Elm.Syntax.Node.Node String)) (from -> to) -> ContextCreator from to
Request the module documentation. Modules don't always have a documentation.
When that is the case, the module documentation will be Nothing
.
contextCreator : Rule.ContextCreator () Context
contextCreator =
Rule.initContextCreator
(\moduleDocumentation () ->
{ moduleDocumentation = moduleDocumentation
-- ...other fields
}
)
|> Rule.withModuleDocumentation
Metadata for the module being visited.
@deprecated: More practical functions have been made available since the introduction of this type.
Do not store the metadata directly in your context. Prefer storing the individual pieces of information.
withMetadata : ContextCreator Metadata (from -> to) -> ContextCreator from to
Request metadata about the module.
@deprecated: Use more practical functions like
moduleNameFromMetadata : Metadata -> Elm.Syntax.ModuleName.ModuleName
Get the module name of the current module.
@deprecated: Use the more practical withModuleName
instead.
moduleNameNodeFromMetadata : Metadata -> Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName
Get the Node
to the module name of the current module.
@deprecated: Use the more practical withModuleNameNode
instead.
isInSourceDirectories : Metadata -> Basics.Bool
Learn 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.
@deprecated: Use the more practical withIsInSourceDirectories
instead.
Represents an error found by a Rule
. These are created by the rules.
error : { message : String, details : List String } -> Elm.Syntax.Range.Range -> Error {}
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
].
errorWithFix : { message : String, details : List String } -> Elm.Syntax.Range.Range -> List Review.Fix.Fix -> Error {}
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
.
errorForModule : ModuleKey -> { message : String, details : List String } -> Elm.Syntax.Range.Range -> Error scope
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
.
errorForModuleWithFix : ModuleKey -> { message : String, details : List String } -> Elm.Syntax.Range.Range -> List Review.Fix.Fix -> Error scope
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.
errorForElmJson : ElmJsonKey -> (String -> { message : String, details : List String, range : Elm.Syntax.Range.Range }) -> Error scope
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.
errorForElmJsonWithFix : ElmJsonKey -> (String -> { message : String, details : List String, range : Elm.Syntax.Range.Range }) -> (Elm.Project.Project -> Maybe Elm.Project.Project) -> Error scope
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.
The third argument is a function that takes the elm.json
and returns a different one that will be suggested as a fix. If the function returns Nothing
, no fix will be applied.
The elm.json
will be the same as the one you got from withElmJsonProjectVisitor
, use either depending on what you find most practical.
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.
errorForReadme : ReadmeKey -> { message : String, details : List String } -> Elm.Syntax.Range.Range -> Error scope
Create an Error
for the README.md
file.
You will need an ReadmeKey
, which you can get from the withReadmeProjectVisitor
function.
errorForReadmeWithFix : ReadmeKey -> { message : String, details : List String } -> Elm.Syntax.Range.Range -> List Review.Fix.Fix -> Error scope
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.
globalError : { message : String, details : List String } -> Error scope
Create an Error
that is not attached to any specific location in the project.
This can be useful when needing to report problems that are not tied to any file. For instance for reporting missing elements like a module that was expected to be there.
This is however NOT the recommended way when it is possible to attach an error to a location (even if it is simply the module name of a file's module declaration), because giving hints to where the problem is makes it easier for the user to solve it.
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 : String -> Error scope
error moduleName =
Rule.globalError
{ message = "Could not find module " ++ moduleName
, details =
[ "You mentioned the module " ++ moduleName ++ " in the configuration of this rule, but it could not be found."
, "This likely means you misconfigured the rule or the configuration has become out of date with recent changes in your project."
]
}
configurationError : String -> { message : String, details : List String } -> Rule
Creates a rule that will only report a configuration error, which stops elm-review
from reviewing the project
until the user has addressed the issue.
When writing rules, some of them may take configuration arguments that specify what exactly the rule should do. I recommend to define custom types to limit the possibilities of what can be considered valid and invalid configuration, so that the user gets information from the compiler when the configuration is unexpected.
Unfortunately it is not always possible or practical to let the type system forbid invalid possibilities, and you may need to manually parse or validate the arguments.
rule : SomeCustomConfiguration -> Rule
rule config =
case parseFunctionName config.functionName of
Nothing ->
Rule.configurationError "RuleName"
{ message = config.functionName ++ " is not a valid function name"
, details =
[ "I was expecting functionName to be a valid Elm function name."
, "When that is not the case, I am not able to function as expected."
]
}
Just functionName ->
Rule.newModuleRuleSchema "RuleName" ()
|> Rule.withExpressionEnterVisitor (expressionVisitor functionName)
|> Rule.fromModuleRuleSchema
When you need to look at the project before determining whether something is actually a configuration error, for instance
when reporting that a targeted function does not fit some criteria (unexpected arguments, ...), you should go for more
usual errors like error
or potentially globalError
. error
would be better because
it will give the user a starting place to fix the issue.
Be careful that the rule name is the same for the rule and for the configuration error.
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.
Review.Error.ReviewError
Represents an error found by a Rule
. These are the ones that will
be reported to the user.
If you are building a Rule
, you shouldn't have to use this.
errorRuleName : ReviewError -> String
Get the name of the rule that triggered this Error
.
errorMessage : ReviewError -> String
Get the error message of an Error
.
errorDetails : ReviewError -> List String
Get the error details of an Error
.
errorRange : ReviewError -> Elm.Syntax.Range.Range
errorFilePath : ReviewError -> String
Get the file path of an Error
.
errorTarget : ReviewError -> Review.Error.Target
Get the target of an Error
.
errorFixes : ReviewError -> Maybe (List Review.Fix.Fix)
Get the automatic fixes
of an Error
, if it
defined any.
errorFixFailure : ReviewError -> Maybe Review.Fix.Problem
Get the reason why the fix for an error failed when its available automatic fix was attempted and deemed incorrect.
Note that if the review process was not run in fix mode previously, then this will return Nothing
.
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, or only report errors found in specific 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.
ignoreErrorsForDirectories : List String -> Rule -> Rule
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 ignoreErrorsForDirectories
several 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 (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
|> Rule.ignoreErrorsForDirectories [ "tests/" ]
expressionVisitor : Node Expression -> List (Rule.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)
]
_ ->
[]
ignoreErrorsForFiles : List String -> Rule -> Rule
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 (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoHtmlButton"
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
|> Rule.ignoreErrorsForFiles [ "src/Button.elm" ]
expressionVisitor : Node Expression -> List (Rule.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)
]
_ ->
[]
filterErrorsForFiles : (String -> Basics.Bool) -> Rule -> Rule
Filter the files to report errors for.
Use it to control precisely which files the rule applies or does not apply to. For example, you might have written a rule that should only be applied to one specific file.
config : List Rule
config =
[ Some.Rule.rule
|> Rule.filterErrorsForFiles (\path -> path == "src/Some/File.elm")
, Some.Other.Rule.rule
]
If you want to specify a condition for all of your rules, you can apply
filterErrorsForFiles
like this:
config : List Rule
config =
[ Some.Rule.For.Tests.rule
, Some.Other.Rule.rule
]
|> List.map (Rule.filterErrorsForFiles (String.startsWith "tests/"))
The received paths will be relative to the elm.json
file, just like the ones for the
elm.json
's source-directories
, and will be formatted in the Unix style src/Some/File.elm
.
You can apply filterErrorsForFiles
several times for a rule, the conditions will get
compounded, following the behavior of List.filter
.
When ignoreErrorsForFiles
or ignoreErrorsForDirectories
are used in combination with this function, all constraints are observed.
You can also use it when writing a rule. We can hardcode in the rule that a rule
is only applicable to a folder, like src/Api/
for instance. The following example
forbids using strings with hardcoded URLs, but only in the src/Api/
folder.
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoHardcodedURLs" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
|> Rule.filterErrorsForFiles (String.startsWith "src/Api/")
expressionVisitor : Node Expression -> List (Rule.Error {})
expressionVisitor node =
case Node.value node of
Expression.Literal string ->
if isUrl string then
[ Rule.error
{ message = "Do not use hardcoded URLs in the API modules"
, details = [ "Hardcoded URLs should never make it to production. Please refer to the documentation of the `Endpoint` module." ]
}
(Node.range node)
]
else
[]
_ ->
[]
As you might have seen so far, elm-review
has quite a nice way of traversing the files of a project and collecting data.
While you have only seen the tool be used to report errors, you can also use it to extract information from the codebase. You can use this to gain insight into your codebase, or provide information to other tools to enable powerful integrations.
You can read more about how to use this in Extract information in the README, and you can find the tools to extract data below.
withDataExtractor : (projectContext -> Json.Encode.Value) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext
Extract arbitrary data from the codebase, which can be accessed by running
elm-review --report=json --extract
and by reading the value at <output>.extracts["YourRuleName"]
in the output.
import Json.Encode
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newProjectRuleSchema "Some.Rule.Name" initialContext
-- visitors to collect information...
|> Rule.withDataExtractor dataExtractor
|> Rule.fromProjectRuleSchema
dataExtractor : ProjectContext -> Json.Encode.Value
dataExtractor projectContext =
Json.Encode.list
(\thing ->
Json.Encode.object
[ ( "name", Json.Encode.string thing.name )
, ( "value", Json.Encode.int thing.value )
]
)
projectContext.things
preventExtract : Error a -> Error a
Make this error prevent extracting data using withDataExtractor
.
Use this if the rule extracts data and an issue is discovered that would make the extraction output incorrect data.
Rule.error
{ message = "..."
, details = [ "..." ]
}
(Node.range node)
|> Rule.preventExtract
reviewV3 : Review.Options.ReviewOptions -> List Rule -> Review.Project.Internal.Project -> { errors : List ReviewError, fixedErrors : Dict String (List ReviewError), rules : List Rule, project : Review.Project.Internal.Project, extracts : Dict String Json.Encode.Value }
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)
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, extracts } =
-- Replace `config` by `rules` next time you call reviewV2
-- Replace `Nothing` by `projectData` next time you call reviewV2
Rule.reviewV3 config Nothing project
in
doSomethingWithTheseValues
The 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.
reviewV2 : List Rule -> Maybe ProjectData -> Review.Project.Internal.Project -> { errors : List ReviewError, rules : List Rule, projectData : Maybe ProjectData }
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)
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
doSomethingWithTheseValues
The 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 : List Rule -> Review.Project.Internal.Project -> ( List ReviewError, List Rule )
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)
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
doSomethingWithTheseValues
The 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.
ruleName : Rule -> String
Get the name of a rule.
You should not have to use this when writing a rule.
ruleProvidesFixes : Rule -> Basics.Bool
Indicates whether the rule provides fixes.
You should not have to use this when writing a rule.
ruleKnowsAboutIgnoredFiles : Rule -> Basics.Bool
Indicates whether the rule knows about which files are ignored.
You should not have to use this when writing a rule.
withRuleId : Basics.Int -> Rule -> Rule
Assign an id to a rule. This id should be unique.
config =
[ rule1, rule2, rule3 ]
|> List.indexedMap Rule.withUniqueId
You should not have to use this when writing a rule.
getConfigurationError : Rule -> Maybe { message : String, details : List String }
Get the configuration error for a rule.
You should not have to use this when writing a rule. You might be looking for configurationError
instead.
Used for phantom type constraints. You can safely ignore this type.
Used for phantom type constraints. You can safely ignore this type.