jfmengels / elm-review-unused / NoUnused.Exports

Forbid the use of exposed elements (functions, values or types) that are never used in your project.

🔧 Running with --fix will automatically remove all the reported errors, except for the ones reported when using reportUnusedProductionExports. It won't automatically remove unused modules though.

If the project is a package and the module that declared the element is exposed, then nothing will be reported.

The behavior of the rule also depends on whether the analyzed module is exposing elements explicitly (module X exposing (A, b)) or exposing everything (module X exposing (..)).

When exposing elements explicitly, the rule will report and remove elements from the exposing clause (exposing (used, unused) to exposing (used)) when they're never used in other Elm files of the project.

When exposing all, the rule will report and remove the declaration of elements if they're used neither in the file they're declared in nor in any other files, making it act somewhat like NoUnused.Variables, complementing it because NoUnused.Variables doesn't report top-level declarations when the module is exposing everything.

rule : Review.Rule.Rule

Report functions and types that are exposed from a module but that are never used in other modules. Also reports when a module is entirely unused.

config =
    [ NoUnused.Exports.rule
    ]

This is equivalent to NoUnused.Exports.toRule NoUnused.Exports.defaults.

Going one step further

This rule can be configured to report more unused elements than the default configuration.


type Configuration

Configuration for the rule. Use defaults to get a default configuration and use toRule to turn it into a rule. You can change the configuration using reportUnusedProductionExports.

defaults : Configuration

Default configuration. This will only report exported elements that are never used in other modules.

reportUnusedProductionExports : { isProductionFile : { moduleName : Elm.Syntax.ModuleName.ModuleName, filePath : String, isInSourceDirectories : Basics.Bool } -> Basics.Bool, exceptionsAre : List Exception } -> Configuration -> Configuration

Configures the rule to report elements defined in production code but only used in non-production files.

import NoUnused.Exports exposing (annotatedBy)

config =
    [ NoUnused.Exports.defaults
        |> NoUnused.Exports.reportUnusedProductionExports
            { isProductionFile =
                \{ moduleName, filePath, isInSourceDirectories } ->
                    isInSourceDirectories
                        && not (String.endsWith "/Example.elm" filePath)
            , exceptionsAre = [ annotatedBy "@test-helper" ]
            }
        |> NoUnused.Exports.toRule
    ]

Elements reported using this configuration won't be automatically fixed as they require removing the code that uses the element.

This function needs to know two things:

  1. Which files are considered to be production files, which is determined by a function that you provide. Generally, production files are in the "source-directories", which is indicated by isInSourceDirectories (given as an argument to the function) being True. If you want to exclude more files, you can use the filePath or moduleName of the Elm module, whichever is more practical for you to use. filePath is relative to the folder containing the elm.json file and is written in a UNIX format (/, no \).

  2. How to identify exceptions. See Exception for more information.


type Exception

Predicate to identify exceptions (that shouldn't be reported) for elements defined in production code that are only used in non-production code.

A problem with reporting these elements is that it's going to produce false positives, as there are legitimate use-cases for exporting these elements, hence the need for the rule to be able to identify them.

For instance, while it's generally discouraged, you might want to test the internals of an API (to make sure some properties hold given very specific situations). In this case, your module then needs to expose a way to gain insight to the internals.

Another example is giving the means for tests to create opaque types that are impossible or very hard to create in a test environment. This can be the case for types that can only be created through the decoding of an HTTP request.

Note that another common way to handle these use-cases is to move the internals to another module that exposes everything while making sure only specific production modules import it.

annotatedBy : String -> Exception

Prevents reporting usages of elements that contain a specific tag in their documentation.

Given the following configuration

NoUnused.Exports.defaults
    |> NoUnused.Exports.reportUnusedProductionExports
        { isProductionFile = isProductionFile
        , exceptionsAre = [ annotatedBy "@test-helper" ]
        }
    |> NoUnused.Exports.toRule

any element that has @test-helper in its documentation will not be reported as unused (as long as its used at least once in the project):

{-| @test-helper
-}
someFunction input =
    doSomethingComplexWith input

A recommended practice is to have annotations start with @.

You can use this function several times to define multiple annotations.

suffixedBy : String -> Exception

Prevents reporting usages of elements whose name end with a specific string.

Given the following configuration

NoUnused.Exports.defaults
    |> NoUnused.Exports.reportUnusedProductionExports
        { isProductionFile = isProductionFile
        , exceptionsAre = [ suffixedBy "_FOR_TESTS" ]
        }
    |> NoUnused.Exports.toRule

any element that ends with "_FOR_TESTS" will not be reported as unused (as long as its used at least once in the project):

someFunction_FOR_TESTS input =
    doSomethingComplexWith input

You can use this function several times to define multiple suffixes.

prefixedBy : String -> Exception

Prevents reporting usages of elements whose name start with a specific string.

Given the following configuration

NoUnused.Exports.defaults
    |> NoUnused.Exports.reportUnusedProductionExports
        { isProductionFile = isProductionFile
        , exceptionsAre = [ prefixedBy "test_" ]
        }
    |> NoUnused.Exports.toRule

any element that starts with "test_" will not be reported as unused (as long as its used at least once in the project):

test_someFunction input =
    doSomethingComplexWith input

You can use this function several times to define multiple prefixes.

definedInModule : ({ moduleName : Elm.Syntax.ModuleName.ModuleName, filePath : String } -> Basics.Bool) -> Exception

Prevents reporting usages of elements in some modules.

Given the following configuration

NoUnused.Exports.defaults
    |> NoUnused.Exports.reportUnusedProductionExports
        { isProductionFile = isProductionFile
        , exceptionsAre =
            [ definedInModule
                (\{ moduleName, filePath } ->
                    List.member "Util" moduleName
                        || String.startsWith "src/test-helpers/" filePath
                )
            ]
        }
    |> NoUnused.Exports.toRule

no elements from modules named *.Util.* or modules inside src/test-helpers/ will be reported.

The provided filePath is relative to the project's elm.json and is in a UNIX style (/, no \).

Try it out

You can try this rule out by running the following commands:

Using the default configuration:

elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Exports

Using reportUnusedProductionExports with the following configuration:

NoUnused.Exports.defaults
    |> NoUnused.Exports.reportUnusedProductionExports
        { isProductionFile = \{ moduleName, filePath, isInSourceDirectories } -> isInSourceDirectories
        , exceptionsAre = [ annotatedBy "