Add rule NoUnusedTypeConstructors

This commit is contained in:
Jeroen Engels 2019-07-04 23:57:55 +02:00
parent f771ef1540
commit 3f6cc7c3e7
5 changed files with 302 additions and 1 deletions

View File

@ -46,6 +46,7 @@ These are the rules that are built-in and available for you to choose from.
- **NoExtraBooleanComparison** - Forbid extraneous comparisons with booleans, like `(isAdmin user) == True`.
- **NoImportingEverything** - Forbid importing everything from your module. This can especially be confusing to newcomers when the exposed functions and types are unknown to them.
- **NoUnusedVariables** - Reports variables that are declared but never used.
- **NoUnusedTypeConstructors** - Reports type constructors that are declared but never used.
The following is a list of rules that were temporarily removed when changing the AST implementation, and which can potentially be re-added later.
- **NoExposingEverything** - Forbid exporting everything in your modules `module Main exposing (..)`, to make your module explicit in what it exposes.
@ -70,10 +71,11 @@ module LintConfig exposing (config)
import Lint exposing (Severity(..))
import Lint.Rule exposing (Rule)
import Lint.Rule.DefaultPatternPosition as DefaultPatternPosition
import Lint.Rule.NoUnusedVariables
import Lint.Rule.NoDebug
import Lint.Rule.NoExtraBooleanComparison
import Lint.Rule.NoImportingEverything
import Lint.Rule.NoUnusedTypeConstructors
import Lint.Rule.NoUnusedVariables
config : List ( Severity, Rule )
@ -81,6 +83,7 @@ config =
[ ( Critical, DefaultPatternPosition.rule DefaultPatternPosition.ShouldBeLast )
, ( Critical, Lint.Rule.NoExtraBooleanComparison.rule )
, ( Critical, Lint.Rule.NoUnusedVariables.rule )
, ( Critical, Lint.Rule.NoUnusedTypeConstructors.rule )
, ( Warning, Lint.Rule.NoDebug.rule )
, ( Critical, Lint.Rule.NoDuplicateImports.rule )
, ( Critical, Lint.Rule.NoImportingEverything.rule { exceptions = [ "Html" ] } )

View File

@ -11,6 +11,7 @@
"Lint.Rule.NoDebug",
"Lint.Rule.NoExtraBooleanComparison",
"Lint.Rule.NoImportingEverything",
"Lint.Rule.NoUnusedTypeConstructors",
"Lint.Rule.NoUnusedVariables",
"Lint.Test"
],

View File

@ -10,6 +10,7 @@ import Lint.Rule.DefaultPatternPosition as DefaultPatternPosition exposing (Patt
import Lint.Rule.NoDebug
import Lint.Rule.NoExtraBooleanComparison
import Lint.Rule.NoImportingEverything
import Lint.Rule.NoUnusedTypeConstructors
import Lint.Rule.NoUnusedVariables
@ -24,6 +25,7 @@ config model =
, ( model.noImportingEverythingEnabled, ( Critical, Lint.Rule.NoImportingEverything.rule { exceptions = [ "Html" ] } ) )
, ( model.defaultPatternPositionEnabled, ( Critical, DefaultPatternPosition.rule model.defaultPatternPositionPattern ) )
, ( model.noExtraBooleanComparisonEnabled, ( Critical, Lint.Rule.NoExtraBooleanComparison.rule ) )
, ( model.noUnusedTypeConstructorsEnabled, ( Critical, Lint.Rule.NoUnusedTypeConstructors.rule ) )
-- , ( Critical, Lint.Rule.NoConstantCondition.rule )
-- , ( Critical, Lint.Rule.NoDuplicateImports.rule )
@ -55,6 +57,7 @@ type alias Model =
, defaultPatternPositionEnabled : Bool
, defaultPatternPositionPattern : PatternPosition
, noExtraBooleanComparisonEnabled : Bool
, noUnusedTypeConstructorsEnabled : Bool
, showConfigurationAsText : Bool
}
@ -88,6 +91,7 @@ g n = n + 1
, defaultPatternPositionEnabled = True
, defaultPatternPositionPattern = DefaultPatternPosition.ShouldBeLast
, noExtraBooleanComparisonEnabled = True
, noUnusedTypeConstructorsEnabled = True
, showConfigurationAsText = False
}
in
@ -105,6 +109,7 @@ type Msg
| UserToggledNoImportingEverythingRule
| UserToggledDefaultPatternPositionRule
| UserToggledNoExtraBooleanComparisonRule
| UserToggledNoUnusedTypeConstructorsRule
| UserChangedDefaultPatternSetting PatternPosition
| UserToggledConfigurationAsText
@ -142,6 +147,10 @@ update action model =
{ model | noExtraBooleanComparisonEnabled = not model.noExtraBooleanComparisonEnabled }
|> rerunLinting
UserToggledNoUnusedTypeConstructorsRule ->
{ model | noUnusedTypeConstructorsEnabled = not model.noUnusedTypeConstructorsEnabled }
|> rerunLinting
UserToggledConfigurationAsText ->
{ model | showConfigurationAsText = not model.showConfigurationAsText }
@ -210,6 +219,7 @@ viewConfigurationPanel model =
model.defaultPatternPositionPattern
]
, viewCheckbox UserToggledNoExtraBooleanComparisonRule "NoExtraBooleanComparison" model.noExtraBooleanComparisonEnabled
, viewCheckbox UserToggledNoUnusedTypeConstructorsRule "NoUnusedTypeConstructors" model.noUnusedTypeConstructorsEnabled
]
]
@ -280,6 +290,11 @@ configurationAsText model =
, configExpression = "Lint.Rule.NoExtraBooleanComparison.rule"
}
)
, ( model.noUnusedTypeConstructorsEnabled
, { import_ = "Lint.Rule.NoUnusedTypeConstructors"
, configExpression = "Lint.Rule.NoUnusedTypeConstructors.rule"
}
)
]
|> List.filter Tuple.first
|> List.map Tuple.second

View File

@ -0,0 +1,210 @@
module Lint.Rule.NoUnusedTypeConstructors exposing (rule)
{-| Forbid having unused custom type constructors in a file.
Note that this does not report a constructor if it's exposed in the module, even
if it is not used anywhere in the project.
## Fail
-- module A exposing (a)
type MyType
= UsedType
| UnusedType -- Will get reported
a =
UsedType
## Success
-- module A exposing (ExposedType(..))
type MyType
= UsedType
a =
UsedType
type ExposedType
= A
| B
| C
-----------------------
-- module A exposing (..)
type ExposedType
= A
| B
| C
# Rule
@docs rule
-}
import Dict exposing (Dict)
import Elm.Syntax.Declaration exposing (Declaration(..))
import Elm.Syntax.Exposing exposing (Exposing(..), TopLevelExpose(..))
import Elm.Syntax.Expression exposing (Expression(..), Function, FunctionImplementation, LetDeclaration(..))
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Module as Module exposing (Module(..))
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Elm.Syntax.TypeAnnotation exposing (TypeAnnotation(..))
import Lint.Rule as Rule exposing (Direction, Error, Rule)
import List.Nonempty as Nonempty exposing (Nonempty)
import Set exposing (Set)
{-| Forbid having unused custom type constructors in a file.
Note that this does not report a constructor if it's exposed in the module, even
if it is not used anywhere in the project.
config =
[ ( Critical, NoUnusedTypeConstructors.rule )
]
-}
rule : Rule
rule =
Rule.newSchema "NoUnusedTypeConstructors"
|> Rule.withInitialContext initialContext
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withDeclarationVisitor declarationVisitor
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.withFinalEvaluation finalEvaluation
|> Rule.fromSchema
type alias Context =
{ exposedCustomTypesWithConstructors : Set String
, exposesEverything : Bool
, declaredTypesWithConstructors : Dict String (Node String)
, usedFunctionOrValues : Set String
}
initialContext : Context
initialContext =
{ exposedCustomTypesWithConstructors = Set.empty
, exposesEverything = False
, declaredTypesWithConstructors = Dict.empty
, usedFunctionOrValues = Set.empty
}
error : Node String -> Error
error node =
Rule.error
("Type constructor `" ++ Node.value node ++ "` is not used.")
(Node.range node)
moduleDefinitionVisitor : Node Module -> Context -> ( List Error, Context )
moduleDefinitionVisitor moduleNode context =
case Module.exposingList (Node.value moduleNode) of
All _ ->
( [], { context | exposesEverything = True } )
Explicit list ->
let
names : List String
names =
List.filterMap
(\node ->
case Node.value node of
TypeExpose { name, open } ->
case open of
Just _ ->
Just name
Nothing ->
Nothing
_ ->
Nothing
)
list
in
( []
, { context
| exposedCustomTypesWithConstructors =
Set.union (Set.fromList names) context.exposedCustomTypesWithConstructors
}
)
declarationVisitor : Node Declaration -> Direction -> Context -> ( List Error, Context )
declarationVisitor node direction context =
case ( direction, Node.value node ) of
( Rule.OnEnter, CustomTypeDeclaration { name, constructors } ) ->
if Set.member (Node.value name) context.exposedCustomTypesWithConstructors then
( [], context )
else
let
newContext : Context
newContext =
List.foldl
(\constructor ctx ->
let
nameNode : Node String
nameNode =
(Node.value constructor).name
in
{ ctx
| declaredTypesWithConstructors =
Dict.insert
(Node.value nameNode)
nameNode
ctx.declaredTypesWithConstructors
}
)
context
constructors
in
( [], newContext )
_ ->
( [], context )
expressionVisitor : Node Expression -> Direction -> Context -> ( List Error, Context )
expressionVisitor node direction context =
if context.exposesEverything then
( [], context )
else
case ( direction, Node.value node ) of
( Rule.OnEnter, FunctionOrValue [] name ) ->
( [], { context | usedFunctionOrValues = Set.insert name context.usedFunctionOrValues } )
_ ->
( [], context )
finalEvaluation : Context -> List Error
finalEvaluation context =
if context.exposesEverything then
[]
else
context.declaredTypesWithConstructors
|> Dict.filter (\name node -> not <| Set.member name context.usedFunctionOrValues)
|> Dict.toList
|> List.map (\( _, node ) -> error node)
-- if context.exposesEverything then
-- []
--
-- else
-- context.scopes
-- |> Nonempty.head
-- |> makeReport
-- |> Tuple.first

View File

@ -0,0 +1,72 @@
module NoUnusedTypeConstructorsTest exposing (all)
import Lint.Rule.NoUnusedTypeConstructors exposing (rule)
import Lint.Test exposing (LintResult)
import Test exposing (Test, describe, test)
testRule : String -> LintResult
testRule =
Lint.Test.run rule
tests : List Test
tests =
[ test "should not report non-exposed variables" <|
\() ->
testRule """module A exposing (b)
a = 1"""
|> Lint.Test.expectNoErrors
, test "should not report used type constructors" <|
\() ->
testRule """module A exposing (b)
type Foo = Bar | Baz
a = Bar
b = Baz"""
|> Lint.Test.expectNoErrors
, test "should not report unused type constructors when module is exposing all" <|
\() ->
testRule """module A exposing (..)
type Foo = Bar | Baz
"""
|> Lint.Test.expectNoErrors
, test "should not report unused type constructors when module is exposing the constructors of that type" <|
\() ->
testRule """module A exposing (Foo(..))
type Foo = Bar | Baz
"""
|> Lint.Test.expectNoErrors
, test "should report unused type constructors" <|
\() ->
testRule """module A exposing (b)
type Foo = Bar | Baz"""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = "Type constructor `Bar` is not used."
, under = "Bar"
}
, Lint.Test.error
{ message = "Type constructor `Baz` is not used."
, under = "Baz"
}
]
, test "should report unused type constructors, even if the type is exposed" <|
\() ->
testRule """module A exposing (Foo)
type Foo = Bar | Baz"""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = "Type constructor `Bar` is not used."
, under = "Bar"
}
, Lint.Test.error
{ message = "Type constructor `Baz` is not used."
, under = "Baz"
}
]
]
all : Test
all =
describe "NoUnusedTypeConstructors" tests