This module contains functions that are used for writing rules.
elm-review turns the code of the analyzed file into an Abstract Syntax Tree (AST)
(a tree-like structure which represents your source code) using the
elm-syntax package.
Then, for each file and rule, it will give the details of your project (like the elm.json file) and the
contents of the file to analyze to the rule. The order in which things get passed to the rule is the following:
elm.json file, visited by withElmJsonVisitorwithSimpleModuleDefinitionVisitor and withModuleDefinitionVisitorwithSimpleImportVisitor and withImportVisitorwithDeclarationListVisitor.withSimpleDeclarationVisitor and withDeclarationVisitor.
Before evaluating the next declaration, the expression contained in the declaration
will be visited recursively using by withSimpleExpressionVisitor and withExpressionVisitorwithFinalEvaluationEvaluating a node means two things:
context to have more information available in a later
node evaluation. This is only available using "non-simple with*" visitors.
I recommend using the "simple with*" visitors if you don't need to collect
data, as they are simpler to useelm-review relies on the elm-syntaxpackage,
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 the documentation for each visitor function, and you can also look at the source code for existing rules to better grasp how rules work.
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 file containing the rule be the same as the rule name. This will make it easier to find the module in the project or on the packages website when trying to get more information.
The error message should give more information about the problem. It is split into two parts:
message: A short sentence that describes the forbidden pattern. A user
that has encountered this error multiple times should know exactly what to do.
Example: "Function foo is never used". With this information, a user who
knows the rule probably knows that a function needs to be removed from the
source code, and also knows which one.details: All the additional information that can be useful to the
user, such as the rationale behind forbidding the pattern, and suggestions
for a solution or alternative.When writing the error message that the user will see, try to make them be as helpful as the messages the compiler gives you when it encounters a problem.
When creating an error, you need to specify under which section of the code this message appears. This is where you would see squiggly lines in your editor when you have review or compiler errors.
To make the error easier to spot, it is best to make this section as small as
possible, as long as that makes sense. For instance, in a rule that would forbid
Debug.log, you would the error to appear under Debug.log, not on the whole
function which contains this piece of code.
The rule documentation should give the same information as what you would see in the error message.
If published in a package, the rule documentation should explain when not to enable the rule in the user's review configuration. For instance, for a rule that makes sure that a package is publishable by ensuring that all docs are valid, the rule might say something along the lines of "If you are writing an application, then you should not use this rule.".
Additionally, it could give a few examples of patterns that will be reported and of patterns that will not be reported, so that users can have a better grasp of what to expect.
This package comes with Review.Test, which works with elm-test.
I recommend reading through the strategies for effective testing before
starting writing a rule.
elm-syntaxelm-review is heavily dependent on the types that elm-syntax
provides. If you don't understand the AST it provides, you will have a hard time
implementing the rule you wish to create.
NOTE: There are a lot of rule examples in the documentation of the functions below. They are only here to showcase how to write rules and how a function can be used. The rule examples 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.
Creates a new schema for a rule. Will require calling fromSchema
to create a usable Rule. Use "with*" functions from this module, like
withSimpleExpressionVisitor or withSimpleImportVisitor
to make it report something.
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newSchema "NoDebug"
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.withSimpleImportVisitor importVisitor
|> Rule.fromSchemaIf you wish to build a Rule that collects data as the file gets traversed,
take a look at withInitialContext and "with*" functions without
"Simple" in their name, like withExpressionVisitor,
withImportVisitor or withFinalEvaluation.
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newSchema "NoUnusedVariables"
|> Rule.withInitialContext { declaredVariables = [], usedVariables = [] }
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.withImportVisitor importVisitor
|> Rule.fromSchemaAdd a visitor to the Schema which will visit the File's module definition (module SomeModuleName exposing (a, b)) and report patterns.
The following example forbids having _ in any part of a module name.
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newSchema "NoUnderscoreInModuleName"
|> Rule.withSimpleModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.fromSchema
moduleDefinitionVisitor : Node Module -> List Error
moduleDefinitionVisitor node =
if List.any (String.contains "") (Node.value node |> Module.moduleName) then
[ Rule.error
{ message = "Do not use `_` in a module name"
, details = [ "By convention, Elm modules names use Pascal case (like `MyModuleName`). Please rename your module using this format." ]
}
(Node.range node)
]
else
[]Note: withSimpleModuleDefinitionVisitor is a simplified version of withModuleDefinitionVisitor,
which isn't passed a context and doesn't return one. You can use withSimpleModuleDefinitionVisitor even if you use "non-simple with*" functions.
Add a visitor to the Schema which will visit the File's import statements (import Html as H exposing (div)) in order of their definition and report patterns.
The following example forbids using the core Html package and suggests using
elm-css instead.
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newSchema "NoCoreHtml"
|> Rule.withSimpleImportVisitor importVisitor
|> Rule.fromSchema
importVisitor : Node Import -> List Error
importVisitor node =
let
moduleName : List String
moduleName =
node
|> Node.value
|> .moduleName
|> Node.value
in
case moduleName of
[ "Html" ] ->
[ Rule.error
{ message = "Use `elm-css` instead of the core HTML package."
, details =
[ "At fruits.com, we chose to use the `elm-css` package (https://package.elm-lang.org/packages/rtfeldman/elm-css/latest/Css) to build our HTML and CSS rather than the core Html package. To keep things simple, we think it is best to not mix these different libraries."
, "The API is very similar, but instead of using the `Html` module, use the `Html.Styled`. CSS is then defined using the Html.Styled.Attributes.css function (https://package.elm-lang.org/packages/rtfeldman/elm-css/latest/Html-Styled-Attributes#css)."
]
}
(Node.range node)
]
_ ->
[]Note: withSimpleImportVisitor is a simplified version of withImportVisitor,
which isn't passed a context and doesn't return one. You can use withSimpleImportVisitor even if you use "non-simple with*" functions.
Add a visitor to the Schema which will visit the File'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 exposing (Declaration(..))
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newSchema "NoMissingTypeAnnotation"
|> Rule.withSimpleDeclarationVisitor declarationVisitor
|> Rule.fromSchema
declarationVisitor : Node Declaration -> List Error
declarationVisitor node =
case Node.value node of
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 withDeclarationVisitor,
which isn't passed a Direction (it will only be called OnEntering the node) and a context and doesn't return a context. You can use withSimpleDeclarationVisitor even if you use "non-simple with*" functions.
Add a visitor to the Schema which will visit the File'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 exposing (Expression(..))
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newSchema "NoDebug"
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromSchema
expressionVisitor : Node Expression -> List Error
expressionVisitor node =
case Node.value node of
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 withExpressionVisitor,
which isn't passed a Direction (it will only be called OnEntering the node) and a context and doesn't return a context. You can use withSimpleExpressionVisitor even if you use "non-simple with*" functions.
Adds an initial context to start collecting data during your traversal.
In some cases, you can't just report a pattern when you see it, but you want to
not report or report differently depending on information located in a different
part of the file. In that case, you collect data as the nodes in the file get
traversed and store it in what we'll call a context. This context will be
available and updated by non-"simple" "with*" functions, like
withExpressionVisitor or withImportVisitor.
Once the file has been traversed and you have collected all the data available
from the file, you can report some final errors using withFinalEvaluation.
A few use examples:
Debug.log: and if you see a call using a log
function, you need to check whether log was defined in the file, or imported
using import Debug exposing (log) or import Debug exposing (..).Html.button function,
so you built a nice Button module. You now want to forbid all uses of
Html.button, except in the Button module (See simplified example).The context you choose needs to be of the same type for all visitors. In practice,
it is similar to a Model for a rule.
The following example forbids calling Rule.newSchema with a name that is not
the same as the module's name (forbidding Rule.newSchema "NoSomething" when the
module name is Review.Rule.NoSomethingElse).
import Elm.Syntax.Expression exposing (Expression(..))
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
type alias Context =
-- Contains the module name's last part
Maybe String
rule : Rule
rule =
Rule.newSchema "NoDifferentNameForRuleAndModuleName"
|> Rule.withInitialContext Nothing
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.fromSchema
moduleDefinitionVisitor : Node Module -> Context -> ( List Error, Context )
moduleDefinitionVisitor node context =
let
moduleLastName : Maybe String
moduleLastName =
node
|> Node.value
|> Module.moduleName
|> List.reverse
|> List.head
in
( [], moduleLastName )
expressionVisitor : Node Expression -> Direction -> Context -> ( List Error, Context )
expressionVisitor node direction context =
case ( direction, Node.value node ) of
( Rule.OnEnter, Application (function :: ruleNameNode :: _) ) ->
case ( Node.value function, Node.value ruleNameNode ) of
( FunctionOrValue [ "Rule" ] "newSchema", Literal ruleName ) ->
if Just ruleName /= context then
let
suggestedName : String
suggestedName =
case context of
Just name ->
" (`" ++ name ++ "`)"
Nothing ->
""
in
( [ Rule.error
{ message = "Rule name should be the same as the module name" ++ suggestedName
, details = [ "This makes it easier to find the documentation for a rule or to find the rule in the configuration." ]
}
(Node.range ruleNameNode)
]
, context
)
else
( [], context )
_ ->
( [], context )
_ ->
( [], context )Note that due to implementation details, withInitialContext needs to be chained
right after newSchema just like in the example above, as previous
"with*" functions will be ignored.
Add a visitor to the Schema which will visit the File'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" file.
The example is simplified to only forbid the use of the Html.button expression.
import Elm.Syntax.Expression exposing (Expression(..))
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
type Context
= HtmlButtonIsAllowed
| HtmlButtonIsForbidden
rule : Rule
rule =
Rule.newSchema "NoHtmlButton"
|> Rule.withInitialContext HtmlButtonIsForbidden
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.fromSchema
moduleDefinitionVisitor : Node Module -> Context -> ( List Error, Context )
moduleDefinitionVisitor node context =
if (Node.value node |> Module.moduleName) == [ "Button" ] then
( [], HtmlButtonIsAllowed )
else
( [], HtmlButtonIsForbidden )
expressionVisitor : Node Expression -> Direction -> Context -> ( List Error, Context )
expressionVisitor node direction context =
case ( direction, context ) of
( Rule.OnEnter, HtmlButtonIsAllowed ) ->
( [], context )
( Rule.OnEnter, HtmlButtonIsForbidden ) ->
case Node.value node of
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).
Add a visitor to the Schema which will visit the File's
import statements
(import Html as H exposing (div)) in order of their definition, collect data
in the context and/or report patterns.
The following example forbids importing both Element (elm-ui) and
Html.Styled (elm-css).
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
type alias Context =
{ elmUiWasImported : Bool
, elmCssWasImported : Bool
}
rule : Rule
rule =
Rule.newSchema "NoUsingBothHtmlAndHtmlStyled"
|> Rule.withInitialContext { elmUiWasImported = False, elmCssWasImported = False }
|> Rule.withImportVisitor importVisitor
|> Rule.fromSchema
error : Node Import -> Error
error node =
Rule.error
{ message = "Do not use both `elm-ui` and `elm-css`"
, details = [ "At fruits.com, we use `elm-ui` in the dashboard application, and `elm-css` in the rest of the code. We want to use `elm-ui` in our new projects, but in projects using `elm-css`, we don't want to use both libraries to keep things simple." ]
}
(Node.range node)
importVisitor : Node Import -> Context -> ( List Error, Context )
importVisitor node context =
case Node.value node |> .moduleName |> Node.value of
[ "Element" ] ->
if context.elmCssWasImported then
( [ error node ]
, { context | elmUiWasImported = True }
)
else
( [ error node ]
, { context | elmUiWasImported = True }
)
[ "Html", "Styled" ] ->
if context.elmUiWasImported then
( [ error node ]
, { context | elmCssWasImported = True }
)
else
( [ error node ]
, { context | elmCssWasImported = True }
)
_ ->
( [], context )This example was written in a different way in the example for withFinalEvaluation.
Tip: If you do not need to collect or use the context in this visitor, you may wish to use the
simpler withSimpleImportVisitor function.
Represents whether a Node is being traversed before having seen its children (OnEntering the Node), or after (OnExiting the Node).
When visiting the AST, nodes are visited twice: once on OnEnter, before the
children of the node will be visited, and once on OnExit, after the children of
the node have been visited.
In most cases, you'll only want to handle the OnEnter case, but in some cases,
you'll want to visit a Node after having seen its children. For instance, if
you're trying to detect the unused variables defined inside of a let in expression,
you'll 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 : Context -> Direction -> Node Expression -> ( List Error, Context )
expressionVisitor context direction node =
case ( direction, node ) of
( Rule.OnEnter, Expression.FunctionOrValue moduleName name ) ->
( [], markVariableAsUsed context name )
-- Find variables declared in `let in` expression
( Rule.OnEnter, LetExpression letBlock ) ->
( [], registerVariables context letBlock )
-- When exiting the `let in expression, report the variables that were not used.
( Rule.OnExit, LetExpression _ ) ->
( unusedVariables context |> List.map createError, context )Add a visitor to the Schema which will visit the File'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 exposing (Declaration(..))
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
type ExposedFunctions
= All
| OnlySome (List String)
rule : Rule
rule =
Rule.newSchema "NoMissingDocumentationForExposedFunctions"
|> Rule.withInitialContext (OnlySome [])
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withDeclarationVisitor declarationVisitor
|> Rule.fromSchema
moduleDefinitionVisitor : Node Module -> ExposedFunctions -> ( List Error, ExposedFunctions )
moduleDefinitionVisitor node context =
case Node.value node |> Module.exposingList of
Exposing.All _ ->
( [], All )
Exposing.Explicit exposedValues ->
( [], OnlySome (List.filterMap exposedFunctionName exposedValues) )
exposedFunctionName : Node Exposing.TopLevelExpose -> Maybe String
exposedFunctionName value =
case Node.value value of
Exposing.FunctionExpose functionName ->
Just functionName
_ ->
Nothing
declarationVisitor : Node Declaration -> Direction -> ExposedFunctions -> ( List Error, ExposedFunctions )
declarationVisitor node direction context =
case ( direction, Node.value node ) of
( Rule.OnEnter, FunctionDeclaration { documentation, declaration } ) ->
let
functionName : String
functionName =
Node.value declaration |> .name |> Node.value
in
if documentation == Nothing && isExposed context functionName then
( [ Rule.error
{ message = "Exposed function " ++ functionName ++ " is missing a type annotation"
, details =
[ "Type annotations are very helpful for people who use the module. It can give a lot of information without having to read the contents of the function."
, "To add a type annotation, add a line like `" functionName ++ " : ()`, and replace the `()` by the type of the function. If you don't replace `()`, the compiler should give you a suggestion of what the type should be."
]
}
(Node.range node)
]
, context
)
else
( [], context )
_ ->
( [], context )
isExposed : ExposedFunctions -> String -> Bool
isExposed exposedFunctions name =
case exposedFunctions of
All ->
True
OnlySome exposedList ->
List.member name exposedListTip: If you do not need to collect or use the context in this visitor, you may wish to use the
simpler withSimpleDeclarationVisitor function.
Add a visitor to the Schema which will visit the File's
declaration statements
(someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg),
collect data and/or report patterns.
It is similar to withDeclarationVisitor, but the visitor used with this function is called before the visitor added with withDeclarationVisitor. You can use this visitor in order to look ahead and add the file's types and variables into your context, before visiting the contents of the file using withDeclarationVisitor and withExpressionVisitor. Otherwise, using withDeclarationVisitor is probably a simpler choice.
Add a visitor to the Schema which will visit the File'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).
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 exposing (Expression(..))
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Direction, Error, Rule)
type Context
= DebugLogWasNotImported
| DebugLogWasImported
rule : Rule
rule =
Rule.newSchema "NoDebugEvenIfImported"
|> Rule.withInitialContext DebugLogWasNotImported
|> Rule.withImportVisitor importVisitor
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.fromSchema
importVisitor : Node Import -> Context -> ( List Error, Context )
importVisitor node context =
case ( Node.value node |> .moduleName |> Node.value, (Node.value node).exposingList |> Maybe.map Node.value ) of
( [ "Debug" ], Just (Exposing.All _) ) ->
( [], DebugLogWasImported )
( [ "Debug" ], Just (Exposing.Explicit exposedFunctions) ) ->
let
isLogFunction : Node Exposing.TopLevelExpose -> Bool
isLogFunction exposeNode =
case Node.value exposeNode of
FunctionExpose "log" ->
True
_ ->
False
in
if List.any isLogFunction exposedFunctions then
( [], DebugLogWasImported )
else
( [], DebugLogWasNotImported )
_ ->
( [], DebugLogWasNotImported )
expressionVisitor : Node Expression -> Direction -> Context -> ( List Error, Context )
expressionVisitor node direction context =
case context of
DebugLogWasNotImported ->
( [], context )
DebugLogWasImported ->
case ( direction, Node.value node ) of
( Rule.OnEnter, FunctionOrValue [] "log" ) ->
( [ Rule.error
{ message = "Remove the use of `Debug` before shipping to production"
, details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
}
(Node.range node)
]
, context
)
_ ->
( [], context )Tip: If you do not need to collect or use the context in this visitor, you may wish to use the
simpler withSimpleExpressionVisitor function.
Add a function that makes a final evaluation based only on the data that was
collected in the context. This can be useful if you can't or if it is hard to
determine something as you traverse the file.
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 withFinalEvaluation.
import Dict as Dict exposing (Dict)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.Rule as Rule exposing (Error, Rule)
type alias Context =
Dict (List String) Range
rule : Rule
rule =
Rule.newSchema "NoUsingBothHtmlAndHtmlStyled"
|> Rule.withInitialContext Dict.empty
|> Rule.withImportVisitor importVisitor
|> Rule.withFinalEvaluation finalEvaluation
|> Rule.fromSchema
importVisitor : Node Import -> Context -> ( List Error, Context )
importVisitor node context =
( [], Dict.insert (Node.value node |> .moduleName |> Node.value) (Node.range node) context )
finalEvaluation : Context -> List Error
finalEvaluation context =
case ( Dict.get [ "Element" ] context, Dict.get [ "Html", "Styled" ] context ) of
( Just elmUiRange, Just _ ) ->
[ Rule.error
{ message = "Do not use both `elm-ui` and `elm-css`"
, details = [ "At fruits.com, we use `elm-ui` in the dashboard application, and `elm-css` in the rest of the code. We want to use `elm-ui` in our new projects, but in projects using `elm-css`, we don't want to use both libraries to keep things simple." ]
}
elmUiRange
]
_ ->
[]Add a visitor to the Schema which will visit the project's
elm.json file.
information, such as the contents of the elm.json file, to collect data (module SomeModuleName exposing (a, b)), collect data in the context and/or report patterns.
The following example forbids exposing a file in an "Internal" directory in your elm.json file.
import Elm.Module
import Elm.Project
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
type alias Context =
Maybe Elm.Project.Project
rule : Rule
rule =
Rule.newSchema "DoNoExposeInternalModules"
|> Rule.withInitialContext Nothing
|> Rule.withElmJsonVisitor elmJsonVisitor
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.fromSchema
elmJsonVisitor : Maybe Elm.Project.Project -> Context -> Context
elmJsonVisitor elmJson context =
elmJson
moduleDefinitionVisitor : Node Module -> Context -> ( List Error, Context )
moduleDefinitionVisitor node context =
let
moduleName : List String
moduleName =
Node.value node |> Module.moduleName
in
if List.member "Internal" moduleName then
case context of
Just (Elm.Project.Package { exposed }) ->
let
exposedModules : List String
exposedModules =
case exposed of
Elm.Project.ExposedList names ->
names
|> List.map Elm.Module.toString
Elm.Project.ExposedDict fakeDict ->
fakeDict
|> List.concatMap Tuple.second
|> List.map Elm.Module.toString
in
if List.member (String.join "." moduleName) exposedModules then
( [ Rule.error "Do not expose modules in `Internal` as part of the public API" (Node.range node) ], context )
else
( [], context )
_ ->
( [], context )
else
( [], context )For more information on automatic fixing, read the documentation for Review.Fix.
Give a list of fixes to automatically fix the error.
import Review.Fix as Fix
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)
|> withFixes [ Fix.removeRange (Node.range node) ]Take a look at Review.Fix to know more on how to makes fixes.
If you pass withFixes an empty list, the error will be considered as having no
automatic fix available. Calling withFixes several times on an error will
overwrite the previous fixes.
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.
Represents an error found by a Rule.
Note: This should not be confused with Review.Error from the
Review module. Review.Error is created from
this module's Error but contains additional information like the
name of the rule that emitted it and the file name.
Creates an Error. Use it when you find a pattern that the rule should forbid.
It takes the message you want to display to the user, and a Range,
which is the location where the error should be shown (under which to put the squiggly lines in an editor).
In most cases, you can get it using Node.range.
The details is a list of strings, 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)Get the error message of an Error.
Get the error details of an Error.
Represents a construct able to analyze a
Fileand report unwanted patterns. SeenewSchema, andfromSchemafor how to create one.