jfmengels / elm-lint / Lint.Rule

This module contains functions that are used for writing rules.

How does it work?

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:

Evaluating a node means two things:

elm-lint 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.

What makes a good rule

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.

A good rule name

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.

A helpful error message and details

The error message should give more information about the problem. It is split into two parts:

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.

The smallest section of code that makes sense

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.

Good rule documentation

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.

Strategies for writing rules effectively

Use Test-Driven Development

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.

Look at the documentation for 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.

Writing a Rule

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.

Definition


type Rule

Represents a construct able to analyze a File and report unwanted patterns. See newSchema, and fromSchema for how to create one.


type Schema configurationState context

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

Creating a Rule

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.

Builder functions without context

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 OnEntering 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 OnEntering the node) and a context and doesn't return a context. You can use withSimpleExpressionVisitor even if you use "non-simple with*" functions.

Builder functions with context

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:

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.


type Direction
    = OnEnter
    | OnExit

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 )

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
            ]

        _ ->
            []

Builder functions to analyze the project's data

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 )

Automatic fixing

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.

Errors


type Error

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

Get the Range of an Error.

errorFixes : Error -> Maybe (List Lint.Fix.Fix)

Get the Range of an Error.

ACCESS

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.