Backport rules from elm-review-unused

This commit is contained in:
Jeroen Engels 2021-04-06 17:19:13 +02:00
parent 871a9faae1
commit 9992afbff8
9 changed files with 787 additions and 230 deletions

View File

@ -17,6 +17,7 @@ import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range exposing (Range)
import Elm.Syntax.TypeAnnotation as TypeAnnotation
import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable)
import Review.Rule as Rule exposing (Error, Rule)
import Set exposing (Set)
@ -33,8 +34,10 @@ This rule will warn arguments that are always pattern matched using a wildcard (
For package projects, custom types whose constructors are exposed as part of the package API are not reported.
Note that this rule may report false positives if you compare custom types with the `==`/`/=` operators (and never destructure the custom type), like when you do `value == Just 0`.
If you have a solution in mind to better handle these cases in a manageable way, please open an issue so we can talk about it.
Note that this rule **may report false positives** if you compare custom types with the `==` or `/=` operators
(and never destructure the custom type), like when you do `value == Just 0`, or store them in lists for instance with
[`assoc-list`](https://package.elm-lang.org/packages/pzp1997/assoc-list/latest).
This rule attempts to detect when the custom type is used in comparisons, but it may still result in false positives.
## Fail
@ -95,6 +98,7 @@ type alias ProjectContext =
, args : Dict String (List Range)
}
, usedArguments : Dict ( ModuleName, String ) (Set Int)
, customTypesNotToReport : Set ( ModuleName, String )
}
@ -104,6 +108,7 @@ type alias ModuleContext =
, exposed : Exposing
, customTypeArgs : Dict String (Dict String (List Range))
, usedArguments : Dict ( ModuleName, String ) (Set Int)
, customTypesNotToReport : Set ( ModuleName, String )
}
@ -147,6 +152,7 @@ initialProjectContext =
{ exposedModules = Set.empty
, customTypeArgs = Dict.empty
, usedArguments = Dict.empty
, customTypesNotToReport = Set.empty
}
@ -159,6 +165,7 @@ fromProjectToModule =
, exposed = Exposing.Explicit []
, customTypeArgs = Dict.empty
, usedArguments = Dict.empty
, customTypesNotToReport = Set.empty
}
)
|> Rule.withModuleNameLookupTable
@ -176,24 +183,43 @@ fromModuleToProject =
{ moduleKey = moduleKey
, args = getNonExposedCustomTypes moduleContext
}
, usedArguments =
Dict.foldl
(\( moduleNameForType, name ) value dict ->
case moduleNameForType of
[] ->
Dict.insert ( Rule.moduleNameFromMetadata metadata, name ) value dict
_ ->
Dict.insert ( moduleNameForType, name ) value dict
)
Dict.empty
moduleContext.usedArguments
, usedArguments = replaceLocalModuleNameForDict (Rule.moduleNameFromMetadata metadata) moduleContext.usedArguments
, customTypesNotToReport = replaceLocalModuleNameForSet (Rule.moduleNameFromMetadata metadata) moduleContext.customTypesNotToReport
}
)
|> Rule.withModuleKey
|> Rule.withMetadata
replaceLocalModuleNameForSet : ModuleName -> Set ( ModuleName, comparable ) -> Set ( ModuleName, comparable )
replaceLocalModuleNameForSet moduleName set =
Set.map
(\( moduleNameForType, name ) ->
case moduleNameForType of
[] ->
( moduleName, name )
_ ->
( moduleNameForType, name )
)
set
replaceLocalModuleNameForDict : ModuleName -> Dict ( ModuleName, comparable ) b -> Dict ( ModuleName, comparable ) b
replaceLocalModuleNameForDict moduleName dict =
Dict.foldl
(\( moduleNameForType, name ) value acc ->
case moduleNameForType of
[] ->
Dict.insert ( moduleName, name ) value acc
_ ->
Dict.insert ( moduleNameForType, name ) value acc
)
Dict.empty
dict
getNonExposedCustomTypes : ModuleContext -> Dict String (List Range)
getNonExposedCustomTypes moduleContext =
if moduleContext.isModuleExposed then
@ -248,6 +274,7 @@ foldProjectContexts newContext previousContext =
newContext.usedArguments
previousContext.usedArguments
Dict.empty
, customTypesNotToReport = Set.union newContext.customTypesNotToReport previousContext.customTypesNotToReport
}
@ -269,20 +296,42 @@ declarationListVisitor nodes context =
let
customTypeArgs : List ( String, Dict String (List Range) )
customTypeArgs =
List.filterMap collectCustomType nodes
List.filterMap (collectCustomType context.lookupTable) nodes
in
( [], { context | customTypeArgs = Dict.fromList customTypeArgs } )
collectCustomType : Node Declaration -> Maybe ( String, Dict String (List Range) )
collectCustomType node =
collectCustomType : ModuleNameLookupTable -> Node Declaration -> Maybe ( String, Dict String (List Range) )
collectCustomType lookupTable node =
case Node.value node of
Declaration.CustomTypeDeclaration typeDeclaration ->
let
customTypeConstructors : List ( String, List Range )
customTypeConstructors =
List.map
(Node.value
>> (\{ name, arguments } ->
( Node.value name
, arguments
|> List.filter
(\arg ->
case Node.value arg of
TypeAnnotation.Typed (Node _ ( _, "Never" )) [] ->
case ModuleNameLookupTable.moduleNameFor lookupTable arg of
Just [ "Basics" ] ->
False
_ ->
True
_ ->
True
)
|> List.map Node.range
)
)
)
typeDeclaration.constructors
|> List.map (Node.value >> (\{ name, arguments } -> ( Node.value name, List.map Node.range arguments )))
in
Just ( Node.value typeDeclaration.name, Dict.fromList customTypeConstructors )
@ -361,10 +410,77 @@ expressionVisitor node context =
}
)
Expression.OperatorApplication operator _ left right ->
if operator == "==" || operator == "/=" then
let
customTypesNotToReport : Set ( ModuleName, String )
customTypesNotToReport =
Set.union
(findCustomTypes context.lookupTable left)
(findCustomTypes context.lookupTable right)
in
( [], { context | customTypesNotToReport = Set.union customTypesNotToReport context.customTypesNotToReport } )
else
( [], context )
_ ->
( [], context )
findCustomTypes : ModuleNameLookupTable -> Node Expression -> Set ( ModuleName, String )
findCustomTypes lookupTable node =
case Node.value node of
Expression.FunctionOrValue rawModuleName functionName ->
if isCustomTypeConstructor functionName then
case ModuleNameLookupTable.moduleNameFor lookupTable node of
Just moduleName ->
Set.singleton ( moduleName, functionName )
Nothing ->
Set.singleton ( rawModuleName, functionName )
else
Set.empty
Expression.TupledExpression expressions ->
List.foldl (findCustomTypes lookupTable >> Set.union) Set.empty expressions
Expression.ParenthesizedExpression expression ->
findCustomTypes lookupTable expression
Expression.Application [] ->
Set.empty
Expression.Application (((Node _ (Expression.FunctionOrValue _ functionName)) as first) :: expressions) ->
if isCustomTypeConstructor functionName then
List.foldl (findCustomTypes lookupTable >> Set.union) Set.empty (first :: expressions)
else
Set.empty
Expression.OperatorApplication _ _ left right ->
Set.union
(findCustomTypes lookupTable left)
(findCustomTypes lookupTable right)
Expression.Negation expression ->
findCustomTypes lookupTable expression
Expression.ListExpr expressions ->
List.foldl (findCustomTypes lookupTable >> Set.union) Set.empty expressions
_ ->
Set.empty
isCustomTypeConstructor : String -> Bool
isCustomTypeConstructor functionName =
String.toList functionName
|> List.take 1
|> List.all Char.isUpper
registerUsedPatterns : List ( ( ModuleName, String ), Set Int ) -> Dict ( ModuleName, String ) (Set Int) -> Dict ( ModuleName, String ) (Set Int)
registerUsedPatterns newUsedArguments previouslyUsedArguments =
List.foldl
@ -452,12 +568,22 @@ finalEvaluation context =
|> Dict.toList
|> List.concatMap
(\( name, ranges ) ->
if Set.member ( moduleName, name ) context.customTypesNotToReport then
[]
else
case Dict.get ( moduleName, name ) context.usedArguments of
Just usedArgumentPositions ->
ranges
|> List.indexedMap Tuple.pair
|> List.filter (\( index, _ ) -> not <| Set.member index usedArgumentPositions)
|> List.map (Tuple.second >> error moduleKey)
|> List.filterMap
(\( index, range ) ->
if Set.member index usedArgumentPositions then
Nothing
else
Just (error moduleKey range)
)
Nothing ->
List.map (error moduleKey) ranges

View File

@ -5,6 +5,7 @@ import Json.Decode as Decode
import NoUnused.CustomTypeConstructorArgs exposing (rule)
import Review.Project as Project exposing (Project)
import Review.Test
import Review.Test.Dependencies
import Test exposing (Test, describe, test)
@ -384,12 +385,146 @@ type Msg
]
|> Review.Test.runOnModules rule
|> Review.Test.expectNoErrors
, test "should not report Never arguments" <|
\() ->
"""
module Main exposing (a)
a = 1
type CustomType
= B Never
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectNoErrors
, test "should not report Never arguments even when aliased" <|
\() ->
"""
module Main exposing (a)
import Basics as B
a = 1
type CustomType
= B B.Never
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectNoErrors
, test "should report arguments next to Never" <|
-- Honestly I'm unsure about doing this, but I currently don't see
-- the point of having other args next to a Never arg.
\() ->
"""
module Main exposing (a)
a = 1
type CustomType
= B SomeData Never
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "SomeData"
}
]
, test "should not report args for type constructors used in an equality expression (==)" <|
\() ->
"""
module MyModule exposing (a, b)
type Foo = Unused Int | B
a = Unused 0 == b
b = B
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectNoErrors
, test "should not report args for type constructors used in an inequality expression (/=)" <|
\() ->
"""
module MyModule exposing (a, b)
type Foo = Unused Int | B
a = Unused 0 /= b
b = B
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectNoErrors
, test "should report args for type constructors used in non-equality operator expressions" <|
\() ->
"""
module MyModule exposing (a, b)
type Foo = Unused Int | B
a = Unused <| b
b = B
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Int"
}
]
, test "should not report args for type constructors used in an equality expression with parenthesized expressions" <|
\() ->
"""
module MyModule exposing (a, b)
type Foo = Unused Int | B
a = ( Unused 0 ) == b
b = ( B )
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectNoErrors
, test "should not report args for type constructors used in an equality expression with tuples" <|
\() ->
"""
module MyModule exposing (a, b)
type Foo = Unused Int | B
a = ( Unused 0, Unused 1 ) == b
b = ( B, B )
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectNoErrors
, test "should not report args for type constructors used in an equality expression with lists" <|
\() ->
"""
module MyModule exposing (a, b)
type Foo = Unused Int | B
a = [ Unused 0 ] == b
b = [ B ]
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectNoErrors
, test "should not report args for type constructors used in an equality expression (==) in a different module" <|
\() ->
[ """
module MyModule exposing (a, b)
import Foo as F
a = F.Unused 0 == b
b = F.B
""", """
module Foo exposing (Foo(..))
type Foo = Unused Int | B
""" ]
|> Review.Test.runOnModulesWithProjectData packageProject rule
|> Review.Test.expectNoErrors
, test "should report args for type constructors used in an equality expression when value is passed to a function" <|
\() ->
"""
module MyModule exposing (a, b)
type Foo = Unused Int | B
a = foo (Unused 0) == b
b = B
"""
|> Review.Test.runWithProjectData packageProject rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Int"
}
]
]
packageProject : Project
packageProject =
Project.new
Review.Test.Dependencies.projectWithElmCore
|> Project.addElmJson (createElmJson packageElmJson)

View File

@ -504,8 +504,8 @@ declarationName declaration =
Declaration.CustomTypeDeclaration type_ ->
Just <| Node.value type_.name
Declaration.AliasDeclaration alias ->
Just <| Node.value alias.name
Declaration.AliasDeclaration alias_ ->
Just <| Node.value alias_.name
Declaration.PortDeclaration port_ ->
Just <| Node.value port_.name
@ -569,8 +569,8 @@ typesUsedInDeclaration moduleContext declaration =
False
)
Declaration.AliasDeclaration alias ->
( collectTypesFromTypeAnnotation moduleContext alias.typeAnnotation, False )
Declaration.AliasDeclaration alias_ ->
( collectTypesFromTypeAnnotation moduleContext alias_.typeAnnotation, False )
Declaration.PortDeclaration _ ->
( [], False )
@ -648,5 +648,24 @@ expressionVisitor node moduleContext =
Nothing ->
( [], moduleContext )
Expression.LetExpression { declarations } ->
let
used : List ( ModuleName, String )
used =
List.concatMap
(\declaration ->
case Node.value declaration of
Expression.LetFunction function ->
function.signature
|> Maybe.map (Node.value >> .typeAnnotation >> collectTypesFromTypeAnnotation moduleContext)
|> Maybe.withDefault []
Expression.LetDestructuring _ _ ->
[]
)
declarations
in
( [], registerMultipleAsUsed used moduleContext )
_ ->
( [], moduleContext )

View File

@ -47,8 +47,8 @@ applicationElmJson =
}
package : Project
package =
package_ : Project
package_ =
Project.new
|> Project.addElmJson (createPackageElmJson ())
@ -203,7 +203,7 @@ module A exposing (exposed)
exposed = 1
main = exposed
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Exposed function or value `exposed` is never used outside this module."
@ -219,7 +219,7 @@ module A exposing (exposed1, exposed2)
exposed1 = 1
exposed2 = 2
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Exposed function or value `exposed1` is never used outside this module."
@ -250,7 +250,7 @@ exposed2 = 2
module A exposing (..)
a = 1
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report the `main` function for an application even if it is unused" <|
\() ->
@ -266,7 +266,7 @@ main = text ""
module Main exposing (main)
main = text ""
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Exposed function or value `main` is never used outside this module."
@ -281,7 +281,7 @@ main = text ""
module A exposing (b)
a = 1
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report exposed tests" <|
\() ->
@ -291,7 +291,7 @@ import Test exposing (Test)
a : Test
a = Test.describe "thing" []
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not ReviewConfig.config" <|
\() ->
@ -299,7 +299,7 @@ a = Test.describe "thing" []
module ReviewConfig exposing (config)
config = []
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report a function in a 'shadowed' module" <|
\() ->
@ -360,7 +360,7 @@ import A
type alias B = A.ExposedB
type alias C = A.ExposedC
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed custom type if it's part of the package's exposed API" <|
\() ->
@ -368,7 +368,7 @@ type alias C = A.ExposedC
module Exposed exposing (MyType)
type MyType = VariantA | VariantB
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed custom type if it's present in the signature of an exposed function" <|
\() ->
@ -391,7 +391,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed custom type if it's present in an exposed type alias" <|
\() ->
@ -404,7 +404,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed custom type if it's present in an exposed type alias (nested)" <|
\() ->
@ -417,7 +417,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed custom type if it's present in an exposed custom type constructor's arguments" <|
\() ->
@ -430,7 +430,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed custom type if it's present in an exposed custom type constructor's arguments but the constructors are not exposed" <|
\() ->
@ -443,7 +443,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
@ -471,7 +471,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed custom type if it's present in an exposed custom type constructor's arguments (nested) but the constructors are not exposed" <|
\() ->
@ -484,7 +484,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
@ -501,6 +501,22 @@ type OtherType = OtherThing | SomeThing ((), List MyType)
]
)
]
, test "should not report an exposed type if it is used in a let block type annotation" <|
\() ->
[ """module Main exposing (main)
import B
main =
let
type1 : B.Type1
type1 =
1
in
type1
""", """module B exposing (Type1)
type Type1 = Type1
""" ]
|> Review.Test.runOnModulesWithProjectData application rule
|> Review.Test.expectNoErrors
]
@ -532,6 +548,34 @@ module B exposing (main)
import A
main : A.Exposed
main = {}
""" ]
|> Review.Test.runOnModulesWithProjectData application rule
|> Review.Test.expectNoErrors
, test "should not report an exposed type alias if it is used in a let block type annotation" <|
\() ->
[ """module Main exposing (main)
import B
main =
let
type1 : B.Type1
type1 =
1
in
type1
""", """module B exposing (Type1)
type alias Type1 = Int
""" ]
|> Review.Test.runOnModulesWithProjectData application rule
|> Review.Test.expectNoErrors
, test "should not report an exposed type if it is used as value in a let block" <|
\() ->
[ """module Main exposing (main)
import B
main =
let type1 = B.Type1
in type1
""", """module B exposing (Type1)
type alias Type1 = {}
""" ]
|> Review.Test.runOnModulesWithProjectData application rule
|> Review.Test.expectNoErrors
@ -545,7 +589,7 @@ module Exposed exposing (B)
import A
type alias B = A.ExposedB
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed type alias if it's part of the package's exposed API" <|
\() ->
@ -553,7 +597,7 @@ type alias B = A.ExposedB
module Exposed exposing (MyType)
type alias MyType = {}
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed type alias if it's present in the signature of an exposed function" <|
\() ->
@ -576,7 +620,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed type alias if it's present in an exposed type alias" <|
\() ->
@ -589,7 +633,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed type alias if it's present in an exposed type alias (nested)" <|
\() ->
@ -602,7 +646,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed type alias if it's present in an exposed custom type constructor's arguments" <|
\() ->
@ -615,7 +659,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should not report an unused exposed type alias if it's present in an exposed custom type constructor's arguments but the constructors are not exposed" <|
\() ->
@ -628,7 +672,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
@ -656,7 +700,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should report an unused exposed type alias if it's present in an exposed custom type constructor's arguments (nested) but the constructors are not exposed" <|
\() ->
@ -669,7 +713,7 @@ module Exposed exposing (..)
import A
type alias B = A.OtherType
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
@ -702,7 +746,7 @@ module Exposed exposing (..)
import A
a = A.Card A.init A.toElement
""" ]
|> Review.Test.runOnModulesWithProjectData package rule
|> Review.Test.runOnModulesWithProjectData package_ rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error

View File

@ -47,8 +47,8 @@ applicationElmJson =
}
package : Project
package =
package_ : Project
package_ =
Project.new
|> Project.addElmJson (createPackageElmJson ())
@ -203,7 +203,7 @@ config = []
module Exposed exposing (..)
a = 1
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectNoErrors
, test "should report non-exposed and non-used modules from a package" <|
\() ->
@ -211,7 +211,7 @@ a = 1
module NotExposed exposing (..)
a = 1
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Module `NotExposed` is never used."
@ -225,7 +225,7 @@ a = 1
module Reported exposing (main)
main = text ""
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Module `Reported` is never used."
@ -240,7 +240,7 @@ module Reported exposing (a)
main = text ""
a = 1
"""
|> Review.Test.runWithProjectData package rule
|> Review.Test.runWithProjectData package_ rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Module `Reported` is never used."

View File

@ -154,8 +154,7 @@ expressionEnterVisitorHelp node context =
Expression.CaseExpression { cases } ->
( []
, { context
| scopes = { declared = [], used = Set.empty } :: context.scopes
, scopesToCreate =
| scopesToCreate =
List.foldl
(\( pattern, expr ) scopesToCreate -> RangeDict.insert (Node.range expr) (findPatterns Matching pattern) scopesToCreate)
context.scopesToCreate

View File

@ -300,6 +300,37 @@ bar =
case bar of
_ ->
Nothing
"""
]
, test "should report patterns in nested case declarations" <|
\() ->
"""
module A exposing (..)
foo =
case thing of
Just data ->
case () of
_ -> 1
_ ->
bosh
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Value `data` is not used."
, details = useOrReplaceDetails
, under = "data"
}
|> Review.Test.whenFixed
"""
module A exposing (..)
foo =
case thing of
Just _ ->
case () of
_ -> 1
_ ->
bosh
"""
]
]

View File

@ -315,9 +315,14 @@ moduleDefinitionVisitor (Node _ moduleNode) context =
let
names : List String
names =
List.map
(\(Node _ node) ->
case node of
List.map getExposingName list
in
( [], markAllAsUsed names context )
getExposingName : Node Exposing.TopLevelExpose -> String
getExposingName node =
case Node.value node of
Exposing.FunctionExpose name ->
name
@ -329,10 +334,6 @@ moduleDefinitionVisitor (Node _ moduleNode) context =
Exposing.InfixExpose name ->
name
)
list
in
( [], markAllAsUsed names context )
importVisitor : Node Import -> ModuleContext -> ( List (Error {}), ModuleContext )
@ -363,12 +364,6 @@ importVisitor ((Node importRange import_) as node) context =
Just declaredImports ->
let
customTypesFromModule : Dict String (List String)
customTypesFromModule =
context.customTypes
|> Dict.get (Node.value import_.moduleName)
|> Maybe.withDefault Dict.empty
contextWithAlias : ModuleContext
contextWithAlias =
case import_.moduleAlias of
@ -381,6 +376,7 @@ importVisitor ((Node importRange import_) as node) context =
( errors
, case Node.value declaredImports of
Exposing.All _ ->
if Dict.member (Node.value import_.moduleName) context.customTypes then
{ contextWithAlias
| exposingAllModules =
{ name = Node.value import_.moduleName
@ -394,7 +390,17 @@ importVisitor ((Node importRange import_) as node) context =
:: context.exposingAllModules
}
else
contextWithAlias
Exposing.Explicit list ->
let
customTypesFromModule : Dict String (List String)
customTypesFromModule =
context.customTypes
|> Dict.get (Node.value import_.moduleName)
|> Maybe.withDefault Dict.empty
in
List.foldl
(registerExposedElements customTypesFromModule)
contextWithAlias
@ -403,27 +409,27 @@ importVisitor ((Node importRange import_) as node) context =
registerExposedElements : Dict String (List String) -> ExposedElement -> ModuleContext -> ModuleContext
registerExposedElements customTypesFromModule importedElement context_ =
registerExposedElements customTypesFromModule importedElement context =
case importedElement of
CustomType name variableInfo ->
case Dict.get name customTypesFromModule of
Just constructorNames ->
{ context_
| unusedImportedCustomTypes = Dict.insert name variableInfo context_.unusedImportedCustomTypes
{ context
| unusedImportedCustomTypes = Dict.insert name variableInfo context.unusedImportedCustomTypes
, importedCustomTypeLookup =
Dict.union
(constructorNames
|> List.map (\constructorName -> ( constructorName, name ))
|> Dict.fromList
)
context_.importedCustomTypeLookup
context.importedCustomTypeLookup
}
Nothing ->
context_
context
TypeOrValue name variableInfo ->
registerVariable variableInfo name context_
registerVariable variableInfo name context
collectExplicitlyExposedElements : Range -> List (Node Exposing.TopLevelExpose) -> List ExposedElement
@ -584,7 +590,7 @@ expressionEnterVisitorHelp (Node range value) context =
Expression.FunctionOrValue [] name ->
case Dict.get name context.constructorNameToTypeName of
Just typeName ->
( [], markAsUsed typeName context )
( [], markValueAsUsed typeName context )
Nothing ->
case Dict.get name context.importedCustomTypeLookup of
@ -596,12 +602,12 @@ expressionEnterVisitorHelp (Node range value) context =
Just realModuleName ->
( []
, context
|> markAsUsed name
|> markValueAsUsed name
|> markModuleAsUsed ( realModuleName, [] )
)
Nothing ->
( [], markAsUsed name context )
( [], markValueAsUsed name context )
Expression.FunctionOrValue moduleName _ ->
case ModuleNameLookupTable.moduleNameAt context.lookupTable range of
@ -612,10 +618,10 @@ expressionEnterVisitorHelp (Node range value) context =
( [], context )
Expression.OperatorApplication name _ _ _ ->
( [], markAsUsed name context )
( [], markValueAsUsed name context )
Expression.PrefixOperator name ->
( [], markAsUsed name context )
( [], markValueAsUsed name context )
Expression.LetExpression { declarations, expression } ->
let
@ -628,7 +634,7 @@ expressionEnterVisitorHelp (Node range value) context =
HasMultipleDeclarations
in
List.foldl
(\declaration (( errors, foldContext ) as unchangedResult) ->
(\declaration ( errors, foldContext ) ->
case Node.value declaration of
Expression.LetFunction function ->
let
@ -651,15 +657,15 @@ expressionEnterVisitorHelp (Node range value) context =
}
in
( errors
, foldContext
, List.foldl markValueAsUsed foldContext namesUsedInArgumentPatterns.types
|> markAllModulesAsUsed namesUsedInArgumentPatterns.modules
|> registerFunction letBlockContext function
|> markUsedTypesAndModules namesUsedInArgumentPatterns
|> markAsInTheDeclarationOf (function.declaration |> Node.value |> .name |> Node.value)
)
Expression.LetDestructuring pattern _ ->
case isAllPattern pattern of
Just wildCardRange ->
case removeParens pattern of
Node wildCardRange Pattern.AllPattern ->
( Rule.errorWithFix
{ message = "Value assigned to `_` is unused"
, details =
@ -667,21 +673,34 @@ expressionEnterVisitorHelp (Node range value) context =
]
}
wildCardRange
[ case letBlockContext of
HasMultipleDeclarations ->
Fix.removeRange (Node.range declaration)
HasNoOtherDeclarations letDeclarationsRange ->
-- If there are no other declarations in the let in block,
-- we also need to remove the `let in` keywords.
Fix.removeRange letDeclarationsRange
]
[ Fix.removeRange (letDeclarationToRemoveRange letBlockContext (Node.range declaration)) ]
:: errors
, foldContext
)
Nothing ->
unchangedResult
Node unitPattern Pattern.UnitPattern ->
( Rule.errorWithFix
{ message = "Unit value is unused"
, details =
[ "This value has no data, which makes the value unusable. You should remove it at the location I pointed at."
]
}
unitPattern
[ Fix.removeRange (letDeclarationToRemoveRange letBlockContext (Node.range declaration)) ]
:: errors
, foldContext
)
_ ->
let
namesUsedInPattern : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInPattern =
getUsedVariablesFromPattern context pattern
in
( []
, List.foldl markValueAsUsed foldContext namesUsedInPattern.types
|> markAllModulesAsUsed namesUsedInPattern.modules
)
)
( [], { context | scopes = NonemptyList.cons emptyScope context.scopes } )
declarations
@ -694,23 +713,35 @@ expressionEnterVisitorHelp (Node range value) context =
|> List.map (getUsedVariablesFromPattern context)
|> foldUsedTypesAndModules
in
( [], markUsedTypesAndModules namesUsedInArgumentPatterns context )
( []
, List.foldl markValueAsUsed context namesUsedInArgumentPatterns.types
|> markAllModulesAsUsed namesUsedInArgumentPatterns.modules
)
_ ->
( [], context )
isAllPattern : Node Pattern -> Maybe Range
isAllPattern node =
letDeclarationToRemoveRange : LetBlockContext -> Range -> Range
letDeclarationToRemoveRange letBlockContext range =
case letBlockContext of
HasMultipleDeclarations ->
range
HasNoOtherDeclarations letDeclarationsRange ->
-- If there are no other declarations in the let in block,
-- we also need to remove the `let in` keywords.
letDeclarationsRange
removeParens : Node Pattern -> Node Pattern
removeParens node =
case Node.value node of
Pattern.ParenthesizedPattern pattern ->
isAllPattern pattern
Pattern.AllPattern ->
Just (Node.range node)
removeParens pattern
_ ->
Nothing
node
expressionExitVisitor : Node Expression -> ModuleContext -> ( List (Error {}), ModuleContext )
@ -731,7 +762,7 @@ expressionExitVisitorHelp : Node Expression -> ModuleContext -> ( List (Error {}
expressionExitVisitorHelp node context =
case Node.value node of
Expression.RecordUpdateExpression expr _ ->
( [], markAsUsed (Node.value expr) context )
( [], markValueAsUsed (Node.value expr) context )
Expression.CaseExpression { cases } ->
let
@ -745,7 +776,8 @@ expressionExitVisitorHelp node context =
|> foldUsedTypesAndModules
in
( []
, markUsedTypesAndModules usedVariables context
, List.foldl markValueAsUsed context usedVariables.types
|> markAllModulesAsUsed usedVariables.modules
)
Expression.LetExpression _ ->
@ -922,15 +954,24 @@ declarationListVisitor nodes context =
)
Declaration.AliasDeclaration { name, documentation } ->
let
contextWithRemovedShadowedImports : ModuleContext
contextWithRemovedShadowedImports =
{ context | importedCustomTypeLookup = Dict.remove (Node.value name) context.importedCustomTypeLookup }
in
( []
, registerVariable
, if context.exposesEverything then
contextWithRemovedShadowedImports
else
registerVariable
{ typeName = "Type"
, under = Node.range name
, rangeToRemove = Just (rangeToRemoveForNodeWithDocumentation node documentation)
, warning = ""
}
(Node.value name)
{ context | importedCustomTypeLookup = Dict.remove (Node.value name) context.importedCustomTypeLookup }
contextWithRemovedShadowedImports
)
_ ->
@ -944,7 +985,7 @@ declarationListVisitor nodes context =
-- DECLARATION VISITOR
declarationVisitor : Node Declaration -> ModuleContext -> ( List nothing, ModuleContext )
declarationVisitor : Node Declaration -> ModuleContext -> ( List (Error {}), ModuleContext )
declarationVisitor node context =
case Node.value node of
Declaration.FunctionDeclaration function ->
@ -953,6 +994,10 @@ declarationVisitor node context =
functionImplementation =
Node.value function.declaration
functionName : String
functionName =
Node.value functionImplementation.name
namesUsedInSignature : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInSignature =
function.signature
@ -972,7 +1017,7 @@ declarationVisitor node context =
if
context.exposesEverything
-- The main function is "exposed" by default for applications
|| (context.isApplication && Node.value functionImplementation.name == "main")
|| (context.isApplication && functionName == "main")
then
context
@ -983,16 +1028,31 @@ declarationVisitor node context =
, rangeToRemove = Just (rangeToRemoveForNodeWithDocumentation node function.documentation)
, warning = ""
}
(Node.value functionImplementation.name)
functionName
context
newContext : ModuleContext
newContext =
{ newContextWhereFunctionIsRegistered | inTheDeclarationOf = [ Node.value functionImplementation.name ], declarations = Dict.empty }
|> markUsedTypesAndModules namesUsedInSignature
|> markUsedTypesAndModules namesUsedInArgumentPatterns
{ newContextWhereFunctionIsRegistered | inTheDeclarationOf = [ functionName ], declarations = Dict.empty }
|> (\ctx -> List.foldl markAsUsed ctx namesUsedInSignature.types)
|> (\ctx -> List.foldl markValueAsUsed ctx namesUsedInArgumentPatterns.types)
|> markAllModulesAsUsed namesUsedInSignature.modules
|> markAllModulesAsUsed namesUsedInArgumentPatterns.modules
shadowingImportError : List (Error {})
shadowingImportError =
case Dict.get functionName (NonemptyList.head context.scopes).declared of
Just existingVariable ->
if existingVariable.typeName == "Imported variable" then
[ error existingVariable functionName ]
else
[]
_ ->
[]
in
( [], newContext )
( shadowingImportError, newContext )
Declaration.CustomTypeDeclaration { name, constructors } ->
let
@ -1003,11 +1063,10 @@ declarationVisitor node context =
|> foldUsedTypesAndModules
in
( []
, markUsedTypesAndModules
{ types = List.filter ((/=) (Node.value name)) types
, modules = modules
}
context
, types
|> List.filter ((/=) (Node.value name))
|> List.foldl markAsUsed context
|> markAllModulesAsUsed modules
)
Declaration.AliasDeclaration { name, typeAnnotation } ->
@ -1017,7 +1076,8 @@ declarationVisitor node context =
collectNamesFromTypeAnnotation context.lookupTable typeAnnotation
in
( []
, markUsedTypesAndModules namesUsedInTypeAnnotation context
, List.foldl markAsUsed context namesUsedInTypeAnnotation.types
|> markAllModulesAsUsed namesUsedInTypeAnnotation.modules
)
Declaration.PortDeclaration { name, typeAnnotation } ->
@ -1025,23 +1085,31 @@ declarationVisitor node context =
namesUsedInTypeAnnotation : { types : List String, modules : List ( ModuleName, ModuleName ) }
namesUsedInTypeAnnotation =
collectNamesFromTypeAnnotation context.lookupTable typeAnnotation
contextWithUsedElements : ModuleContext
contextWithUsedElements =
List.foldl markAsUsed context namesUsedInTypeAnnotation.types
|> markAllModulesAsUsed namesUsedInTypeAnnotation.modules
in
( []
, context
|> markUsedTypesAndModules namesUsedInTypeAnnotation
|> registerVariable
, if context.exposesEverything then
contextWithUsedElements
else
registerVariable
{ typeName = "Port"
, under = Node.range name
, rangeToRemove = Nothing
, warning = " (Warning: Removing this port may break your application if it is used in the JS code)"
}
(Node.value name)
contextWithUsedElements
)
Declaration.InfixDeclaration { operator, function } ->
( []
, context
|> markAsUsed (Node.value function)
|> markValueAsUsed (Node.value function)
|> registerVariable
{ typeName = "Declared operator"
, under = Node.range operator
@ -1060,13 +1128,6 @@ foldUsedTypesAndModules =
List.foldl (\a b -> { types = a.types ++ b.types, modules = a.modules ++ b.modules }) { types = [], modules = [] }
markUsedTypesAndModules : { types : List String, modules : List ( ModuleName, ModuleName ) } -> ModuleContext -> ModuleContext
markUsedTypesAndModules { types, modules } context =
context
|> markAllAsUsed types
|> markAllModulesAsUsed modules
rangeToRemoveForNodeWithDocumentation : Node Declaration -> Maybe (Node a) -> Range
rangeToRemoveForNodeWithDocumentation (Node nodeRange _) documentation =
case documentation of
@ -1231,6 +1292,10 @@ finalEvaluation context =
customTypeErrors : List (Error {})
customTypeErrors =
if context.exposesEverything then
[]
else
context.localCustomTypes
|> Dict.toList
|> List.filter (\( name, _ ) -> not <| Set.member name usedLocally)
@ -1282,23 +1347,15 @@ registerFunction letBlockContext function context =
Nothing ->
Node.range function.declaration
in
context
List.foldl markAsUsed context namesUsedInSignature.types
|> markAllModulesAsUsed namesUsedInSignature.modules
|> registerVariable
{ typeName = "`let in` variable"
, under = Node.range declaration.name
, rangeToRemove =
case letBlockContext of
HasMultipleDeclarations ->
Just functionRange
HasNoOtherDeclarations letDeclarationsRange ->
-- If there are no other declarations in the let in block,
-- we also need to remove the `let in` keywords.
Just letDeclarationsRange
, rangeToRemove = Just (letDeclarationToRemoveRange letBlockContext functionRange)
, warning = ""
}
(Node.value declaration.name)
|> markUsedTypesAndModules namesUsedInSignature
type ExposedElement
@ -1421,13 +1478,22 @@ markAllAsUsed names context =
List.foldl markAsUsed context names
markAsUsed : String -> ModuleContext -> ModuleContext
markAsUsed name context =
case ( Dict.get name <| context.importedCustomTypeLookup, Dict.get name context.constructorNameToTypeName ) of
( Just customTypeName, Nothing ) ->
markValueAsUsed : String -> ModuleContext -> ModuleContext
markValueAsUsed name context =
if Dict.member name context.constructorNameToTypeName then
markAsUsed name context
else
case Dict.get name context.importedCustomTypeLookup of
Just customTypeName ->
{ context | unusedImportedCustomTypes = Dict.remove customTypeName context.unusedImportedCustomTypes }
_ ->
markAsUsed name context
markAsUsed : String -> ModuleContext -> ModuleContext
markAsUsed name context =
if List.member name context.inTheDeclarationOf then
context
@ -1602,14 +1668,9 @@ positionAsInt { row, column } =
comparePosition : { row : Int, column : Int } -> { row : Int, column : Int } -> Order
comparePosition a b =
let
order : Order
order =
compare a.row b.row
in
case order of
case compare a.row b.row of
EQ ->
compare a.column b.column
_ ->
order ->
order

View File

@ -188,13 +188,25 @@ b = 2"""
|> Review.Test.whenFixed """module SomeModule exposing (b)
b = 2"""
]
, test "should not report unused top-level variables if everything is exposed" <|
, test "should not report unused top-level variables if everything is exposed (functions)" <|
\() ->
"""module SomeModule exposing (..)
a n = 1
b = a 1"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report unused top-level variables if everything is exposed (custom types)" <|
\() ->
"""module SomeModule exposing (..)
type A = A"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report unused top-level variables if everything is exposed (type aliases)" <|
\() ->
"""module SomeModule exposing (..)
type alias A = ()"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report unused top-level variables that are exposed by name" <|
\() ->
"""module SomeModule exposing (a, b)
@ -437,6 +449,23 @@ a = let _ = 1
, under = "_"
}
|> Review.Test.whenFixed """module SomeModule exposing (a)
a = 2"""
]
, test "should report () destructuring" <|
\() ->
"""module SomeModule exposing (a)
a = let () = b
in 2"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Unit value is unused"
, details =
[ "This value has no data, which makes the value unusable. You should remove it at the location I pointed at."
]
, under = "()"
}
|> Review.Test.whenFixed """module SomeModule exposing (a)
a = 2"""
]
, test "should report parenthesized wildcard assignments" <|
@ -743,6 +772,18 @@ import Html.Styled.Attributes
a = Html.Styled.Attributes.href"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report used import (let destructuring)" <|
\() ->
[ """module SomeModule exposing (a)
import B
a = let (B.B ()) = x
in 1
"""
, """module B exposing (B)
type B = B ()"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectNoErrors
, test "should not report unused import if it is aliased" <|
\() ->
"""module SomeModule exposing (a)
@ -1225,6 +1266,20 @@ a = 1"""
]
)
]
, test "should not report import if it exposes all and its contents are unknown" <|
\() ->
"""module SomeModule exposing (a)
import Unknown exposing (..)
a = 1"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report imported type if it exposes the constructors and the module is unknown" <|
\() ->
"""module SomeModule exposing (a)
import Unknown exposing (A(..))
a = 1"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should report unused import from dependency that exposes everything" <|
\() ->
"""module SomeModule exposing (..)
@ -1374,6 +1429,77 @@ b = 1
]
|> Review.Test.runOnModules rule
|> Review.Test.expectNoErrors
, test "should report unused imported value if it is redefined" <|
\() ->
[ """module A exposing (a)
import Used exposing (shadowed)
shadowed = 1
a = shadowed"""
, """module Used exposing (shadowed)
shadowed = 1
"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
{ message = "Imported variable `shadowed` is not used"
, details = details
, under = "shadowed"
}
|> Review.Test.atExactly { start = { row = 2, column = 23 }, end = { row = 2, column = 31 } }
|> Review.Test.whenFixed ("""module A exposing (a)
import Used$
shadowed = 1
a = shadowed""" |> String.replace "$" " ")
]
)
]
, test "should report unused imported value if it is redefined, and should not report the top-level one even if used before declaration" <|
\() ->
[ """module A exposing (a)
import Used exposing (shadowed)
a = shadowed
shadowed = 1"""
, """module Used exposing (shadowed)
shadowed = 1
"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectErrorsForModules
[ ( "A"
, [ Review.Test.error
{ message = "Imported variable `shadowed` is not used"
, details = details
, under = "shadowed"
}
|> Review.Test.atExactly { start = { row = 2, column = 23 }, end = { row = 2, column = 31 } }
|> Review.Test.whenFixed ("""module A exposing (a)
import Used$
a = shadowed
shadowed = 1""" |> String.replace "$" " ")
]
)
]
, test "should not report imported type as unused when it's used in a type annotation, and the name conflicts with an imported custom type constructor" <|
\() ->
[ """module Main exposing (thing, main)
import ModuleA exposing (A)
import ModuleB exposing (Variants(..))
thing : Variants
thing = A
main : A
main = ()
"""
, """module ModuleA exposing (A)
type alias A = ()"""
, """module ModuleB exposing (Variants(..))
type Variants = A"""
]
|> Review.Test.runOnModules rule
|> Review.Test.expectNoErrors
]
@ -1934,6 +2060,22 @@ a = output ()
subscriptions = input GotInput"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report exposed ports" <|
\() ->
"""port module SomeModule exposing (output, input)
import Json.Decode
port output : () -> Cmd msg
port input : (Json.Decode.Value -> msg) -> Sub msg"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report exposed ports using (..)" <|
\() ->
"""port module SomeModule exposing (..)
import Json.Decode
port output : () -> Cmd msg
port input : (Json.Decode.Value -> msg) -> Sub msg"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should report unused ports (ingoing)" <|
\() ->
"""port module SomeModule exposing (a)