This module contains functions that are used for writing rules.
elm-lint
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 withElmJsonVisitor
withSimpleModuleDefinitionVisitor
and withModuleDefinitionVisitor
withSimpleImportVisitor
and withImportVisitor
withDeclarationListVisitor
.withSimpleDeclarationVisitor
and withDeclarationVisitor
.
Before evaluating the next declaration, the expression contained in the declaration
will be visited recursively using by withSimpleExpressionVisitor
and withExpressionVisitor
withFinalEvaluation
Evaluating 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-lint
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 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 linting 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 linting 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 lint 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 Lint.Test
, which works with elm-test
.
I recommend reading through the strategies for effective testing
before
starting writing a rule.
elm-syntax
elm-lint
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.
Represents a construct able to analyze a File
and report unwanted patterns.
See newSchema
, and fromSchema
for how to create one.
Represents a Schema for a Rule
. Create one using newSchema
.
import Lint.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newSchema "NoDebug"
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromSchema
newSchema : String -> Schema { hasNoVisitor : () } ()
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 Lint.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newSchema "NoDebug"
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.withSimpleImportVisitor importVisitor
|> Rule.fromSchema
If 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 Lint.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newSchema "NoUnusedVariables"
|> Rule.withInitialContext { declaredVariables = [], usedVariables = [] }
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.withImportVisitor importVisitor
|> Rule.fromSchema
fromSchema : Schema { hasAtLeastOneVisitor : () } context -> Rule
Create a Rule
from a configured Schema
.
withSimpleModuleDefinitionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Module.Module -> List Error) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
Add 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 Lint.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.
withSimpleImportVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Import.Import -> List Error) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
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 Lint.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.
withSimpleDeclarationVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> List Error) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } 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
)
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 Lint.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 OnEnter
ing the node) and a context
and doesn't return a context. You can use withSimpleDeclarationVisitor
even if you use "non-simple with*" functions.
withSimpleExpressionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> List Error) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
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 Lint.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 OnEnter
ing the node) and a context
and doesn't return a context. You can use withSimpleExpressionVisitor
even if you use "non-simple with*" functions.
withInitialContext : context -> Schema { hasNoVisitor : () } () -> Schema { hasNoVisitor : () } context
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 Lint.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 Lint.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.
withModuleDefinitionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Module.Module -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
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 Lint.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).
withImportVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Import.Import -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
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 Lint.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 (OnEnter
ing the Node), or after (OnExit
ing 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 )
withDeclarationVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> Direction -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } 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 Lint.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 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) -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } 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.
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.
withExpressionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> Direction -> context -> ( List Error, context )) -> Schema anything context -> Schema { hasAtLeastOneVisitor : () } context
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 Lint.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.
withFinalEvaluation : (context -> List Error) -> Schema { hasAtLeastOneVisitor : () } context -> Schema { hasAtLeastOneVisitor : () } context
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 Lint.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
]
_ ->
[]
withElmJsonVisitor : (Maybe Elm.Project.Project -> context -> context) -> Schema anything context -> Schema anything context
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 Lint.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 Lint.Fix
.
withFixes : List Lint.Fix.Fix -> Error -> Error
Give a list of fixes to automatically fix the error.
import Lint.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 Lint.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 Lint.Error
from the
Lint
module. Lint.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.
error : { message : String, details : List String } -> Elm.Syntax.Range.Range -> Error
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)
errorMessage : Error -> String
Get the error message of an Error
.
errorDetails : Error -> List String
Get the error details of an Error
.
errorRange : Error -> Elm.Syntax.Range.Range
errorFixes : Error -> Maybe (List Lint.Fix.Fix)
name : Rule -> String
Get the name of a Rule
.
analyzer : Rule -> Lint.Project.Project -> Elm.Syntax.File.File -> List Error
Get the analyzer function of a Rule
.