jfmengels / elm-review / Review.Rule

This module contains functions that are used for writing rules.

NOTE: If you want to create a package containing elm-review rules, I highly recommend using the CLI's elm-review new-package subcommand. This will create a new package that will help you use the best practices and give you helpful tools like easy auto-publishing. More information is available in the maintenance file generated along with it.

If you want to add/create a rule for the package or for your local configuration, then I recommend using elm-review new-rule, which will create a source and test file which you can use as a starting point. For packages, it will add the rule everywhere it should be present (exposed-modules, README, ...).

How does it work?

elm-review reads the modules, elm.json, dependencies and README.md from your project, and turns each module into an Abstract Syntax Tree (AST), a tree-like structure which represents your source code, using the elm-syntax package.

elm-review then feeds all this data into review rules, that traverse them to report problems. The way that review rules go through the data depends on whether it is a module rule or a project rule.

elm-review relies on the elm-syntax package, and all the node types you'll see will be coming from there. You are likely to need to have the documentation for that package open when writing a rule.

There are plenty of examples in this documentation, and you can also look at the source code of existing rules to better grasp how they work.

NOTE: These examples are only here to showcase how to write rules and how a function can be used. They are not necessarily good rules to enforce. See the section on whether to write a rule for more on that. Even if you think they are good ideas to enforce, they are often not complete, as there are other patterns you would want to forbid, but that are not handled by the example.

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

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 module containing the rule be the same as the rule name. This will make it easier to find the module in the project or on the packages website when trying to get more information.

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

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

Strategies for writing rules effectively

Use Test-Driven Development

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.

Look at the documentation for elm-syntax

elm-review is heavily dependent on the types that elm-syntax provides. If you don't understand the AST it provides, you will have a hard time implementing the rule you wish to create.

Writing a Rule


type Rule

Represents a construct able to analyze a project and report unwanted patterns.

You can create module rules or project rules.

Creating a module rule

A "module rule" looks at modules (i.e. files) one by one. When it finishes looking at a module and reporting errors, it forgets everything about the module it just analyzed before starting to look at a different module. You should create one of these if you do not need to know the contents of a different module in the project, such as what functions are exposed. If you do need that information, you should create a project rule.

If you are new to writing rules, I would recommend learning how to build a module rule first, as they are in practice a simpler version of project rules.

The traversal of a module rule is the following:

Evaluating/visiting a node means two things:


type ModuleRuleSchema schemaState moduleContext

Represents a schema for a module Rule.

Start by using newModuleRuleSchema, then add visitors to look at the parts of the code you are interested in.

import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoDebug" ()
        |> Rule.withSimpleExpressionVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

newModuleRuleSchema : String -> moduleContext -> ModuleRuleSchema { canCollectProjectData : () } moduleContext

Creates a schema for a module rule. Will require adding module visitors calling fromModuleRuleSchema to create a usable Rule. Use "with*" functions from this module, like withSimpleExpressionVisitor or withSimpleImportVisitor to make it report something.

The first argument is the rule name. I highly recommend naming it just like the module name (including all the . there may be).

The second argument is the initial moduleContext, i.e. the data that the rule will accumulate as the module will be traversed, and allows the rule to know/remember what happens in other parts of the module. If you don't need a context, I recommend specifying (), and using functions from this module with names starting with "withSimple".

module My.Rule.Name exposing (rule)

import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "My.Rule.Name" ()
        |> Rule.withSimpleExpressionVisitor expressionVisitor
        |> Rule.withSimpleImportVisitor importVisitor
        |> Rule.fromModuleRuleSchema

If you do need information from other parts of the module, then you should specify an initial context, and I recommend using "with*" functions without "Simple" in their name, like withExpressionEnterVisitor, withImportVisitor or withFinalModuleEvaluation.

import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoUnusedVariables" initialContext
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.withImportVisitor importVisitor
        |> Rule.fromModuleRuleSchema

type alias Context =
    { declaredVariables : List String
    , usedVariables : List String
    }

initialContext : Context
initialContext =
    { declaredVariables = [], usedVariables = [] }

fromModuleRuleSchema : ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext -> Rule

Create a Rule from a configured ModuleRuleSchema.

Builder functions without context

withSimpleModuleDefinitionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Module.Module -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's module definition (module SomeModuleName exposing (a, b)) and report patterns.

The following example forbids having _ in any part of a module name.

import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoUnderscoreInModuleName" ()
        |> Rule.withSimpleModuleDefinitionVisitor moduleDefinitionVisitor
        |> Rule.fromModuleRuleSchema

moduleDefinitionVisitor : Node Module -> List (Rule.Error {})
moduleDefinitionVisitor node =
    if List.any (String.contains "_") (Node.value node |> Module.moduleName) then
        [ Rule.error
            { message = "Do not use `_` in a module name"
            , details = [ "By convention, Elm modules names use Pascal case (like `MyModuleName`). Please rename your module using this format." ]
            }
            (Node.range node)
        ]

    else
        []

Note: withSimpleModuleDefinitionVisitor is a simplified version of withModuleDefinitionVisitor, which isn't passed a context and doesn't return one. You can use withSimpleModuleDefinitionVisitor even if you use "non-simple with*" functions.

withSimpleCommentsVisitor : (List (Elm.Syntax.Node.Node String) -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's comments.

This visitor will give you access to the list of comments (in source order) in the module all at once. Note that comments that are parsed as documentation comments by elm-syntax are not included in this list.

As such, the following comments are included (✅) / excluded (❌):

The following example forbids words like "TODO" appearing in a comment.

import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoTodoComment" ()
        |> Rule.withSimpleCommentsVisitor commentsVisitor
        |> Rule.fromModuleRuleSchema

commentsVisitor : List (Node String) -> List (Rule.Error {})
commentsVisitor comments =
    comments
        |> List.concatMap
            (\commentNode ->
                String.indexes "TODO" (Node.value commentNode)
                    |> List.map (errorAtPosition (Node.range commentNode))
            )

errorAtPosition : Range -> Int -> Error {}
errorAtPosition range index =
    Rule.error
        { message = "TODO needs to be handled"
        , details = [ "At fruits.com, we prefer not to have lingering TODO comments. Either fix the TODO now or create an issue for it." ]
        }
        -- Here you would ideally only target the TODO keyword
        -- or the rest of the line it appears on,
        -- so you would change `range` using `index`.
        range

Note: withSimpleCommentsVisitor is a simplified version of withCommentsVisitor, which isn't passed a context and doesn't return one. You can use withCommentsVisitor even if you use "non-simple with*" functions.

withSimpleImportVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Import.Import -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's import statements (import Html as H exposing (div)) in order of their definition and report patterns.

The following example forbids using the core Html package and suggests using elm-css instead.

import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoCoreHtml" ()
        |> Rule.withSimpleImportVisitor importVisitor
        |> Rule.fromModuleRuleSchema

importVisitor : Node Import -> List (Rule.Error {})
importVisitor node =
    let
        moduleName : List String
        moduleName =
            node
                |> Node.value
                |> .moduleName
                |> Node.value
    in
    case moduleName of
        [ "Html" ] ->
            [ Rule.error
                { message = "Use `elm-css` instead of the core HTML package."
                , details =
                    [ "At fruits.com, we chose to use the `elm-css` package (https://package.elm-lang.org/packages/rtfeldman/elm-css/latest/Css) to build our HTML and CSS rather than the core Html package. To keep things simple, we think it is best to not mix these different libraries."
                    , "The API is very similar, but instead of using the `Html` module, use the `Html.Styled`. CSS is then defined using the Html.Styled.Attributes.css function (https://package.elm-lang.org/packages/rtfeldman/elm-css/latest/Html-Styled-Attributes#css)."
                    ]
                }
                (Node.range node)
            ]

        _ ->
            []

Note: withSimpleImportVisitor is a simplified version of withImportVisitor, which isn't passed a context and doesn't return one. You can use withSimpleImportVisitor even if you use "non-simple with*" functions.

withSimpleDeclarationVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's declaration statements (someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg) and report patterns. The declarations will be visited in the order of their definition.

The following example forbids declaring a function or a value without a type annotation.

import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoMissingTypeAnnotation" ()
        |> Rule.withSimpleDeclarationVisitor declarationVisitor
        |> Rule.fromModuleRuleSchema

declarationVisitor : Node Declaration -> List (Rule.Error {})
declarationVisitor node =
    case Node.value node of
        Declaration.FunctionDeclaration { signature, declaration } ->
            case signature of
                Just _ ->
                    []

                Nothing ->
                    let
                        functionName : String
                        functionName =
                            declaration |> Node.value |> .name |> Node.value
                    in
                    [ Rule.error
                        { message = "Missing type annotation for `" ++ functionName ++ "`"
                        , details =
                            [ "Type annotations are very helpful for people who read your code. It can give a lot of information without having to read the contents of the function. When encountering problems, the compiler will also give much more precise and helpful information to help you solve the problem."
                            , "To add a type annotation, add a line like `" functionName ++ " : ()`, and replace the `()` by the type of the function. If you don't replace `()`, the compiler should give you a suggestion of what the type should be."
                            ]
                        }
                        (Node.range node)
                    ]

        _ ->
            []

Note: withSimpleDeclarationVisitor is a simplified version of withDeclarationEnterVisitor, which isn't passed a context and doesn't return one either. You can use withSimpleDeclarationVisitor even if you use "non-simple with*" functions.

withSimpleExpressionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> List (Error {})) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's expressions (1, True, add 1 2, 1 + 2). The expressions are visited in pre-order depth-first search, meaning that an expression will be visited, then its first child, the first child's children (and so on), then the second child (and so on).

The following example forbids using the Debug module.

import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoDebug" ()
        |> Rule.withSimpleExpressionVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

expressionVisitor : Node Expression -> List (Rule.Error {})
expressionVisitor node =
    case Node.value node of
        Expression.FunctionOrValue moduleName fnName ->
            if List.member "Debug" moduleName then
                [ Rule.error
                    { message = "Remove the use of `Debug` before shipping to production"
                    , details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
                    }
                    (Node.range node)
                ]

            else
                []

        _ ->
            []

Note: withSimpleExpressionVisitor is a simplified version of withExpressionEnterVisitor, which isn't passed a context and doesn't return one either. You can use withSimpleExpressionVisitor even if you use "non-simple with*" functions.

Builder functions with context

newModuleRuleSchemaUsingContextCreator : String -> ContextCreator () moduleContext -> ModuleRuleSchema {} moduleContext

Same as newModuleRuleSchema, except that you can request for data to help initialize the context.

module My.Rule.Name exposing (rule)

import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "My.Rule.Name" ()
        |> Rule.withSimpleExpressionVisitor expressionVisitor
        |> Rule.withSimpleImportVisitor importVisitor
        |> Rule.fromModuleRuleSchema

If you do need information from other parts of the module, then you should specify an initial context, and I recommend using "with*" functions without "Simple" in their name, like withExpressionEnterVisitor, withImportVisitor or withFinalModuleEvaluation.

import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchemaUsingContextCreator "Rule.Name" contextCreator
        -- visitors
        |> Rule.fromModuleRuleSchema

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\isInSourceDirectories () ->
            { hasTodoBeenImported = False
            , hasToStringBeenImported = False
            , isInSourceDirectories = isInSourceDirectories
            }
        )
        |> Rule.withIsInSourceDirectories

withModuleDefinitionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Module.Module -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's module definition (module SomeModuleName exposing (a, b)), collect data in the context and/or report patterns.

The following example forbids the use of Html.button except in the "Button" module. The example is simplified to only forbid the use of the Html.button expression.

import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

type Context
    = HtmlButtonIsAllowed
    | HtmlButtonIsForbidden

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoHtmlButton" HtmlButtonIsForbidden
        |> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

moduleDefinitionVisitor : Node Module -> Context -> ( List (Rule.Error {}), Context )
moduleDefinitionVisitor node context =
    if (Node.value node |> Module.moduleName) == [ "Button" ] then
        ( [], HtmlButtonIsAllowed )

    else
        ( [], HtmlButtonIsForbidden )

expressionVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionVisitor node context =
    case context of
        HtmlButtonIsAllowed ->
            ( [], context )

        HtmlButtonIsForbidden ->
            case Node.value node of
                Expression.FunctionOrValue [ "Html" ] "button" ->
                    ( [ Rule.error
                            { message = "Do not use `Html.button` directly"
                            , details = [ "At fruits.com, we've built a nice `Button` module that suits our needs better. Using this module instead of `Html.button` ensures we have a consistent button experience across the website." ]
                            }
                            (Node.range node)
                      ]
                    , context
                    )

                _ ->
                    ( [], context )

        _ ->
            ( [], context )

Tip: If you do not need to collect data in this visitor, you may wish to use the simpler withSimpleModuleDefinitionVisitor function.

Tip: The rule above is very brittle. What if button was imported using import Html exposing (button) or import Html exposing (..), or if Html was aliased (import Html as H)? Then the rule above would not catch and report the use Html.button. To handle this, check out withModuleNameLookupTable.

withModuleDocumentationVisitor : (Maybe (Elm.Syntax.Node.Node String) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's documentation, collect data in the context and/or report patterns.

This visitor will give you access to the module documentation comment. Modules don't always have a documentation. When that is the case, the visitor will be called with the Nothing as the module documentation.

withCommentsVisitor : (List (Elm.Syntax.Node.Node String) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's comments, collect data in the context and/or report patterns.

This visitor will give you access to the list of comments (in source order) in the module all at once. Note that comments that are parsed as documentation comments by elm-syntax are not included in this list.

As such, the following comments are included (✅) / excluded (❌):

Tip: If you do not need to collect data in this visitor, you may wish to use the simpler withSimpleCommentsVisitor function.

Tip: If you only need to access the module documentation, you should use withModuleDocumentationVisitor instead.

withImportVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Import.Import -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's import statements (import Html as H exposing (div)) in order of their definition, collect data in the context and/or report patterns.

The following example forbids importing both Element (elm-ui) and Html.Styled (elm-css).

import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

type alias Context =
    { elmUiWasImported : Bool
    , elmCssWasImported : Bool
    }

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoUsingBothHtmlAndHtmlStyled" initialContext
        |> Rule.withImportVisitor importVisitor
        |> Rule.fromModuleRuleSchema

initialContext : Context
initialContext =
    { elmUiWasImported = False
    , elmCssWasImported = False
    }

error : Node Import -> Error {}
error node =
    Rule.error
        { message = "Do not use both `elm-ui` and `elm-css`"
        , details = [ "At fruits.com, we use `elm-ui` in the dashboard application, and `elm-css` in the rest of the code. We want to use `elm-ui` in our new projects, but in projects using `elm-css`, we don't want to use both libraries to keep things simple." ]
        }
        (Node.range node)

importVisitor : Node Import -> Context -> ( List (Rule.Error {}), Context )
importVisitor node context =
    case Node.value node |> .moduleName |> Node.value of
        [ "Element" ] ->
            if context.elmCssWasImported then
                ( [ error node ]
                , { context | elmUiWasImported = True }
                )

            else
                ( [ error node ]
                , { context | elmUiWasImported = True }
                )

        [ "Html", "Styled" ] ->
            if context.elmUiWasImported then
                ( [ error node ]
                , { context | elmCssWasImported = True }
                )

            else
                ( [ error node ]
                , { context | elmCssWasImported = True }
                )

        _ ->
            ( [], context )

This example was written in a different way in the example for withFinalModuleEvaluation.

Tip: If you do not need to collect or use the context in this visitor, you may wish to use the simpler withSimpleImportVisitor function.


type Direction
    = OnEnter
    | OnExit

@deprecated

This is used in withDeclarationVisitor and withDeclarationVisitor, which are deprecated and will be removed in the next major version. This type will be removed along with them.

To replicate the same behavior, take a look at

/@deprecated

Represents whether a node is being traversed before having seen its children (OnEntering the node), or after (OnExiting the node).

When visiting the AST, declaration and expression nodes are visited twice: once with OnEnter, before the children of the node are visited, and once with OnExit, after the children of the node have been visited. In most cases, you'll only want to handle the OnEnter case, but there are cases where you'll want to visit a Node after having seen its children.

For instance, if you are trying to detect the unused variables defined inside of a let expression, you will want to collect the declaration of variables, note which ones are used, and at the end of the block report the ones that weren't used.

expressionVisitor : Node Expression -> Direction -> Context -> ( List (Rule.Error {}), Context )
expressionVisitor node direction context =
    case ( direction, Node.value node ) of
        ( Rule.OnEnter, Expression.FunctionOrValue moduleName name ) ->
            ( [], markVariableAsUsed context name )

        -- Find variables declared in let expression
        ( Rule.OnEnter, Expression.LetExpression letBlock ) ->
            ( [], registerVariables context letBlock )

        -- When exiting the let expression, report the variables that were not used.
        ( Rule.OnExit, Expression.LetExpression _ ) ->
            ( unusedVariables context |> List.map createError, context )

        _ ->
            ( [], context )

withDeclarationEnterVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's declaration statements (someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg), collect data and/or report patterns. The declarations will be visited in the order of their definition.

The following example forbids exposing a function or a value without it having a type annotation.

import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

type ExposedFunctions
    = All
    | OnlySome (List String)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoMissingDocumentationForExposedFunctions" (OnlySome [])
        |> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
        |> Rule.withDeclarationEnterVisitor declarationVisitor
        |> Rule.fromModuleRuleSchema

moduleDefinitionVisitor : Node Module -> ExposedFunctions -> ( List (Rule.Error {}), ExposedFunctions )
moduleDefinitionVisitor node context =
    case Node.value node |> Module.exposingList of
        Exposing.All _ ->
            ( [], All )

        Exposing.Explicit exposedValues ->
            ( [], OnlySome (List.filterMap exposedFunctionName exposedValues) )

exposedFunctionName : Node Exposing.TopLevelExpose -> Maybe String
exposedFunctionName value =
    case Node.value value of
        Exposing.FunctionExpose functionName ->
            Just functionName

        _ ->
            Nothing

declarationVisitor : Node Declaration -> ExposedFunctions -> ( List (Rule.Error {}), ExposedFunctions )
declarationVisitor node direction context =
    case Node.value node of
        Declaration.FunctionDeclaration { documentation, declaration } ->
            let
                functionName : String
                functionName =
                    Node.value declaration |> .name |> Node.value
            in
            if documentation == Nothing && isExposed context functionName then
                ( [ Rule.error
                        { message = "Exposed function " ++ functionName ++ " is missing a type annotation"
                        , details =
                            [ "Type annotations are very helpful for people who use the module. It can give a lot of information without having to read the contents of the function."
                            , "To add a type annotation, add a line like `" functionName ++ " : ()`, and replace the `()` by the type of the function. If you don't replace `()`, the compiler should give you a suggestion of what the type should be."
                            ]
                        }
                        (Node.range node)
                  ]
                , context
                )

            else
                ( [], context )

        _ ->
            ( [], context )

isExposed : ExposedFunctions -> String -> Bool
isExposed exposedFunctions name =
    case exposedFunctions of
        All ->
            True

        OnlySome exposedList ->
            List.member name exposedList

Tip: If you do not need to collect or use the context in this visitor, you may wish to use the simpler withSimpleDeclarationVisitor function.

withDeclarationExitVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's declaration statements (someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg), collect data and/or report patterns. The declarations will be visited in the order of their definition.

The following example reports unused parameters from top-level declarations.

import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
        |> Rule.withDeclarationEnterVisitor declarationEnterVisitor
        |> Rule.withDeclarationExitVisitor declarationExitVisitor
        -- Omitted, but this marks parameters as used
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

declarationEnterVisitor : Node Declaration -> Context -> ( List (Rule.Error {}), Context )
declarationEnterVisitor node context =
    case Node.value node of
        Declaration.FunctionDeclaration function ->
            ( [], registerArguments context function )

        _ ->
            ( [], context )

declarationExitVisitor : Node Declaration -> Context -> ( List (Rule.Error {}), Context )
declarationExitVisitor node context =
    case Node.value node of
        -- When exiting the function expression, report the parameters that were not used.
        Declaration.FunctionDeclaration function ->
            ( unusedParameters context |> List.map createError, removeArguments context )

        _ ->
            ( [], context )

withDeclarationVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration -> Direction -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

@deprecated

Use withDeclarationEnterVisitor and withDeclarationExitVisitor instead. In the next major version, this function will be removed and withDeclarationEnterVisitor will be renamed to withDeclarationVisitor.

/@deprecated

Add a visitor to the ModuleRuleSchema which will visit the module's declaration statements (someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg), collect data and/or report patterns. The declarations will be visited in the order of their definition.

Contrary to withSimpleDeclarationVisitor, the visitor function will be called twice with different Direction values. It will be visited with OnEnter, then the children will be visited, and then it will be visited again with OnExit. If you do not check the value of the Direction parameter, you might end up with duplicate errors and/or an unexpected moduleContext. Read more about Direction here.

The following example forbids exposing a function or a value without it having a type annotation.

import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

type ExposedFunctions
    = All
    | OnlySome (List String)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoMissingDocumentationForExposedFunctions" (OnlySome [])
        |> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
        |> Rule.withDeclarationVisitor declarationVisitor
        |> Rule.fromModuleRuleSchema

moduleDefinitionVisitor : Node Module -> ExposedFunctions -> ( List (Rule.Error {}), ExposedFunctions )
moduleDefinitionVisitor node context =
    case Node.value node |> Module.exposingList of
        Exposing.All _ ->
            ( [], All )

        Exposing.Explicit exposedValues ->
            ( [], OnlySome (List.filterMap exposedFunctionName exposedValues) )

exposedFunctionName : Node Exposing.TopLevelExpose -> Maybe String
exposedFunctionName value =
    case Node.value value of
        Exposing.FunctionExpose functionName ->
            Just functionName

        _ ->
            Nothing

declarationVisitor : Node Declaration -> Rule.Direction -> ExposedFunctions -> ( List (Rule.Error {}), ExposedFunctions )
declarationVisitor node direction context =
    case ( direction, Node.value node ) of
        ( Rule.OnEnter, Declaration.FunctionDeclaration { documentation, declaration } ) ->
            let
                functionName : String
                functionName =
                    Node.value declaration |> .name |> Node.value
            in
            if documentation == Nothing && isExposed context functionName then
                ( [ Rule.error
                        { message = "Exposed function " ++ functionName ++ " is missing a type annotation"
                        , details =
                            [ "Type annotations are very helpful for people who use the module. It can give a lot of information without having to read the contents of the function."
                            , "To add a type annotation, add a line like `" functionName ++ " : ()`, and replace the `()` by the type of the function. If you don't replace `()`, the compiler should give you a suggestion of what the type should be."
                            ]
                        }
                        (Node.range node)
                  ]
                , context
                )

            else
                ( [], context )

        _ ->
            ( [], context )

isExposed : ExposedFunctions -> String -> Bool
isExposed exposedFunctions name =
    case exposedFunctions of
        All ->
            True

        OnlySome exposedList ->
            List.member name exposedList

Tip: If you do not need to collect or use the context in this visitor, you may wish to use the simpler withSimpleDeclarationVisitor function.

withDeclarationListVisitor : (List (Elm.Syntax.Node.Node Elm.Syntax.Declaration.Declaration) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's declaration statements (someVar = add 1 2, type Bool = True | False, port output : Json.Encode.Value -> Cmd msg), to collect data and/or report patterns. The declarations will be in the same order that they appear in the source code.

It is similar to withDeclarationVisitor, but the visitor used with this function is called before the visitor added with withDeclarationVisitor. You can use this visitor in order to look ahead and add the module's types and variables into your context, before visiting the contents of the module using withDeclarationVisitor and withExpressionEnterVisitor. Otherwise, using withDeclarationVisitor is probably a simpler choice.

withExpressionEnterVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's expressions (1, True, add 1 2, 1 + 2), collect data in the context and/or report patterns. The expressions are visited in pre-order depth-first search, meaning that an expression will be visited, then its first child, the first child's children (and so on), then the second child (and so on).

Contrary to withExpressionVisitor, the visitor function will be called only once, when the expression is "entered", meaning before its children are visited.

The following example forbids the use of Debug.log even when it is imported like import Debug exposing (log).

import Elm.Syntax.Exposing as Exposing exposing (TopLevelExpose)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

type Context
    = DebugLogWasNotImported
    | DebugLogWasImported

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
        |> Rule.withImportVisitor importVisitor
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

importVisitor : Node Import -> Context -> ( List (Rule.Error {}), Context )
importVisitor node context =
    case ( Node.value node |> .moduleName |> Node.value, (Node.value node).exposingList |> Maybe.map Node.value ) of
        ( [ "Debug" ], Just (Exposing.All _) ) ->
            ( [], DebugLogWasImported )

        ( [ "Debug" ], Just (Exposing.Explicit exposedFunctions) ) ->
            let
                isLogFunction : Node Exposing.TopLevelExpose -> Bool
                isLogFunction exposeNode =
                    case Node.value exposeNode of
                        Exposing.FunctionExpose "log" ->
                            True

                        _ ->
                            False
            in
            if List.any isLogFunction exposedFunctions then
                ( [], DebugLogWasImported )

            else
                ( [], DebugLogWasNotImported )

        _ ->
            ( [], DebugLogWasNotImported )

expressionVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionVisitor node context =
    case context of
        DebugLogWasNotImported ->
            ( [], context )

        DebugLogWasImported ->
            case Node.value node of
                Expression.FunctionOrValue [] "log" ->
                    ( [ Rule.error
                            { message = "Remove the use of `Debug` before shipping to production"
                            , details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
                            }
                            (Node.range node)
                      ]
                    , context
                    )

                _ ->
                    ( [], context )

Tip: If you do not need to collect or use the context in this visitor, you may wish to use the simpler withSimpleExpressionVisitor function.

withExpressionExitVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's expressions (1, True, add 1 2, 1 + 2), collect data in the context and/or report patterns. The expressions are visited in pre-order depth-first search, meaning that an expression will be visited, then its first child, the first child's children (and so on), then the second child (and so on).

Contrary to withExpressionEnterVisitor, the visitor function will be called when the expression is "exited", meaning after its children are visited.

import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
        |> Rule.withExpressionEnterVisitor expressionEnterVisitor
        |> Rule.withExpressionExitVisitor expressionExitVisitor
        |> Rule.fromModuleRuleSchema

expressionEnterVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionEnterVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue moduleName name ->
            ( [], markVariableAsUsed context name )

        -- Find variables declared in let expression
        Expression.LetExpression letBlock ->
            ( [], registerVariables context letBlock )

        _ ->
            ( [], context )

expressionExitVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionExitVisitor node context =
    case Node.value node of
        -- When exiting the let expression, report the variables that were not used.
        Expression.LetExpression _ ->
            ( unusedVariables context |> List.map createError, removeVariables context )

        _ ->
            ( [], context )

withExpressionVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression -> Direction -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

@deprecated

Use withExpressionEnterVisitor and withExpressionExitVisitor instead. In the next major version, this function will be removed and withExpressionEnterVisitor will be renamed to withExpressionVisitor.

/@deprecated

Add a visitor to the ModuleRuleSchema which will visit the module's expressions (1, True, add 1 2, 1 + 2), collect data in the context and/or report patterns. The expressions are visited in pre-order depth-first search, meaning that an expression will be visited, then its first child, the first child's children (and so on), then the second child (and so on).

Contrary to withSimpleExpressionVisitor, the visitor function will be called twice with different Direction values. It will be visited with OnEnter, then the children will be visited, and then it will be visited again with OnExit. If you do not check the value of the Direction parameter, you might end up with duplicate errors and/or an unexpected moduleContext. Read more about Direction here.

The following example forbids the use of Debug.log even when it is imported like import Debug exposing (log).

import Elm.Syntax.Exposing as Exposing exposing (TopLevelExpose)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

type Context
    = DebugLogWasNotImported
    | DebugLogWasImported

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
        |> Rule.withImportVisitor importVisitor
        |> Rule.withExpressionVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

importVisitor : Node Import -> Context -> ( List (Rule.Error {}), Context )
importVisitor node context =
    case ( Node.value node |> .moduleName |> Node.value, (Node.value node).exposingList |> Maybe.map Node.value ) of
        ( [ "Debug" ], Just (Exposing.All _) ) ->
            ( [], DebugLogWasImported )

        ( [ "Debug" ], Just (Exposing.Explicit exposedFunctions) ) ->
            let
                isLogFunction : Node Exposing.TopLevelExpose -> Bool
                isLogFunction exposeNode =
                    case Node.value exposeNode of
                        Exposing.FunctionExpose "log" ->
                            True

                        _ ->
                            False
            in
            if List.any isLogFunction exposedFunctions then
                ( [], DebugLogWasImported )

            else
                ( [], DebugLogWasNotImported )

        _ ->
            ( [], DebugLogWasNotImported )

expressionVisitor : Node Expression -> Rule.Direction -> Context -> ( List (Error {}), Context )
expressionVisitor node direction context =
    case context of
        DebugLogWasNotImported ->
            ( [], context )

        DebugLogWasImported ->
            case ( direction, Node.value node ) of
                ( Rule.OnEnter, Expression.FunctionOrValue [] "log" ) ->
                    ( [ Rule.error
                            { message = "Remove the use of `Debug` before shipping to production"
                            , details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
                            }
                            (Node.range node)
                      ]
                    , context
                    )

                _ ->
                    ( [], context )

Tip: If you do not need to collect or use the context in this visitor, you may wish to use the simpler withSimpleExpressionVisitor function.

withCaseBranchEnterVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.CaseBlock -> ( Elm.Syntax.Node.Node Elm.Syntax.Pattern.Pattern, Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression ) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's case branches when entering the branch.

The visitor can be very useful if you need to change the context when inside a case branch.

The visitors would be called in the following order (ignore the expression visitor if you don't have one):

x =
    case evaluated of
        Pattern1 ->
            expression1

        Pattern2 ->
            expression2
  1. Expression visitor (enter) for the entire case expression.
  2. Expression visitor (enter then exit) for evaluated
  3. Case branch visitor (enter) for ( Pattern1, expression1 )
  4. Expression visitor (enter then exit) for expression1
  5. Case branch visitor (exit) for ( Pattern1, expression1 )
  6. Case branch visitor (enter) for ( Pattern2, expression2 )
  7. Expression visitor (enter then exit) for expression2
  8. Case branch visitor (exit) for ( Pattern2, expression2 )
  9. Expression visitor (exit) for the entire case expression.

You can use withCaseBranchExitVisitor to visit the node on exit.

import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Pattern exposing (Pattern)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoUnusedCaseVariables" ( [], [] )
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.withCaseBranchEnterVisitor caseBranchEnterVisitor
        |> Rule.withCaseBranchExitVisitor caseBranchExitVisitor
        |> Rule.fromModuleRuleSchema

type alias Context =
    ( List String, List (List String) )

expressionVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionVisitor node (( scope, parentScopes ) as context) =
    case context of
        Expression.FunctionOrValue [] name ->
            ( [], ( name :: used, parentScopes ) )

        _ ->
            ( [], context )

caseBranchEnterVisitor : Node Expression.LetBlock -> ( Node Pattern, Node Expression ) -> Context -> List ( Rule.Error {}, Context )
caseBranchEnterVisitor _ _ ( scope, parentScopes ) =
    -- Entering a new scope every time we enter a new branch
    ( [], ( [], scope :: parentScopes ) )

caseBranchExitVisitor : Node Expression.LetBlock -> ( Node Pattern, Node Expression ) -> Context -> List ( Rule.Error {}, Context )
caseBranchExitVisitor _ ( pattern, _ ) ( scope, parentScopes ) =
    -- Exiting the current scope every time we enter a new branch, and reporting the patterns that weren't used
    let
        namesFromPattern =
            findNamesFromPattern pattern

        ( unusedPatterns, unmatchedUsed ) =
            findUnused namesFromPattern scope

        newScopes =
            case parentScopes of
                head :: tail ->
                    ( unmatchedUsed ++ head, tail )

                [] ->
                    ( unmatched, [] )
    in
    ( List.map errorForUnused unusedPatterns, newScopes )

For convenience, the entire case expression is passed as the first argument.

withCaseBranchExitVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.CaseBlock -> ( Elm.Syntax.Node.Node Elm.Syntax.Pattern.Pattern, Elm.Syntax.Node.Node Elm.Syntax.Expression.Expression ) -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's case branches when exiting the branch.

See the documentation for withCaseBranchEnterVisitor for explanations and an example.

withLetDeclarationEnterVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.LetBlock -> Elm.Syntax.Node.Node Elm.Syntax.Expression.LetDeclaration -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's let declarations branches when entering the declaration.

The visitor can be very useful if you need to change the context when inside a let declaration.

The visitors would be called in the following order (ignore the expression visitor if you don't have one):

x =
    let
        declaration1 =
            expression1

        declaration2 =
            expression2
    in
    letInValue
  1. Expression visitor (enter) for the entire let expression.
  2. Let declaration visitor (enter) for ( declaration1, expression1 )
  3. Expression visitor (enter then exit) for expression1
  4. Let declaration visitor (exit) for ( declaration1, expression1 )
  5. Let declaration visitor (enter) for ( declaration2, expression2 )
  6. Expression visitor (enter then exit) for expression2
  7. Let declaration visitor (exit) for ( declaration2, expression2 )
  8. Expression visitor (enter then exit) for letInValue
  9. Expression visitor (exit) for the entire let expression.

You can use withLetDeclarationExitVisitor to visit the node on exit.

import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoUnusedLetFunctionParameters" ( [], [] )
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.withLetDeclarationEnterVisitor letDeclarationEnterVisitor
        |> Rule.withLetDeclarationExitVisitor letDeclarationExitVisitor
        |> Rule.fromModuleRuleSchema

type alias Context =
    ( List String, List (List String) )

expressionVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context )
expressionVisitor node (( scope, parentScopes ) as context) =
    case context of
        Expression.FunctionOrValue [] name ->
            ( [], ( name :: used, parentScopes ) )

        _ ->
            ( [], context )

letDeclarationEnterVisitor : Node Expression.LetBlock -> Node Expression.LetDeclaration -> Context -> List ( Rule.Error {}, Context )
letDeclarationEnterVisitor _ letDeclaration (( scope, parentScopes ) as context) =
    case Node.value letDeclaration of
        Expression.LetFunction _ ->
            ( [], ( [], scope :: parentScopes ) )

        Expression.LetDestructuring _ ->
            ( [], context )

letDeclarationExitVisitor : Node Expression.LetBlock -> Node Expression.LetDeclaration -> Context -> List ( Rule.Error {}, Context )
letDeclarationExitVisitor _ letDeclaration (( scope, parentScopes ) as context) =
    case Node.value letDeclaration of
        Expression.LetFunction _ ->
            let
                namesFromPattern =
                    findNamesFromArguments letFunction

                ( unusedArguments, unmatchedUsed ) =
                    findUnused namesFromPattern scope

                newScopes =
                    case parentScopes of
                        head :: tail ->
                            ( unmatchedUsed ++ head, tail )

                        [] ->
                            ( unmatched, [] )
            in
            ( List.map errorForUnused unusedArguments, newScopes )

        Expression.LetDestructuring _ ->
            ( [], context )

For convenience, the entire let expression is passed as the first argument.

withLetDeclarationExitVisitor : (Elm.Syntax.Node.Node Elm.Syntax.Expression.LetBlock -> Elm.Syntax.Node.Node Elm.Syntax.Expression.LetDeclaration -> moduleContext -> ( List (Error {}), moduleContext )) -> ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the module's let declarations branches when entering the declaration.

See the documentation for withLetDeclarationEnterVisitor for explanations and an example.

providesFixesForModuleRule : ModuleRuleSchema schemaState moduleContext -> ModuleRuleSchema schemaState moduleContext

Let elm-review know that this rule may provide fixes in the reported errors.

This information is hard for elm-review to deduce on its own, but can be very useful for improving the performance of the tool while running in fix mode.

If your rule is a project rule, then you should use providesFixesForProjectRule instead.

withFinalModuleEvaluation : (moduleContext -> List (Error {})) -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext -> ModuleRuleSchema { schemaState | hasAtLeastOneVisitor : () } moduleContext

Add a function that makes a final evaluation of the module based only on the data that was collected in the moduleContext. This can be useful if you can't or if it is hard to determine something as you traverse the module.

The following example forbids importing both Element (elm-ui) and Html.Styled (elm-css). Note that this is the same one written in the example for withImportVisitor, but using withFinalModuleEvaluation.

import Dict as Dict exposing (Dict)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.Rule as Rule exposing (Rule)

type alias Context =
    Dict (List String) Range

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoUsingBothHtmlAndHtmlStyled" Dict.empty
        |> Rule.withImportVisitor importVisitor
        |> Rule.withFinalModuleEvaluation finalEvaluation
        |> Rule.fromModuleRuleSchema

importVisitor : Node Import -> Context -> ( List (Rule.Error {}), Context )
importVisitor node context =
    ( [], Dict.insert (Node.value node |> .moduleName |> Node.value) (Node.range node) context )

finalEvaluation : Context -> List (Rule.Error {})
finalEvaluation context =
    case ( Dict.get [ "Element" ] context, Dict.get [ "Html", "Styled" ] context ) of
        ( Just elmUiRange, Just _ ) ->
            [ Rule.error
                { message = "Do not use both `elm-ui` and `elm-css`"
                , details = [ "At fruits.com, we use `elm-ui` in the dashboard application, and `elm-css` in the rest of the code. We want to use `elm-ui` in our new projects, but in projects using `elm-css`, we don't want to use both libraries to keep things simple." ]
                }
                elmUiRange
            ]

        _ ->
            []

Builder functions to analyze the project's data

withElmJsonModuleVisitor : (Maybe Elm.Project.Project -> moduleContext -> moduleContext) -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the project's elm.json file.

The following example forbids exposing a module in an "Internal" directory in your elm.json file.

import Elm.Module
import Elm.Project
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

type alias Context =
    Maybe Elm.Project.Project

rule : Rule
rule =
    Rule.newModuleRuleSchema "DoNoExposeInternalModules" Nothing
        |> Rule.withElmJsonModuleVisitor elmJsonVisitor
        |> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
        |> Rule.fromModuleRuleSchema

elmJsonVisitor : Maybe Elm.Project.Project -> Context -> Context
elmJsonVisitor elmJson context =
    elmJson

moduleDefinitionVisitor : Node Module -> Context -> ( List (Rule.Error {}), Context )
moduleDefinitionVisitor node context =
    let
        moduleName : List String
        moduleName =
            Node.value node |> Module.moduleName
    in
    if List.member "Internal" moduleName then
        case context of
            Just (Elm.Project.Package { exposed }) ->
                let
                    exposedModules : List String
                    exposedModules =
                        case exposed of
                            Elm.Project.ExposedList names ->
                                names
                                    |> List.map Elm.Module.toString

                            Elm.Project.ExposedDict fakeDict ->
                                fakeDict
                                    |> List.concatMap Tuple.second
                                    |> List.map Elm.Module.toString
                in
                if List.member (String.join "." moduleName) exposedModules then
                    ( [ Rule.error "Do not expose modules in `Internal` as part of the public API" (Node.range node) ], context )

                else
                    ( [], context )

            _ ->
                ( [], context )

    else
        ( [], context )

withReadmeModuleVisitor : (Maybe String -> moduleContext -> moduleContext) -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext

Add a visitor to the ModuleRuleSchema which will visit the project's README.md file.

withDirectDependenciesModuleVisitor : (Dict String Review.Project.Dependency.Dependency -> moduleContext -> moduleContext) -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext

Add a visitor to the ModuleRuleSchema which will examine the project's direct dependencies.

You can use this look at the modules contained in dependencies, which can make the rule very precise when it targets specific functions.

withDependenciesModuleVisitor : (Dict String Review.Project.Dependency.Dependency -> moduleContext -> moduleContext) -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext -> ModuleRuleSchema { schemaState | canCollectProjectData : () } moduleContext

Add a visitor to the ModuleRuleSchema which will examine the project's dependencies.

You can use this look at the modules contained in dependencies, which can make the rule very precise when it targets specific functions.

Creating a project rule

Project rules can look at the global picture of an Elm project. Contrary to module rules, which forget everything about the module they were looking at when going from one module to another, project rules can retain information about previously analyzed modules, and use it to report errors when analyzing a different module or after all modules have been visited.

Project rules can also report errors in the elm.json or the README.md files.

If you are new to writing rules, I would recommend learning how to build a module rule first, as they are in practice a simpler version of project rules.


type ProjectRuleSchema schemaState projectContext moduleContext

Represents a schema for a project Rule.

See the documentation for newProjectRuleSchema for how to create a project rule.

newProjectRuleSchema : String -> projectContext -> ProjectRuleSchema { canAddModuleVisitor : (), withModuleContext : Forbidden } projectContext moduleContext

Creates a schema for a project rule. Will require adding project visitors and calling fromProjectRuleSchema to create a usable Rule.

The first argument is the rule name. I highly recommend naming it just like the module name (including all the . there may be).

The second argument is the initial projectContext, i.e. the data that the rule will accumulate as the project will be traversed, and allows the rule to know/remember what happens in other parts of the project.

NOTE: Do not store functions, JSON values or regular expressions in your project context, as they will be compared internally, which may cause Elm to crash.

Project rules traverse the project in the following order:

Evaluating/visiting a node means two things:

fromProjectRuleSchema : ProjectRuleSchema { schemaState | withModuleContext : Forbidden, hasAtLeastOneVisitor : () } projectContext moduleContext -> Rule

Create a Rule from a configured ProjectRuleSchema.

withModuleVisitor : (ModuleRuleSchema {} moduleContext -> ModuleRuleSchema { moduleSchemaState | hasAtLeastOneVisitor : () } moduleContext) -> ProjectRuleSchema { projectSchemaState | canAddModuleVisitor : () } projectContext moduleContext -> ProjectRuleSchema { projectSchemaState | canAddModuleVisitor : (), withModuleContext : Required } projectContext moduleContext

Add a visitor to the ProjectRuleSchema which will visit the project's Elm modules.

A module visitor behaves like a module rule, except that it won't visit the project files, as those have already been seen by other visitors for project rules (such as withElmJsonProjectVisitor).

withModuleVisitor takes a function that takes an already initialized module rule schema and adds visitors to it, using the same functions as for building a ModuleRuleSchema.

When you use withModuleVisitor, you will be required to use withModuleContext, in order to specify how to create a moduleContext from a projectContext and vice-versa.

withModuleContext : { fromProjectToModule : ModuleKey -> Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName -> projectContext -> moduleContext, fromModuleToProject : ModuleKey -> Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName -> moduleContext -> projectContext, foldProjectContexts : projectContext -> projectContext -> projectContext } -> ProjectRuleSchema { schemaState | canAddModuleVisitor : (), withModuleContext : Required } projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : (), withModuleContext : Forbidden } projectContext moduleContext

Specify, if the project rule has a module visitor, how to:

NOTE: I suggest reading the section about [foldProjectContexts] carefully, as it is one whose implementation you will need to do carefully.

In project rules, we separate the context related to the analysis of the project as a whole and the context related to the analysis of a single module into a projectContext and a moduleContext respectively. We do this because in most project rules you won't need all the data from the projectContext to analyze a module, and some data from the module context will not make sense inside the project context.

When visiting modules, elm-review follows a kind of map-reduce architecture. The idea is the following: it starts with an initial projectContext and collects data from project-related files into it. Then, it visits every module with an initial moduleContext derived from a projectContext. At the end of a module's visit, the final moduleContext will be transformed ("map") to a projectContext. All or some of the projectContexts will then be folded into a single one, before being used in the final project evaluation or to compute another module's initial moduleContext.

This will help make the result of the review as consistent as possible, by having the results be independent of the order the modules are visited. This also gives internal guarantees as to what needs to be re-computed when re-analyzing the project, which leads to huge performance boosts in watch mode or after fixes have been applied.

The following sections will explain each function, and will be summarized by an example.

fromProjectToModule

The initial moduleContext of the module visitor is computed using fromProjectToModule from a projectContext. By default, this projectContext will be the result of visiting the project-related files (elm.json, README.md, ...). If [withContextFromImportedModules] was used, then the value will be this last projectContext, folded with each imported module's resulting projectContext, using [foldProjectContexts].

The [ModuleKey] will allow you to report errors for this specific module using errorForModule from the final project evaluation or while visiting another module. If you plan to do that, you should store this in the moduleContext. You can also get it from [fromModuleToProject], so choose what's most convenient.

The [Node] containing the module name is passed for convenience, so you don't have to visit the module definition just to get the module name. Just like what it is in elm-syntax, the value will be [ "My", "Module" ] if the module name is My.Module.

fromModuleToProject

When a module has finished being analyzed, the final moduleContext will be converted into a projectContext, so that it can later be folded with the other project contexts using foldProjectContexts. The resulting projectContext will be fed into the final project evaluation and potentially into [fromProjectToModule] for modules that import the current one.

Similarly to fromProjectToModule, the [Node] containing the module name and the [ModuleKey] are passed for convenience, so you don't have to store them in the moduleContext only to store them in the projectContext.

foldProjectContexts

This function folds two projectContext into one. This function requires a few traits to always be true.

It is not necessary for the function to be commutative (i.e. that foldProjectContexts a b equals foldProjectContexts b a). It is fine to take the value from the "initial" projectContext and ignore the other one, especially for data computed in the project-related visitors (for which you will probably define a dummy value in the fromModuleToProject function). If it helps, imagine that the second argument is the initial projectContext, or that it is an accumulator just like in List.foldl.

Summary example - Reporting unused exported functions

As an example, we will write a rule that reports functions that get exported but are unused in the rest of the project.

import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newProjectRuleSchema "NoUnusedExportedFunctions" initialProjectContext
        -- Omitted, but this will collect the list of exposed modules for packages.
        -- We don't want to report functions that are exposed
        |> Rule.withElmJsonProjectVisitor elmJsonVisitor
        |> Rule.withModuleVisitor moduleVisitor
        |> Rule.withModuleContext
            { fromProjectToModule = fromProjectToModule
            , fromModuleToProject = fromModuleToProject
            , foldProjectContexts = foldProjectContexts
            }
        |> Rule.withFinalProjectEvaluation finalEvaluationForProject
        |> Rule.fromProjectRuleSchema

moduleVisitor :
    Rule.ModuleRuleSchema {} ModuleContext
    -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema =
    schema
        -- Omitted, but this will collect the exposed functions
        |> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
        -- Omitted, but this will collect uses of exported functions
        |> Rule.withExpressionEnterVisitor expressionVisitor

type alias ProjectContext =
    { -- Modules exposed by the package, that we should not report
      exposedModules : Set ModuleName
    , exposedFunctions :
        -- An entry for each module
        Dict
            ModuleName
            { -- To report errors in this module
              moduleKey : Rule.ModuleKey

            -- An entry for each function with its location
            , exposed : Dict String Range
            }
    , used : Set ( ModuleName, String )
    }

type alias ModuleContext =
    { isExposed : Bool
    , exposed : Dict String Range
    , used : Set ( ModuleName, String )
    }

initialProjectContext : ProjectContext
initialProjectContext =
    { exposedModules = Set.empty
    , modules = Dict.empty
    , used = Set.empty
    }

fromProjectToModule : Rule.ModuleKey -> Node ModuleName -> ProjectContext -> ModuleContext
fromProjectToModule moduleKey moduleName projectContext =
    { isExposed = Set.member (Node.value moduleName) projectContext.exposedModules
    , exposed = Dict.empty
    , used = Set.empty
    }

fromModuleToProject : Rule.ModuleKey -> Node ModuleName -> ModuleContext -> ProjectContext
fromModuleToProject moduleKey moduleName moduleContext =
    { -- We don't care about this value, we'll take
      -- the one from the initial context when folding
      exposedModules = Set.empty
    , exposedFunctions =
        if moduleContext.isExposed then
            -- If the module is exposed, don't collect the exported functions
            Dict.empty

        else
            -- Create a dictionary with all the exposed functions, associated to
            -- the module that was just visited
            Dict.singleton
                (Node.value moduleName)
                { moduleKey = moduleKey
                , exposed = moduleContext.exposed
                }
    , used = moduleContext.used
    }

foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts newContext previousContext =
    { -- Always take the one from the "initial" context,
      -- which is always the second argument
      exposedModules = previousContext.exposedModules

    -- Collect the exposed functions from the new context and the previous one.
    -- We could use `Dict.merge`, but in this case, that doesn't change anything
    , exposedFunctions = Dict.union newContext.modules previousContext.modules

    -- Collect the used functions from the new context and the previous one
    , used = Set.union newContext.used previousContext.used
    }

finalEvaluationForProject : ProjectContext -> List (Rule.Error { useErrorForModule : () })
finalEvaluationForProject projectContext =
    -- Implementation of `unusedFunctions` omitted, but it returns the list
    -- of unused functions, along with the associated module key and range
    unusedFunctions projectContext
        |> List.map
            (\{ moduleKey, functionName, range } ->
                Rule.errorForModule moduleKey
                    { message = "Function `" ++ functionName ++ "` is never used"
                    , details = [ "<Omitted>" ]
                    }
                    range
            )

withModuleContextUsingContextCreator : { fromProjectToModule : ContextCreator projectContext moduleContext, fromModuleToProject : ContextCreator moduleContext projectContext, foldProjectContexts : projectContext -> projectContext -> projectContext } -> ProjectRuleSchema { schemaState | canAddModuleVisitor : (), withModuleContext : Required } projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : (), withModuleContext : Forbidden } projectContext moduleContext

Use a ContextCreator to initialize your moduleContext and projectContext. This will allow you to request more information

import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newProjectRuleSchema "NoMissingSubscriptionsCall" initialProjectContext
        |> Rule.withModuleVisitor moduleVisitor
        |> Rule.withModuleContextUsingContextCreator
            { fromProjectToModule = fromProjectToModule
            , fromModuleToProject = fromModuleToProject
            , foldProjectContexts = foldProjectContexts
            }
        |> Rule.fromProjectRuleSchema

fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext
fromProjectToModule =
    Rule.initContextCreator
        (\projectContext ->
            { -- something
            }
        )

fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext
fromModuleToProject =
    Rule.initContextCreator
        (\moduleKey moduleName moduleContext ->
            { moduleKeys = Dict.singleton moduleName moduleKey
            }
        )
        |> Rule.withModuleKey
        |> Rule.withModuleName

withElmJsonProjectVisitor : (Maybe { elmJsonKey : ElmJsonKey, project : Elm.Project.Project } -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext )) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext

Add a visitor to the ProjectRuleSchema which will visit the project's elm.json file.

It works exactly like withElmJsonModuleVisitor. The visitor will be called before any module is evaluated.

withReadmeProjectVisitor : (Maybe { readmeKey : ReadmeKey, content : String } -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext )) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext

Add a visitor to the ProjectRuleSchema which will visit the project's README.md file.

It works exactly like withReadmeModuleVisitor. The visitor will be called before any module is evaluated.

withDirectDependenciesProjectVisitor : (Dict String Review.Project.Dependency.Dependency -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext )) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext

Add a visitor to the ProjectRuleSchema which will examine the project's direct dependencies.

It works exactly like withDependenciesModuleVisitor. The visitor will be called before any module is evaluated.

withDependenciesProjectVisitor : (Dict String Review.Project.Dependency.Dependency -> projectContext -> ( List (Error { useErrorForModule : () }), projectContext )) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema { schemaState | hasAtLeastOneVisitor : () } projectContext moduleContext

Add a visitor to the ProjectRuleSchema which will examine the project's dependencies.

It works exactly like withDependenciesModuleVisitor. The visitor will be called before any module is evaluated.

withFinalProjectEvaluation : (projectContext -> List (Error { useErrorForModule : () })) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext

Add a function that makes a final evaluation of the project based only on the data that was collected in the projectContext. This can be useful if you can't report something until you have visited all the modules in the project.

It works similarly withFinalModuleEvaluation.

NOTE: Do not create errors using the error function using withFinalProjectEvaluation, but using errorForModule instead. When the project is evaluated in this function, you are not in the "context" of an Elm module (the idiomatic "context", not projectContext or moduleContext). That means that if you call error, we won't know which module to associate the error to.

withContextFromImportedModules : ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext

Allows the rule to have access to the context of the modules imported by the currently visited module. You can use for instance to know what is exposed in a different module.

When you finish analyzing a module, the moduleContext is turned into a projectContext through fromModuleToProject. Before analyzing a module, the projectContexts of its imported modules get folded into a single one starting with the initial context (that may have visited the elm.json file and/or the project's dependencies) using foldProjectContexts.

If there is information about another module that you wish to access, you should therefore store it in the moduleContext, and have it persist when transitioning to a projectContext and back to a moduleContext.

You can only access data from imported modules, not from modules that import the current module. If you need to do so, I suggest collecting all the information you need, and re-evaluate if from the final project evaluation function.

If you don't use this function, you will only be able to access the contents of the initial context. The benefit is that when re-analyzing the project, after a fix or when a file was changed in watch mode, much less work will need to be done and the analysis will be much faster, because we know other files won't influence the results of other modules' analysis.

providesFixesForProjectRule : ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext

Let elm-review know that this rule may provide fixes in the reported errors.

This information is hard for elm-review to deduce on its own, but can be very useful for improving the performance of the tool while running in fix mode.

If your rule is a module rule, then you should use providesFixesForModuleRule instead.

Requesting more information


type ContextCreator from to

Create a module context from a project context or the other way around. Use functions like withModuleName to request more information.

initContextCreator : (from -> to) -> ContextCreator from to

Initialize a new context creator.

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\moduleName () ->
            { moduleName = moduleName

            -- ...other fields
            }
        )
        |> Rule.withModuleName

withModuleName : ContextCreator Elm.Syntax.ModuleName.ModuleName (from -> to) -> ContextCreator from to

Request the name of the module.

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\moduleName () ->
            { moduleName = moduleName

            -- ...other fields
            }
        )
        |> Rule.withModuleName

withModuleNameNode : ContextCreator (Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName) (from -> to) -> ContextCreator from to

Request the node corresponding to the name of the module.

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\moduleNameNode () ->
            { moduleNameNode = moduleNameNode

            -- ...other fields
            }
        )
        |> Rule.withModuleNameNode

withIsInSourceDirectories : ContextCreator Basics.Bool (from -> to) -> ContextCreator from to

Request to know whether the current module is in the "source-directories" of the project. You can use this information to know whether the module is part of the tests or of the production code.

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\isInSourceDirectories () ->
            { isInSourceDirectories = isInSourceDirectories

            -- ...other fields
            }
        )
        |> Rule.withIsInSourceDirectories

withFilePath : ContextCreator String (from -> to) -> ContextCreator from to

Request the file path for this module, relative to the project's elm.json.

Using newModuleRuleSchemaUsingContextCreator:

rule : Rule
rule =
    Rule.newModuleRuleSchemaUsingContextCreator "YourRuleName" initialContext
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

initialContext : Rule.ContextCreator () Context
initialContext =
    Rule.initContextCreator
        (\filePath () -> { filePath = filePath })
        |> Rule.withFilePath

Using withModuleContextUsingContextCreator in a project rule:

rule : Rule
rule =
    Rule.newProjectRuleSchema "YourRuleName" initialProjectContext
        |> Rule.withModuleVisitor moduleVisitor
        |> Rule.withModuleContextUsingContextCreator
            { fromProjectToModule = fromProjectToModule
            , fromModuleToProject = fromModuleToProject
            , foldProjectContexts = foldProjectContexts
            }

fromModuleToProject : Rule.ContextCreator () Context
fromModuleToProject =
    Rule.initContextCreator
        (\filePath () -> { filePath = filePath })
        |> Rule.withFilePath

withIsFileIgnored : ContextCreator Basics.Bool (from -> to) -> ContextCreator from to

Request to know whether the errors for the current module has been ignored for this particular rule. This may be useful to reduce the amount of work related to ignored files — like collecting unnecessary data or reporting errors — when that will ignored anyway.

Note that for module rules, ignored files will be skipped automatically anyway.

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\isFileIgnored () ->
            { isFileIgnored = isFileIgnored

            -- ...other fields
            }
        )
        |> Rule.withIsFileIgnored

withModuleNameLookupTable : ContextCreator Review.ModuleNameLookupTable.ModuleNameLookupTable (from -> to) -> ContextCreator from to

Requests the module name lookup table for the types and functions inside a module.

When encountering a Expression.FunctionOrValue ModuleName String (among other nodes where we refer to a function or value), the module name available represents the module name that is in the source code. But that module name can be an alias to a different import, or it can be empty, meaning that it refers to a local value or one that has been imported explicitly or implicitly. Resolving which module the type or function comes from can be a bit tricky sometimes, and I recommend against doing it yourself.

elm-review computes this for you already. Store this value inside your module context, then use ModuleNameLookupTable.moduleNameFor or ModuleNameLookupTable.moduleNameAt to get the name of the module the type or value comes from.

import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)

type alias Context =
    { lookupTable : ModuleNameLookupTable }

rule : Rule
rule =
    Rule.newModuleRuleSchemaUsingContextCreator "NoHtmlButton" initialContext
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema
        |> Rule.ignoreErrorsForFiles [ "src/Colors.elm" ]

initialContext : Rule.ContextCreator () Context
initialContext =
    Rule.initContextCreator
        (\lookupTable () -> { lookupTable = lookupTable })
        |> Rule.withModuleNameLookupTable

expressionVisitor : Node Expression -> Context -> ( List (Error {}), Context )
expressionVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue _ "color" ->
            if ModuleNameLookupTable.moduleNameFor context.lookupTable node == Just [ "Css" ] then
                ( [ Rule.error
                        { message = "Do not use `Css.color` directly, use the Colors module instead"
                        , details = [ "We made a module which contains all the available colors of our design system. Use the functions in there instead." ]
                        }
                        (Node.range node)
                  ]
                , context
                )

            else
                ( [], context )

        _ ->
            ( [], context )

Note: If you have been using elm-review-scope before, you should use this instead.

withModuleKey : ContextCreator ModuleKey (from -> to) -> ContextCreator from to

Request the module key for this module.

rule : Rule
rule =
    Rule.newProjectRuleSchema "NoMissingSubscriptionsCall" initialProjectContext
        |> Rule.withModuleVisitor moduleVisitor
        |> Rule.withModuleContextUsingContextCreator
            { fromProjectToModule = fromProjectToModule
            , fromModuleToProject = fromModuleToProject
            , foldProjectContexts = foldProjectContexts
            }

fromModuleToProject : Rule.ContextCreator () Context
fromModuleToProject =
    Rule.initContextCreator
        (\moduleKey () -> { moduleKey = moduleKey })
        |> Rule.withModuleKey

withSourceCodeExtractor : ContextCreator (Elm.Syntax.Range.Range -> String) (from -> to) -> ContextCreator from to

Requests access to a function that gives you the source code at a given range.

rule : Rule
rule =
    Rule.newModuleRuleSchemaUsingContextCreator "YourRuleName" initialContext
        |> Rule.withExpressionEnterVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema

type alias Context =
    { extractSourceCode : Range -> String
    }

initialContext : Rule.ContextCreator () Context
initialContext =
    Rule.initContextCreator
        (\extractSourceCode () -> { extractSourceCode = extractSourceCode })
        |> Rule.withSourceCodeExtractor

The motivation for this capability was for allowing to provide higher-quality fixes, especially where you'd need to move or copy code from one place to another (example: when switching the branches of an if expression).

I discourage using this functionality to explore the source code, as the different visitor functions make for a nicer experience.

withFullAst : ContextCreator Elm.Syntax.File.File (from -> to) -> ContextCreator from to

Request the full AST for the current module.

This can be useful if you wish to avoid initializing the module context with dummy data future node visits can replace them.

For instance, if you wish to know what is exposed from a module, you may need to visit the module definition and then the list of declarations. If you need this information earlier on, you will have to provide dummy data at context initialization and store some intermediary data.

Using the full AST, you can simplify the implementation by computing the data in the context creator, without the use of visitors.

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\ast () ->
            { exposed = collectExposed ast.moduleDefinition ast.declarations

            -- ...other fields
            }
        )
        |> Rule.withFullAst

withModuleDocumentation : ContextCreator (Maybe (Elm.Syntax.Node.Node String)) (from -> to) -> ContextCreator from to

Request the module documentation. Modules don't always have a documentation. When that is the case, the module documentation will be Nothing.

contextCreator : Rule.ContextCreator () Context
contextCreator =
    Rule.initContextCreator
        (\moduleDocumentation () ->
            { moduleDocumentation = moduleDocumentation

            -- ...other fields
            }
        )
        |> Rule.withModuleDocumentation

Requesting more information (DEPRECATED)


type Metadata

Metadata for the module being visited.

@deprecated: More practical functions have been made available since the introduction of this type.

Do not store the metadata directly in your context. Prefer storing the individual pieces of information.

withMetadata : ContextCreator Metadata (from -> to) -> ContextCreator from to

Request metadata about the module.

@deprecated: Use more practical functions like

moduleNameFromMetadata : Metadata -> Elm.Syntax.ModuleName.ModuleName

Get the module name of the current module.

@deprecated: Use the more practical withModuleName instead.

moduleNameNodeFromMetadata : Metadata -> Elm.Syntax.Node.Node Elm.Syntax.ModuleName.ModuleName

Get the Node to the module name of the current module.

@deprecated: Use the more practical withModuleNameNode instead.

isInSourceDirectories : Metadata -> Basics.Bool

Learn whether the current module is in the "source-directories" of the project. You can use this information to know whether the module is part of the tests or of the production code.

@deprecated: Use the more practical withIsInSourceDirectories instead.

Errors


type Error scope

Represents an error found by a Rule. These are created by the rules.

error : { message : String, details : List String } -> Elm.Syntax.Range.Range -> Error {}

Create an Error. Use it when you find a pattern that the rule should forbid.

The message and details represent the message you want to display to the user. The details is a list of paragraphs, and each item will be visually separated when shown to the user. The details may not be empty, and this will be enforced by the tests automatically.

error : Node a -> Error {}
error node =
    Rule.error
        { message = "Remove the use of `Debug` before shipping to production"
        , details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
        }
        (Node.range node)

The [Range] corresponds to the location where the error should be shown, i.e. where to put the squiggly lines in an editor. In most cases, you can get it using [Node.range].

errorWithFix : { message : String, details : List String } -> Elm.Syntax.Range.Range -> List Review.Fix.Fix -> Error {}

Creates an Error, just like the error function, but provides an automatic fix that the user can apply.

import Review.Fix as Fix

error : Node a -> Error {}
error node =
    Rule.errorWithFix
        { message = "Remove the use of `Debug` before shipping to production"
        , details = [ "The `Debug` module is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
        }
        (Node.range node)
        [ Fix.removeRange (Node.range node) ]

Take a look at Review.Fix to know more on how to makes fixes.

If the list of fixes is empty, then it will give the same error as if you had called error instead.

Note: Each fix applies on a location in the code, defined by a range. To avoid an unpredictable result, those ranges may not overlap. The order of the fixes does not matter.


type ModuleKey

A key to be able to report an error for a specific module. You need such a key in order to use the errorForModule function. This is to prevent creating errors for modules you have not visited, or files that do not exist.

You can get a ModuleKey from the fromProjectToModule and fromModuleToProject functions that you define when using newProjectRuleSchema.

errorForModule : ModuleKey -> { message : String, details : List String } -> Elm.Syntax.Range.Range -> Error scope

Just like error, create an Error but for a specific module, instead of the module that is being visited.

You will need a ModuleKey, which you can get from the fromProjectToModule and fromModuleToProject functions that you define when using newProjectRuleSchema.

errorForModuleWithFix : ModuleKey -> { message : String, details : List String } -> Elm.Syntax.Range.Range -> List Review.Fix.Fix -> Error scope

Just like errorForModule, create an Error for a specific module, but provides an automatic fix that the user can apply.

Take a look at Review.Fix to know more on how to makes fixes.

If the list of fixes is empty, then it will give the same error as if you had called errorForModule instead.

Note: Each fix applies on a location in the code, defined by a range. To avoid an unpredictable result, those ranges may not overlap. The order of the fixes does not matter.


type ElmJsonKey

A key to be able to report an error for the elm.json file. You need this key in order to use the errorForElmJson function. This is to prevent creating errors for it if you have not visited it.

You can get a ElmJsonKey using the withElmJsonProjectVisitor function.

errorForElmJson : ElmJsonKey -> (String -> { message : String, details : List String, range : Elm.Syntax.Range.Range }) -> Error scope

Create an Error for the elm.json file.

You will need an ElmJsonKey, which you can get from the withElmJsonProjectVisitor function.

The second argument is a function that takes the elm.json content as a raw string, and returns the error details. Using the raw string, you should try and find the most fitting Range possible for the error.

errorForElmJsonWithFix : ElmJsonKey -> (String -> { message : String, details : List String, range : Elm.Syntax.Range.Range }) -> (Elm.Project.Project -> Maybe Elm.Project.Project) -> Error scope

Create an Error for the elm.json file.

You will need an ElmJsonKey, which you can get from the withElmJsonProjectVisitor function.

The second argument is a function that takes the elm.json content as a raw string, and returns the error details. Using the raw string, you should try and find the most fitting Range possible for the error.

The third argument is a function that takes the elm.json and returns a different one that will be suggested as a fix. If the function returns Nothing, no fix will be applied.

The elm.json will be the same as the one you got from withElmJsonProjectVisitor, use either depending on what you find most practical.


type ReadmeKey

A key to be able to report an error for the README.md file. You need this key in order to use the errorForReadme function. This is to prevent creating errors for it if you have not visited it.

You can get a ReadmeKey using the withReadmeProjectVisitor function.

errorForReadme : ReadmeKey -> { message : String, details : List String } -> Elm.Syntax.Range.Range -> Error scope

Create an Error for the README.md file.

You will need an ReadmeKey, which you can get from the withReadmeProjectVisitor function.

errorForReadmeWithFix : ReadmeKey -> { message : String, details : List String } -> Elm.Syntax.Range.Range -> List Review.Fix.Fix -> Error scope

Just like errorForReadme, create an Error for the README.md file, but provides an automatic fix that the user can apply.

Take a look at Review.Fix to know more on how to makes fixes.

If the list of fixes is empty, then it will give the same error as if you had called errorForReadme instead.

Note: Each fix applies on a location in the code, defined by a range. To avoid an unpredictable result, those ranges may not overlap. The order of the fixes does not matter.

globalError : { message : String, details : List String } -> Error scope

Create an Error that is not attached to any specific location in the project.

This can be useful when needing to report problems that are not tied to any file. For instance for reporting missing elements like a module that was expected to be there.

This is however NOT the recommended way when it is possible to attach an error to a location (even if it is simply the module name of a file's module declaration), because giving hints to where the problem is makes it easier for the user to solve it.

The message and details represent the message you want to display to the user. The details is a list of paragraphs, and each item will be visually separated when shown to the user. The details may not be empty, and this will be enforced by the tests automatically.

error : String -> Error scope
error moduleName =
    Rule.globalError
        { message = "Could not find module " ++ moduleName
        , details =
            [ "You mentioned the module " ++ moduleName ++ " in the configuration of this rule, but it could not be found."
            , "This likely means you misconfigured the rule or the configuration has become out of date with recent changes in your project."
            ]
        }

configurationError : String -> { message : String, details : List String } -> Rule

Creates a rule that will only report a configuration error, which stops elm-review from reviewing the project until the user has addressed the issue.

When writing rules, some of them may take configuration arguments that specify what exactly the rule should do. I recommend to define custom types to limit the possibilities of what can be considered valid and invalid configuration, so that the user gets information from the compiler when the configuration is unexpected.

Unfortunately it is not always possible or practical to let the type system forbid invalid possibilities, and you may need to manually parse or validate the arguments.

rule : SomeCustomConfiguration -> Rule
rule config =
    case parseFunctionName config.functionName of
        Nothing ->
            Rule.configurationError "RuleName"
                { message = config.functionName ++ " is not a valid function name"
                , details =
                    [ "I was expecting functionName to be a valid Elm function name."
                    , "When that is not the case, I am not able to function as expected."
                    ]
                }

        Just functionName ->
            Rule.newModuleRuleSchema "RuleName" ()
                |> Rule.withExpressionEnterVisitor (expressionVisitor functionName)
                |> Rule.fromModuleRuleSchema

When you need to look at the project before determining whether something is actually a configuration error, for instance when reporting that a targeted function does not fit some criteria (unexpected arguments, ...), you should go for more usual errors like error or potentially globalError. error would be better because it will give the user a starting place to fix the issue.

Be careful that the rule name is the same for the rule and for the configuration error.

The message and details represent the message you want to display to the user. The details is a list of paragraphs, and each item will be visually separated when shown to the user. The details may not be empty, and this will be enforced by the tests automatically.


type alias ReviewError =
Review.Error.ReviewError

Represents an error found by a Rule. These are the ones that will be reported to the user.

If you are building a Rule, you shouldn't have to use this.

errorRuleName : ReviewError -> String

Get the name of the rule that triggered this Error.

errorMessage : ReviewError -> String

Get the error message of an Error.

errorDetails : ReviewError -> List String

Get the error details of an Error.

errorRange : ReviewError -> Elm.Syntax.Range.Range

Get the Range of an Error.

errorFilePath : ReviewError -> String

Get the file path of an Error.

errorTarget : ReviewError -> Review.Error.Target

Get the target of an Error.

errorFixes : ReviewError -> Maybe (List Review.Fix.Fix)

Get the automatic fixes of an Error, if it defined any.

errorFixFailure : ReviewError -> Maybe Review.Fix.Problem

Get the reason why the fix for an error failed when its available automatic fix was attempted and deemed incorrect.

Note that if the review process was not run in fix mode previously, then this will return Nothing.

Configuring exceptions

There are situations where you don't want review rules to report errors:

  1. You copied and updated over an external library because one of your needs wasn't met, and you don't want to modify it more than necessary.
  2. Your project contains generated source code, over which you have no control or for which you do not care that some rules are enforced (like the reports of unused variables).
  3. You want to introduce a rule progressively, because there are too many errors in the project for you to fix in one go. You can then ignore the parts of the project where the problem has not yet been solved, and fix them as you go.
  4. You wrote a rule that is very specific and should only be applied to a portion of your code.
  5. You wish to disable some rules for tests files (or enable some only for tests).

You can use the following functions to ignore errors in directories or files, or only report errors found in specific directories or files.

NOTE: Even though they can be used to disable any errors, I strongly recommend against doing so if you are not in the situations listed above. I highly recommend you leave a comment explaining the reason why you use these functions, or to communicate with your colleagues if you see them adding exceptions without reason or seemingly inappropriately.

ignoreErrorsForDirectories : List String -> Rule -> Rule

Ignore the errors reported for modules in specific directories of the project.

Use it when you don't want to get review errors for generated source code or for libraries that you forked and copied over to your project.

config : List Rule
config =
    [ Some.Rule.rule
        |> Rule.ignoreErrorsForDirectories [ "generated-source/", "vendor/" ]
    , Some.Other.Rule.rule
    ]

If you want to ignore some directories for all of your rules, you can apply ignoreErrorsForDirectories like this:

config : List Rule
config =
    [ Some.Rule.rule
    , Some.Other.Rule.rule
    ]
        |> List.map (Rule.ignoreErrorsForDirectories [ "generated-source/", "vendor/" ])

The paths should be relative to the elm.json file, just like the ones for the elm.json's source-directories.

You can apply ignoreErrorsForDirectoriesseveral times for a rule, to add more ignored directories.

You can also use it when writing a rule. We can hardcode in the rule that a rule is not applicable to a folder, like tests/ for instance. The following example forbids using Debug.todo anywhere in the code, except in tests.

import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoDebugEvenIfImported" DebugLogWasNotImported
        |> Rule.withSimpleExpressionVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema
        |> Rule.ignoreErrorsForDirectories [ "tests/" ]

expressionVisitor : Node Expression -> List (Rule.Error {})
expressionVisitor node =
    case Node.value node of
        Expression.FunctionOrValue [ "Debug" ] "todo" ->
            [ Rule.error
                { message = "Remove the use of `Debug.todo` before shipping to production"
                , details = [ "`Debug.todo` is useful when developing, but is not meant to be shipped to production or published in a package. I suggest removing its use before committing and attempting to push to production." ]
                }
                (Node.range node)
            ]

        _ ->
            []

ignoreErrorsForFiles : List String -> Rule -> Rule

Ignore the errors reported for specific file paths. Use it when you don't want to review generated source code or files from external sources that you copied over to your project and don't want to be touched.

config : List Rule
config =
    [ Some.Rule.rule
        |> Rule.ignoreErrorsForFiles [ "src/Some/File.elm" ]
    , Some.Other.Rule.rule
    ]

If you want to ignore some files for all of your rules, you can apply ignoreErrorsForFiles like this:

config : List Rule
config =
    [ Some.Rule.rule
    , Some.Other.Rule.rule
    ]
        |> List.map (Rule.ignoreErrorsForFiles [ "src/Some/File.elm" ])

The paths should be relative to the elm.json file, just like the ones for the elm.json's source-directories.

You can apply ignoreErrorsForFiles several times for a rule, to add more ignored files.

You can also use it when writing a rule. We can simplify the example from withModuleDefinitionVisitor by hardcoding an exception into the rule (that forbids the use of Html.button except in the "Button" module).

import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoHtmlButton"
        |> Rule.withSimpleExpressionVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema
        |> Rule.ignoreErrorsForFiles [ "src/Button.elm" ]

expressionVisitor : Node Expression -> List (Rule.Error {})
expressionVisitor node context =
    case Node.value node of
        Expression.FunctionOrValue [ "Html" ] "button" ->
            [ Rule.error
                { message = "Do not use `Html.button` directly"
                , details = [ "At fruits.com, we've built a nice `Button` module that suits our needs better. Using this module instead of `Html.button` ensures we have a consistent button experience across the website." ]
                }
                (Node.range node)
            ]

        _ ->
            []

filterErrorsForFiles : (String -> Basics.Bool) -> Rule -> Rule

Filter the files to report errors for.

Use it to control precisely which files the rule applies or does not apply to. For example, you might have written a rule that should only be applied to one specific file.

config : List Rule
config =
    [ Some.Rule.rule
        |> Rule.filterErrorsForFiles (\path -> path == "src/Some/File.elm")
    , Some.Other.Rule.rule
    ]

If you want to specify a condition for all of your rules, you can apply filterErrorsForFiles like this:

 config : List Rule
 config =
     [ Some.Rule.For.Tests.rule
     , Some.Other.Rule.rule
     ]
         |> List.map (Rule.filterErrorsForFiles (String.startsWith "tests/"))

The received paths will be relative to the elm.json file, just like the ones for the elm.json's source-directories, and will be formatted in the Unix style src/Some/File.elm.

You can apply filterErrorsForFiles several times for a rule, the conditions will get compounded, following the behavior of List.filter.

When ignoreErrorsForFiles or ignoreErrorsForDirectories are used in combination with this function, all constraints are observed.

You can also use it when writing a rule. We can hardcode in the rule that a rule is only applicable to a folder, like src/Api/ for instance. The following example forbids using strings with hardcoded URLs, but only in the src/Api/ folder.

import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newModuleRuleSchema "NoHardcodedURLs" ()
        |> Rule.withSimpleExpressionVisitor expressionVisitor
        |> Rule.fromModuleRuleSchema
        |> Rule.filterErrorsForFiles (String.startsWith "src/Api/")

expressionVisitor : Node Expression -> List (Rule.Error {})
expressionVisitor node =
    case Node.value node of
        Expression.Literal string ->
            if isUrl string then
                [ Rule.error
                    { message = "Do not use hardcoded URLs in the API modules"
                    , details = [ "Hardcoded URLs should never make it to production. Please refer to the documentation of the `Endpoint` module." ]
                    }
                    (Node.range node)
                ]

            else
                []

        _ ->
            []

Extract information

As you might have seen so far, elm-review has quite a nice way of traversing the files of a project and collecting data.

While you have only seen the tool be used to report errors, you can also use it to extract information from the codebase. You can use this to gain insight into your codebase, or provide information to other tools to enable powerful integrations.

You can read more about how to use this in Extract information in the README, and you can find the tools to extract data below.

withDataExtractor : (projectContext -> Json.Encode.Value) -> ProjectRuleSchema schemaState projectContext moduleContext -> ProjectRuleSchema schemaState projectContext moduleContext

Extract arbitrary data from the codebase, which can be accessed by running

elm-review --report=json --extract

and by reading the value at <output>.extracts["YourRuleName"] in the output.

import Json.Encode
import Review.Rule as Rule exposing (Rule)

rule : Rule
rule =
    Rule.newProjectRuleSchema "Some.Rule.Name" initialContext
        -- visitors to collect information...
        |> Rule.withDataExtractor dataExtractor
        |> Rule.fromProjectRuleSchema

dataExtractor : ProjectContext -> Json.Encode.Value
dataExtractor projectContext =
    Json.Encode.list
        (\thing ->
            Json.Encode.object
                [ ( "name", Json.Encode.string thing.name )
                , ( "value", Json.Encode.int thing.value )
                ]
        )
        projectContext.things

preventExtract : Error a -> Error a

Make this error prevent extracting data using withDataExtractor.

Use this if the rule extracts data and an issue is discovered that would make the extraction output incorrect data.

Rule.error
    { message = "..."
    , details = [ "..." ]
    }
    (Node.range node)
    |> Rule.preventExtract

Running rules

reviewV3 : Review.Options.ReviewOptions -> List Rule -> Review.Project.Internal.Project -> { errors : List ReviewError, fixedErrors : Dict String (List ReviewError), rules : List Rule, project : Review.Project.Internal.Project, extracts : Dict String Json.Encode.Value }

Review a project and gives back the errors raised by the given rules.

Note that you won't need to use this function when writing a rule. You should only need it if you try to make elm-review run in a new environment.

import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Rule)

config : List Rule
config =
    [ Some.Rule.rule
    , Some.Other.Rule.rule
    ]

project : Project
project =
    Project.new
        |> Project.addModule { path = "src/A.elm", source = "module A exposing (a)\na = 1" }
        |> Project.addModule { path = "src/B.elm", source = "module B exposing (b)\nb = 1" }

doReview =
    let
        { errors, rules, projectData, extracts } =
            -- Replace `config` by `rules` next time you call reviewV2
            -- Replace `Nothing` by `projectData` next time you call reviewV2
            Rule.reviewV3 config Nothing project
    in
    doSomethingWithTheseValues

The resulting List Rule is the same list of rules given as input, but with an updated internal cache to make it faster to re-run the rules on the same project. If you plan on re-reviewing with the same rules and project, for instance to review the project after a file has changed, you may want to store the rules in your Model.

The rules are functions, so doing so will make your model unable to be exported/imported with elm/browser's debugger, and may cause a crash if you try to compare them or the model that holds them.

reviewV2 : List Rule -> Maybe ProjectData -> Review.Project.Internal.Project -> { errors : List ReviewError, rules : List Rule, projectData : Maybe ProjectData }

Review a project and gives back the errors raised by the given rules.

Note that you won't need to use this function when writing a rule. You should only need it if you try to make elm-review run in a new environment.

import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Rule)

config : List Rule
config =
    [ Some.Rule.rule
    , Some.Other.Rule.rule
    ]

project : Project
project =
    Project.new
        |> Project.addModule { path = "src/A.elm", source = "module A exposing (a)\na = 1" }
        |> Project.addModule { path = "src/B.elm", source = "module B exposing (b)\nb = 1" }

doReview =
    let
        { errors, rules, projectData } =
            -- Replace `config` by `rules` next time you call reviewV2
            -- Replace `Nothing` by `projectData` next time you call reviewV2
            Rule.reviewV2 config Nothing project
    in
    doSomethingWithTheseValues

The resulting List Rule is the same list of rules given as input, but with an updated internal cache to make it faster to re-run the rules on the same project. If you plan on re-reviewing with the same rules and project, for instance to review the project after a file has changed, you may want to store the rules in your Model.

The rules are functions, so doing so will make your model unable to be exported/imported with elm/browser's debugger, and may cause a crash if you try to compare them or the model that holds them.

review : List Rule -> Review.Project.Internal.Project -> ( List ReviewError, List Rule )

DEPRECATED: Use reviewV2 instead.

Review a project and gives back the errors raised by the given rules.

Note that you won't need to use this function when writing a rule. You should only need it if you try to make elm-review run in a new environment.

import Review.Project as Project exposing (Project)
import Review.Rule as Rule exposing (Rule)

config : List Rule
config =
    [ Some.Rule.rule
    , Some.Other.Rule.rule
    ]

project : Project
project =
    Project.new
        |> Project.addModule { path = "src/A.elm", source = "module A exposing (a)\na = 1" }
        |> Project.addModule { path = "src/B.elm", source = "module B exposing (b)\nb = 1" }

doReview =
    let
        ( errors, rulesWithCachedValues ) =
            Rule.review rules project
    in
    doSomethingWithTheseValues

The resulting List Rule is the same list of rules given as input, but with an updated internal cache to make it faster to re-run the rules on the same project. If you plan on re-reviewing with the same rules and project, for instance to review the project after a file has changed, you may want to store the rules in your Model.

The rules are functions, so doing so will make your model unable to be exported/imported with elm/browser's debugger, and may cause a crash if you try to compare them or the model that holds them.


type ProjectData

Internal cache about the project.

ruleName : Rule -> String

Get the name of a rule.

You should not have to use this when writing a rule.

ruleProvidesFixes : Rule -> Basics.Bool

Indicates whether the rule provides fixes.

You should not have to use this when writing a rule.

ruleKnowsAboutIgnoredFiles : Rule -> Basics.Bool

Indicates whether the rule knows about which files are ignored.

You should not have to use this when writing a rule.

withRuleId : Basics.Int -> Rule -> Rule

Assign an id to a rule. This id should be unique.

config =
    [ rule1, rule2, rule3 ]
        |> List.indexedMap Rule.withUniqueId

You should not have to use this when writing a rule.

getConfigurationError : Rule -> Maybe { message : String, details : List String }

Get the configuration error for a rule.

You should not have to use this when writing a rule. You might be looking for configurationError instead.

Internals


type Required

Used for phantom type constraints. You can safely ignore this type.


type Forbidden

Used for phantom type constraints. You can safely ignore this type.