Backport rules from other packages

This commit is contained in:
Jeroen Engels 2020-08-09 18:55:15 +02:00
parent 7a9246e4fa
commit 449b21addd
53 changed files with 5407 additions and 1535 deletions

View File

@ -33,6 +33,15 @@ and publishing the package. Otherwise the link for a given version could link to
**NOTE**: A similar rule would be useful for links inside the modules. I'll be working on that too!
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-documentation/example --rules NoUselessSubscriptions
```
-}
rule : Rule
rule =

View File

@ -1,43 +0,0 @@
module MiscRules.NoBooleanCase exposing (rule)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Pattern as Pattern
import Elm.Syntax.Range exposing (Range)
import Review.Rule as Rule exposing (Error, Rule)
rule : Rule
rule =
Rule.newModuleRuleSchema "NoBooleanCase" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
expressionVisitor : Node Expression -> List (Error {})
expressionVisitor (Node.Node range expression) =
case expression of
Expression.CaseExpression caseBlock ->
errorsForCaseBlock range caseBlock
_ ->
[]
errorsForCaseBlock : Range -> Expression.CaseBlock -> List (Error {})
errorsForCaseBlock range { cases } =
case cases of
[ ( Node.Node _ (Pattern.NamedPattern { moduleName, name } []), _ ), _ ] ->
if moduleName == [] && (name == "True" || name == "False") then
[ Rule.error
{ message = "Matching boolean values in a case .. of expression"
, details = [ "It's quite silly" ]
}
range
]
else
[]
_ ->
[]

View File

@ -1,31 +0,0 @@
module MiscRules.NoBooleanCaseTest exposing (all)
import MiscRules.NoBooleanCase as NoBooleanCase
import Review.Test
import Test exposing (Test, describe, test)
all : Test
all =
describe "noBooleanCase"
[ test "Simple one" <|
\_ ->
"""module A exposing (..)
a =
case expr of
True -> True
False -> False
"""
|> Review.Test.run NoBooleanCase.rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Matching boolean values in a case .. of expression"
, details =
[ "It's quite silly"
]
, under = """case expr of
True -> True
False -> False"""
}
]
]

View File

@ -1,103 +0,0 @@
module NoBooleanCaseOf exposing (rule)
{-|
@docs rule
-}
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Review.Rule as Rule exposing (Error, Rule)
{-| Reports when pattern matching is used for a boolean value.
The idiomatic way to check for a condition is to use an `if` expression.
Read more about it at: <https://guide.elm-lang.org/core_language.html#if-expressions>
config =
[ NoBooleanCaseOf.rule
]
This won't report pattern matching when a boolean is part of the evaluated value.
## Fail
_ =
case bool of
True ->
expression
False ->
otherExpression
## Success
_ =
if bool then
expression
else
otherExpression
_ =
case ( bool, somethingElse ) of
( True, SomeThingElse ) ->
expression
_ ->
otherExpression
# When (not) to use this rule
You should not use this rule if you do not care about how your boolean values are
evaluated.
-}
rule : Rule
rule =
Rule.newModuleRuleSchema "NoBooleanCaseOf" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
expressionVisitor : Node Expression -> List (Error {})
expressionVisitor node =
case Node.value node of
Expression.CaseExpression { expression, cases } ->
if List.any (Tuple.first >> isBoolConstructor) cases then
[ error expression ]
else
[]
_ ->
[]
error : Node a -> Error {}
error node =
Rule.error
{ message = "Replace `case..of` by an `if` condition"
, details =
[ "The idiomatic way to check for a condition is to use an `if` expression."
, "Read more about it at: https://guide.elm-lang.org/core_language.html#if-expressions"
]
}
(Node.range node)
isBoolConstructor : Node Pattern -> Bool
isBoolConstructor node =
case Node.value node of
Pattern.NamedPattern { moduleName, name } _ ->
(name == "True" || name == "False")
&& (moduleName == [] || moduleName == [ "Basics" ])
_ ->
False

View File

@ -1,111 +0,0 @@
module NoBooleanCaseOfTest exposing (all)
import NoBooleanCaseOf exposing (rule)
import Review.Test exposing (ReviewResult)
import Test exposing (Test, describe, test)
testRule : String -> ReviewResult
testRule string =
"module A exposing (..)\n\n"
++ string
|> Review.Test.run rule
message : String
message =
"Replace `case..of` by an `if` condition"
details : List String
details =
[ "The idiomatic way to check for a condition is to use an `if` expression."
, "Read more about it at: https://guide.elm-lang.org/core_language.html#if-expressions"
]
tests : List Test
tests =
[ test "should not report pattern matches for non-boolean values" <|
\() ->
testRule """
a = case thing of
Thing -> 1"""
|> Review.Test.expectNoErrors
, test "should not report pattern matches when the evaluated expression is a tuple of with a boolean" <|
\() ->
testRule """
a = case ( bool1, bool2 ) of
( True, True ) -> 1
_ -> 2"""
|> Review.Test.expectNoErrors
, test "should report pattern matches when one of the patterns is a bool constructor" <|
\() ->
testRule """
a = case boolTrue of
True -> 1
_ -> 2
b = case boolFalse of
False -> 1
_ -> 2
c = case boolAll of
False -> 1
True -> 2"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "boolTrue"
}
, Review.Test.error
{ message = message
, details = details
, under = "boolFalse"
}
, Review.Test.error
{ message = message
, details = details
, under = "boolAll"
}
]
, test "should report pattern matches for booleans even when one of the patterns starts with `Basics.`" <|
\() ->
testRule """
a = case boolTrue of
Basics.True -> 1
_ -> 2
b = case boolFalse of
Basics.False -> 1
_ -> 2"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "boolTrue"
}
, Review.Test.error
{ message = message
, details = details
, under = "boolFalse"
}
]
, test "should report pattern matches for booleans even when the constructor seems to be for booleans but comes from an unknown module" <|
\() ->
testRule """
a = case boolTrue of
OtherModule.True -> 1
_ -> 2
b = case boolFalse of
OtherModule.False -> 1
_ -> 2"""
|> Review.Test.expectNoErrors
]
all : Test
all =
describe "NoBooleanCaseOf" tests

View File

@ -51,10 +51,19 @@ You should not use this rule if you are developing an application that is not
put into production, and you do not care about having stray debug logs, and you
do not ship to production.
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-debug/example --rules NoDebug.Log
```
-}
rule : Rule
rule =
Rule.newModuleRuleSchema "NoDebugLog" { hasLogBeenImported = False }
Rule.newModuleRuleSchema "NoDebug.Log" { hasLogBeenImported = False }
|> Rule.withImportVisitor importVisitor
|> Rule.withExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema

View File

@ -23,265 +23,261 @@ details =
]
tests : List Test
tests =
[ test "should not report normal function calls" <|
\() ->
testRule """
all : Test
all =
describe "NoDebug.Log"
[ test "should not report normal function calls" <|
\() ->
testRule """
a = foo n
b = bar.foo n
c = debug
e = debug.log n
d = debug.log n
"""
|> Review.Test.expectNoErrors
, test "should not report Debug.todo or Debug.toString calls" <|
\() ->
testRule """
|> Review.Test.expectNoErrors
, test "should not report Debug.todo or Debug.toString calls" <|
\() ->
testRule """
a = Debug.toString n
b = Debug.todo ""
"""
|> Review.Test.expectNoErrors
, test "should report Debug.log use" <|
\() ->
testRule "a = Debug.log"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log calls" <|
\() ->
testRule "a = Debug.log z"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report multiple Debug.log calls" <|
\() ->
testRule """
|> Review.Test.expectNoErrors
, test "should report Debug.log use" <|
\() ->
testRule "a = Debug.log"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log calls" <|
\() ->
testRule "a = Debug.log z"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report multiple Debug.log calls" <|
\() ->
testRule """
a = Debug.log z
b = Debug.log z
"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
|> Review.Test.atExactly { start = { row = 4, column = 5 }, end = { row = 4, column = 14 } }
, Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
|> Review.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 14 } }
]
, test "should report Debug.log in a binary expression" <|
\() ->
testRule "a = ( Debug.log z ) + 2"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in a << binary expression" <|
\() ->
testRule "a = fn << Debug.log"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in a pipe expression" <|
\() ->
testRule "a = fn |> Debug.log z"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an list expression" <|
\() ->
testRule "a = [ Debug.log z y ]"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an record expression" <|
\() ->
testRule "a = { foo = Debug.log z y }"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an record update expression" <|
\() ->
testRule "a = { model | foo = Debug.log z y }"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an lambda expression" <|
\() ->
testRule "a = (\\foo -> Debug.log z foo)"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an if expression condition" <|
\() ->
testRule "a = if Debug.log a b then True else False"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an if expression then branch" <|
\() ->
testRule "a = if True then Debug.log a b else False"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an if expression else branch" <|
\() ->
testRule "a = if True then True else Debug.log a b"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in a case value" <|
\() ->
testRule """
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
|> Review.Test.atExactly { start = { row = 4, column = 5 }, end = { row = 4, column = 14 } }
, Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
|> Review.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 14 } }
]
, test "should report Debug.log in a binary expression" <|
\() ->
testRule "a = ( Debug.log z ) + 2"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in a << binary expression" <|
\() ->
testRule "a = fn << Debug.log"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in a pipe expression" <|
\() ->
testRule "a = fn |> Debug.log z"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an list expression" <|
\() ->
testRule "a = [ Debug.log z y ]"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an record expression" <|
\() ->
testRule "a = { foo = Debug.log z y }"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an record update expression" <|
\() ->
testRule "a = { model | foo = Debug.log z y }"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an lambda expression" <|
\() ->
testRule "a = (\\foo -> Debug.log z foo)"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an if expression condition" <|
\() ->
testRule "a = if Debug.log a b then True else False"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an if expression then branch" <|
\() ->
testRule "a = if True then Debug.log a b else False"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in an if expression else branch" <|
\() ->
testRule "a = if True then True else Debug.log a b"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in a case value" <|
\() ->
testRule """
a = case Debug.log a b of
_ -> []
"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in a case body" <|
\() ->
testRule """
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in a case body" <|
\() ->
testRule """
a = case a of
_ -> Debug.log a b
"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in let declaration section" <|
\() ->
testRule """
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in let declaration section" <|
\() ->
testRule """
a = let b = Debug.log a b
in b
"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in let body" <|
\() ->
testRule """
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should report Debug.log in let body" <|
\() ->
testRule """
a = let b = c
in Debug.log a b
"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should not report calls from a module containing Debug but that is not Debug" <|
\() ->
testRule """
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Debug.log"
}
]
, test "should not report calls from a module containing Debug but that is not Debug" <|
\() ->
testRule """
a = Foo.Debug.log 1
b = Debug.Foo.log 1
"""
|> Review.Test.expectNoErrors
, test "should not report the import of the Debug module" <|
\() ->
testRule "import Debug"
|> Review.Test.expectNoErrors
, test "should report the use of `log` when it has been explicitly imported" <|
\() ->
testRule """
|> Review.Test.expectNoErrors
, test "should not report the import of the Debug module" <|
\() ->
testRule "import Debug"
|> Review.Test.expectNoErrors
, test "should report the use of `log` when it has been explicitly imported" <|
\() ->
testRule """
import Debug exposing (log)
a = log "" 1
"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "log"
}
|> Review.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 8 } }
]
, test "should report the use of `log` when it has been implicitly imported" <|
\() ->
testRule """
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "log"
}
|> Review.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 8 } }
]
, test "should report the use of `log` when it has been implicitly imported" <|
\() ->
testRule """
import Debug exposing (..)
a = log "" 1
"""
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "log"
}
]
, test "should not report the use of `log` when it has not been imported" <|
\() ->
testRule """
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "log"
}
]
, test "should not report the use of `log` when it has not been imported" <|
\() ->
testRule """
import Debug exposing (todo)
a = log "" 1
"""
|> Review.Test.expectNoErrors
]
all : Test
all =
describe "NoDebugLog" tests
|> Review.Test.expectNoErrors
]

View File

@ -68,6 +68,15 @@ can configure the rule like this.
else
b
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-debug/example --rules NoDebug.TodoOrToString
```
[`Debug.log`]: https://package.elm-lang.org/packages/elm/core/latest/Debug#log
[`Debug.todo`]: https://package.elm-lang.org/packages/elm/core/latest/Debug#todo
[`Debug.toString`]: https://package.elm-lang.org/packages/elm/core/latest/Debug#toString

View File

@ -1,16 +1,15 @@
module NoDebug.TodoOrToStringTest exposing (all)
import Elm.Project
import Json.Decode as Decode
import NoDebug.TodoOrToString exposing (rule)
import Review.Project
import Review.Test
import Review.Test exposing (ReviewResult)
import Test exposing (Test, describe, test)
all : Test
all =
describe "NoDebug.TodoOrToString" tests
testRule : String -> ReviewResult
testRule string =
"module A exposing (..)\n\n"
++ string
|> Review.Test.run rule
todoMessage : String
@ -35,11 +34,12 @@ toStringDetails =
]
tests : List Test
tests =
[ test "should not report normal function calls" <|
\() ->
"""module A exposing (..)
all : Test
all =
describe "NoDebug.TodoOrToString"
[ test "should not report normal function calls" <|
\() ->
testRule """
a = foo n
b = bar.foo n
c = debug
@ -48,204 +48,112 @@ c = List.toString
d = debug.todo n
e = debug.toString n
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report Debug.log calls" <|
\() ->
"""module A exposing (..)
|> Review.Test.expectNoErrors
, test "should not report Debug.log calls" <|
\() ->
testRule """
a = Debug.log n
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should report Debug.todo use" <|
\() ->
"""module A exposing (..)
a = Debug.todo"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = todoMessage
, details = todoDetails
, under = "Debug.todo"
}
]
, test "should report Debug.toString use" <|
\() ->
"""module A exposing (..)
a = Debug.toString"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = toStringMessage
, details = toStringDetails
, under = "Debug.toString"
}
]
, test "should not report calls from a module containing Debug but that is not Debug" <|
\() ->
"""module A exposing (..)
|> Review.Test.expectNoErrors
, test "should report Debug.todo use" <|
\() ->
testRule "a = Debug.todo"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = todoMessage
, details = todoDetails
, under = "Debug.todo"
}
]
, test "should report Debug.toString use" <|
\() ->
testRule "a = Debug.toString"
|> Review.Test.expectErrors
[ Review.Test.error
{ message = toStringMessage
, details = toStringDetails
, under = "Debug.toString"
}
]
, test "should not report calls from a module containing Debug but that is not Debug" <|
\() ->
testRule """
a = Foo.Debug.todo 1
b = Debug.Foo.todo 1
a = Foo.Debug.toString 1
b = Debug.Foo.toString 1
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report the import of the Debug module" <|
\() ->
"""module A exposing (..)
import Debug"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should report the use of `todo` when `todo` has been explicitly imported" <|
\() ->
"""module A exposing (..)
"""
|> Review.Test.expectNoErrors
, test "should not report the import of the Debug module" <|
\() ->
testRule "import Debug"
|> Review.Test.expectNoErrors
, test "should report the use of `todo` when `todo` has been explicitly imported" <|
\() ->
testRule """
import Debug exposing (todo)
a = todo ""
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = todoMessage
, details = todoDetails
, under = "todo"
}
|> Review.Test.atExactly { start = { row = 3, column = 5 }, end = { row = 3, column = 9 } }
]
, test "should report the use of `todo` when `todo` has been implicitly imported" <|
\() ->
"""module A exposing (..)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = todoMessage
, details = todoDetails
, under = "todo"
}
|> Review.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 9 } }
]
, test "should report the use of `todo` when `todo` has been implicitly imported" <|
\() ->
testRule """
import Debug exposing (..)
a = todo "" 1
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = todoMessage
, details = todoDetails
, under = "todo"
}
]
, test "should not report the use of `todo` when it has not been imported" <|
\() ->
"""module A exposing (..)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = todoMessage
, details = todoDetails
, under = "todo"
}
]
, test "should not report the use of `todo` when it has not been imported" <|
\() ->
testRule """
import Debug exposing (log)
a = todo "" 1
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should report the use of `toString` when `toString` has been explicitly imported" <|
\() ->
"""module A exposing (..)
|> Review.Test.expectNoErrors
, test "should report the use of `toString` when `toString` has been explicitly imported" <|
\() ->
testRule """
import Debug exposing (toString)
a = toString ""
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = toStringMessage
, details = toStringDetails
, under = "toString"
}
|> Review.Test.atExactly { start = { row = 3, column = 5 }, end = { row = 3, column = 13 } }
]
, test "should report the use of `toString` when `toString` has been implicitly imported" <|
\() ->
"""module A exposing (..)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = toStringMessage
, details = toStringDetails
, under = "toString"
}
|> Review.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 13 } }
]
, test "should report the use of `toString` when `toString` has been implicitly imported" <|
\() ->
testRule """
import Debug exposing (..)
a = toString "" 1
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = toStringMessage
, details = toStringDetails
, under = "toString"
}
]
, test "should not report the use of `toString` when it has not been imported" <|
\() ->
"""module A exposing (..)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = toStringMessage
, details = toStringDetails
, under = "toString"
}
]
, test "should not report the use of `toString` when it has not been imported" <|
\() ->
testRule """
import Debug exposing (log)
a = toString "" 1
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report the use of `toString` or `todo` in files outside the source directories (TODO only with flag)" <|
\() ->
let
project : Review.Project.Project
project =
Review.Project.addModule { path = "tests/FooTest.elm", source = """module A exposing (..)
a = Debug.todo Debug.toString""" } applicationProject
in
Review.Test.runOnModulesWithProjectData project rule []
|> Review.Test.expectNoErrors
, test "should report the use of `toString` or `todo` in files inside the source directories (TODO only with flag)" <|
\() ->
let
project : Review.Project.Project
project =
Review.Project.addModule { path = "src/Foo.elm", source = """module A exposing (..)
a = Debug.todo Debug.toString""" } applicationProject
in
Review.Test.runOnModulesWithProjectData project rule []
|> Review.Test.expectErrors
[ Review.Test.error
{ message = todoMessage
, details = todoDetails
, under = "Debug.todo"
}
, Review.Test.error
{ message = toStringMessage
, details = toStringDetails
, under = "Debug.toString"
}
]
]
applicationProject : Review.Project.Project
applicationProject =
Review.Project.new
|> withDebugTodoElmJson Debug.todo rawApplicationElmJson
rawApplicationElmJson : String
rawApplicationElmJson =
"""
{
"type": "package",
"name": "author/dependency",
"summary": "Summary",
"license": "MIT",
"version": "1.0.0",
"exposed-modules": [
"Foo"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"elm/core": "1.0.0 <= v < 2.0.0"
},
"test-dependencies": {}
}
"""
withDebugTodoElmJson : (String -> Never) -> String -> Review.Project.Project -> Review.Project.Project
withDebugTodoElmJson debugTodo rawElmJson project =
case Decode.decodeString Elm.Project.decoder rawElmJson of
Ok elmJsonProject ->
Review.Project.addElmJson
{ path = "elm.json"
, raw = rawElmJson
, project = elmJsonProject
}
project
Err _ ->
let
_ =
debugTodo "Invalid elm.json supplied to test"
in
withDebugTodoElmJson debugTodo rawElmJson project
|> Review.Test.expectNoErrors
]

View File

@ -40,6 +40,15 @@ in the following manner:
module A exposing (B(..), C, d)
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-common/example --rules NoExposingEverything
```
-}
rule : Rule
rule =

View File

@ -9,7 +9,7 @@ all : Test
all =
describe "NoExposingEverything"
[ test "should not report anything when a module exposes a limited set of things" <|
\_ ->
\() ->
"""
module A exposing (B(..), C, d)
type B = B
@ -18,7 +18,7 @@ d = 1
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should report when a module exposes everything" <|
\_ ->
\() ->
"""
module A exposing (..)
import B exposing (..)

View File

@ -36,6 +36,15 @@ import Review.Rule as Rule exposing (Error, Rule)
_ =
(+)
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/elm-review-simplification/example --rules NoFullyAppliedPrefixOperator
```
-}
rule : Rule
rule =

View File

@ -44,6 +44,15 @@ you can configure a list of exceptions.
-- If configured with `[ "Html" ]`
import Html exposing (..)
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-common/example --rules NoImportingEverything
```
-}
rule : List String -> Rule
rule exceptions =
@ -73,7 +82,7 @@ importVisitor exceptions node =
Just (Exposing.All range) ->
[ Rule.error
{ message = "Prefer listing what you wish to import and/or using qualified imports"
, details = [ "When you import everything from a module, it becomes harder to know where a function or a type comes from" ]
, details = [ "When you import everything from a module it becomes harder to know where a function or a type comes from." ]
}
{ start = { row = range.start.row, column = range.start.column - 1 }
, end = { row = range.end.row, column = range.end.column + 1 }

View File

@ -39,7 +39,7 @@ import Html exposing (..)
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Prefer listing what you wish to import and/or using qualified imports"
, details = [ "When you import everything from a module, it becomes harder to know where a function or a type comes from" ]
, details = [ "When you import everything from a module it becomes harder to know where a function or a type comes from." ]
, under = "(..)"
}
]

View File

@ -0,0 +1,96 @@
module NoInconsistentAliases exposing
( rule, config, noMissingAliases
, Config
)
{-|
@docs rule, config, noMissingAliases
@docs Config
-}
import NoInconsistentAliases.Config as Config
import NoInconsistentAliases.Visitor as Visitor
import Review.Rule exposing (Rule)
{-| Ensure consistent use of import aliases throughout your project.
config : List Rule
config =
[ NoInconsistentAliases.config
[ ( "Html.Attributes", "Attr" )
]
|> NoInconsistentAliases.rule
]
-}
rule : Config -> Rule
rule =
Visitor.rule
{-| Provide a list of preferred names to be enforced. If we find any of the given modules imported with a different alias we will report them.
NoInconsistentAliases.config
[ ( "Html.Attributes", "Attr" )
, ( "Json.Decode", "Decode" )
, ( "Json.Encode", "Encode" )
]
|> NoInconsistentAliases.rule
-}
config : List ( String, String ) -> Config
config =
Config.config
{-| Ensure that imports are aliased if a module is used to qualify a function or type, and has a known alias.
NoInconsistentAliases.config
[ ( "Html.Attributes", "Attr" )
]
|> NoInconsistentAliases.noMissingAliases
|> NoInconsistentAliases.rule
## Failure
Here `Html.Attributes` has been used to call `class` and the preferred alias has not been used.
import Html.Attributes
view children =
div [ Html.Attributes.class "container" ] children
## Success
Here `Html.Attributes` has been aliased to `Attr` as expected.
import Html.Attributes as Attr
view children =
div [ Attr.class "container" ] children
## Success
Here `class` has been exposed so the alias is not needed.
import Html.Attributes exposing (class)
view children =
div [ class "container" ] children
-}
noMissingAliases : Config -> Config
noMissingAliases =
Config.noMissingAliases
{-| Configuration for the NoInconsistentAliases rule.
-}
type alias Config =
Config.Config

View File

@ -0,0 +1,56 @@
module NoInconsistentAliases.BadAlias exposing (BadAlias, mapExpectedName, mapModuleName, mapName, mapUses, new, range, withModuleUse)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Range exposing (Range)
import NoInconsistentAliases.ModuleUse exposing (ModuleUse)
type BadAlias
= BadAlias
{ name : String
, moduleName : ModuleName
, expectedName : String
, at : Range
, uses : List ModuleUse
}
new : { name : String, moduleName : ModuleName, expectedName : String, range : Range } -> BadAlias
new options =
BadAlias
{ name = options.name
, moduleName = options.moduleName
, expectedName = options.expectedName
, at = options.range
, uses = []
}
withModuleUse : ModuleUse -> BadAlias -> BadAlias
withModuleUse moduleUse (BadAlias alias) =
BadAlias { alias | uses = moduleUse :: alias.uses }
range : BadAlias -> Range
range (BadAlias alias) =
alias.at
mapName : (String -> a) -> BadAlias -> a
mapName mapper (BadAlias alias) =
mapper alias.name
mapModuleName : (ModuleName -> a) -> BadAlias -> a
mapModuleName mapper (BadAlias alias) =
mapper alias.moduleName
mapExpectedName : (String -> a) -> BadAlias -> a
mapExpectedName mapper (BadAlias alias) =
mapper alias.expectedName
mapUses : (ModuleUse -> a) -> BadAlias -> List a
mapUses mapper (BadAlias { uses }) =
List.map mapper uses

View File

@ -0,0 +1,38 @@
module NoInconsistentAliases.BadAliasSet exposing (BadAliasSet, empty, fold, insert, use)
import Dict exposing (Dict)
import NoInconsistentAliases.BadAlias as BadAlias exposing (BadAlias)
import NoInconsistentAliases.ModuleUse exposing (ModuleUse)
type BadAliasSet
= BadAliasSet (Dict String BadAlias)
empty : BadAliasSet
empty =
BadAliasSet Dict.empty
insert : BadAlias -> BadAliasSet -> BadAliasSet
insert badAlias (BadAliasSet aliases) =
BadAliasSet (BadAlias.mapName (\name -> Dict.insert name badAlias aliases) badAlias)
use : String -> ModuleUse -> BadAliasSet -> BadAliasSet
use name moduleUse (BadAliasSet aliases) =
case Dict.get name aliases of
Nothing ->
BadAliasSet aliases
Just badAlias ->
let
badAliasWithUse =
BadAlias.withModuleUse moduleUse badAlias
in
BadAliasSet (Dict.insert name badAliasWithUse aliases)
fold : (BadAlias -> a -> a) -> a -> BadAliasSet -> a
fold folder start (BadAliasSet aliases) =
aliases |> Dict.values |> List.foldl folder start

View File

@ -0,0 +1,56 @@
module NoInconsistentAliases.Config exposing
( Config, config, noMissingAliases
, canMissAliases, lookupAlias
)
{-|
@docs Config, config, noMissingAliases
@docs canMissAliases, lookupAlias
-}
import Dict exposing (Dict)
import Elm.Syntax.ModuleName exposing (ModuleName)
type Config
= Config
{ aliases : Dict ModuleName String
, allowMissingAliases : Bool
}
config : List ( String, String ) -> Config
config aliases =
Config
{ aliases =
aliases
|> List.map (Tuple.mapFirst toModuleName)
|> Dict.fromList
, allowMissingAliases = True
}
noMissingAliases : Config -> Config
noMissingAliases (Config cfg) =
Config { cfg | allowMissingAliases = False }
canMissAliases : Config -> Bool
canMissAliases (Config cfg) =
cfg.allowMissingAliases
lookupAlias : Config -> ModuleName -> Maybe String
lookupAlias (Config { aliases }) moduleName =
Dict.get moduleName aliases
--- HELPERS
toModuleName : String -> ModuleName
toModuleName moduleName =
String.split "." moduleName

View File

@ -0,0 +1,105 @@
module NoInconsistentAliases.Context exposing
( Module, initial
, addModuleAlias, lookupModuleName
, addMissingAlias, foldMissingAliases
, addBadAlias, foldBadAliases
, addModuleCall
)
{-|
@docs Module, initial
@docs addModuleAlias, lookupModuleName
@docs addMissingAlias, foldMissingAliases
@docs addBadAlias, foldBadAliases
@docs addModuleCall
-}
import Dict exposing (Dict)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Range exposing (Range)
import NoInconsistentAliases.BadAlias exposing (BadAlias)
import NoInconsistentAliases.BadAliasSet as BadAliasSet exposing (BadAliasSet)
import NoInconsistentAliases.MissingAlias exposing (MissingAlias)
import NoInconsistentAliases.MissingAliasSet as MissingAliasSet exposing (MissingAliasSet)
import NoInconsistentAliases.ModuleUse as ModuleUse exposing (ModuleUse)
type Module
= Module
{ aliases : Dict String ModuleName
, badAliases : BadAliasSet
, missingAliases : MissingAliasSet
}
initial : Module
initial =
Module
{ aliases = Dict.empty
, badAliases = BadAliasSet.empty
, missingAliases = MissingAliasSet.empty
}
addModuleAlias : ModuleName -> String -> Module -> Module
addModuleAlias moduleName moduleAlias (Module context) =
Module { context | aliases = Dict.insert moduleAlias moduleName context.aliases }
addBadAlias : BadAlias -> Module -> Module
addBadAlias badAlias (Module context) =
Module { context | badAliases = BadAliasSet.insert badAlias context.badAliases }
addMissingAlias : MissingAlias -> Module -> Module
addMissingAlias missingAlias (Module context) =
Module { context | missingAliases = MissingAliasSet.insert missingAlias context.missingAliases }
addModuleCall : ModuleName -> String -> Range -> Module -> Module
addModuleCall moduleName function range context =
let
moduleUse =
ModuleUse.new function range
in
context
|> useBadAliasCall moduleName moduleUse
|> useMissingAliasCall moduleName moduleUse
useBadAliasCall : ModuleName -> ModuleUse -> Module -> Module
useBadAliasCall moduleName moduleUse (Module context) =
case moduleName of
[ moduleAlias ] ->
Module
{ context
| badAliases = BadAliasSet.use moduleAlias moduleUse context.badAliases
}
_ ->
Module context
useMissingAliasCall : ModuleName -> ModuleUse -> Module -> Module
useMissingAliasCall moduleName moduleUse (Module context) =
Module
{ context
| missingAliases = MissingAliasSet.use moduleName moduleUse context.missingAliases
}
foldBadAliases : (BadAlias -> a -> a) -> a -> Module -> a
foldBadAliases folder start (Module { badAliases }) =
BadAliasSet.fold folder start badAliases
lookupModuleName : Module -> String -> Maybe ModuleName
lookupModuleName (Module { aliases }) moduleAlias =
Dict.get moduleAlias aliases
foldMissingAliases : (MissingAlias -> a -> a) -> a -> Module -> a
foldMissingAliases folder start (Module { missingAliases }) =
MissingAliasSet.fold folder start missingAliases

View File

@ -0,0 +1,54 @@
module NoInconsistentAliases.MissingAlias exposing (MissingAlias, hasUses, mapExpectedName, mapModuleName, mapUses, new, range, withModuleUse)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Range exposing (Range)
import NoInconsistentAliases.ModuleUse exposing (ModuleUse)
type MissingAlias
= MissingAlias
{ moduleName : ModuleName
, expectedName : String
, at : Range
, uses : List ModuleUse
}
new : ModuleName -> String -> Range -> MissingAlias
new newModuleName newExpectedName newRange =
MissingAlias
{ moduleName = newModuleName
, expectedName = newExpectedName
, at = newRange
, uses = []
}
withModuleUse : ModuleUse -> MissingAlias -> MissingAlias
withModuleUse moduleUse (MissingAlias alias) =
MissingAlias { alias | uses = moduleUse :: alias.uses }
hasUses : MissingAlias -> Bool
hasUses (MissingAlias { uses }) =
uses /= []
mapModuleName : (ModuleName -> a) -> MissingAlias -> a
mapModuleName mapper (MissingAlias { moduleName }) =
mapper moduleName
mapExpectedName : (String -> a) -> MissingAlias -> a
mapExpectedName mapper (MissingAlias { expectedName }) =
mapper expectedName
mapUses : (ModuleUse -> a) -> MissingAlias -> List a
mapUses mapper (MissingAlias { uses }) =
List.map mapper uses
range : MissingAlias -> Range
range (MissingAlias { at }) =
at

View File

@ -0,0 +1,39 @@
module NoInconsistentAliases.MissingAliasSet exposing (MissingAliasSet, empty, fold, insert, use)
import Dict exposing (Dict)
import Elm.Syntax.ModuleName exposing (ModuleName)
import NoInconsistentAliases.MissingAlias as MissingAlias exposing (MissingAlias)
import NoInconsistentAliases.ModuleUse exposing (ModuleUse)
type MissingAliasSet
= MissingAliasSet (Dict ModuleName MissingAlias)
empty : MissingAliasSet
empty =
MissingAliasSet Dict.empty
insert : MissingAlias -> MissingAliasSet -> MissingAliasSet
insert missingAlias (MissingAliasSet aliases) =
MissingAliasSet (MissingAlias.mapModuleName (\name -> Dict.insert name missingAlias aliases) missingAlias)
use : ModuleName -> ModuleUse -> MissingAliasSet -> MissingAliasSet
use moduleName moduleUse (MissingAliasSet aliases) =
case Dict.get moduleName aliases of
Nothing ->
MissingAliasSet aliases
Just missingAlias ->
let
missingAliasWithUse =
MissingAlias.withModuleUse moduleUse missingAlias
in
MissingAliasSet (Dict.insert moduleName missingAliasWithUse aliases)
fold : (MissingAlias -> a -> a) -> a -> MissingAliasSet -> a
fold folder start (MissingAliasSet aliases) =
aliases |> Dict.values |> List.foldl folder start

View File

@ -0,0 +1,22 @@
module NoInconsistentAliases.ModuleUse exposing (ModuleUse, mapFunction, new, range)
import Elm.Syntax.Range exposing (Range)
type ModuleUse
= ModuleUse String Range
new : String -> Range -> ModuleUse
new newFunction newRange =
ModuleUse newFunction newRange
mapFunction : (String -> a) -> ModuleUse -> a
mapFunction mapper (ModuleUse name _) =
mapper name
range : ModuleUse -> Range
range (ModuleUse _ useRange) =
useRange

View File

@ -0,0 +1,233 @@
module NoInconsistentAliases.Visitor exposing (rule)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node(..))
import NoInconsistentAliases.BadAlias as BadAlias exposing (BadAlias)
import NoInconsistentAliases.Config exposing (Config)
import NoInconsistentAliases.Context as Context
import NoInconsistentAliases.MissingAlias as MissingAlias exposing (MissingAlias)
import NoInconsistentAliases.ModuleUse as ModuleUse exposing (ModuleUse)
import NoInconsistentAliases.Visitor.Options as Options exposing (Options)
import Review.Fix as Fix exposing (Fix)
import Review.Rule as Rule exposing (Error, Rule)
import Vendor.NameVisitor as NameVisitor
rule : Config -> Rule
rule config =
let
options : Options
options =
Options.fromConfig config
in
Rule.newModuleRuleSchema "NoInconsistentAliases" Context.initial
|> Rule.withImportVisitor (importVisitor options)
|> NameVisitor.withNameVisitor moduleCallVisitor
|> Rule.withFinalModuleEvaluation (finalEvaluation options.lookupAlias)
|> Rule.fromModuleRuleSchema
importVisitor : Options -> Node Import -> Context.Module -> ( List (Error {}), Context.Module )
importVisitor options (Node _ { moduleName, moduleAlias }) context =
( []
, context
|> rememberModuleAlias moduleName moduleAlias
|> rememberBadAlias options moduleName moduleAlias
)
rememberModuleAlias : Node ModuleName -> Maybe (Node ModuleName) -> Context.Module -> Context.Module
rememberModuleAlias moduleName maybeModuleAlias context =
let
moduleAlias =
maybeModuleAlias |> Maybe.withDefault moduleName |> Node.map formatModuleName
in
context |> Context.addModuleAlias (Node.value moduleName) (Node.value moduleAlias)
rememberBadAlias : Options -> Node ModuleName -> Maybe (Node ModuleName) -> Context.Module -> Context.Module
rememberBadAlias { lookupAlias, canMissAliases } (Node moduleNameRange moduleName) maybeModuleAlias context =
case ( lookupAlias moduleName, maybeModuleAlias ) of
( Just expectedAlias, Just (Node moduleAliasRange moduleAlias) ) ->
if [ expectedAlias ] /= moduleAlias then
let
badAlias =
BadAlias.new
{ name = moduleAlias |> formatModuleName
, moduleName = moduleName
, expectedName = expectedAlias
, range = moduleAliasRange
}
in
context |> Context.addBadAlias badAlias
else
context
( Just expectedAlias, Nothing ) ->
if canMissAliases then
context
else
let
missingAlias =
MissingAlias.new moduleName expectedAlias moduleNameRange
in
context |> Context.addMissingAlias missingAlias
( Nothing, _ ) ->
context
moduleCallVisitor : Node ( ModuleName, String ) -> Context.Module -> ( List (Error {}), Context.Module )
moduleCallVisitor node context =
case Node.value node of
( moduleName, function ) ->
( [], Context.addModuleCall moduleName function (Node.range node) context )
finalEvaluation : Options.AliasLookup -> Context.Module -> List (Error {})
finalEvaluation lookupAlias context =
let
lookupModuleName =
Context.lookupModuleName context
in
Context.foldBadAliases (foldBadAliasError lookupAlias lookupModuleName) [] context
++ Context.foldMissingAliases foldMissingAliasError [] context
foldBadAliasError : Options.AliasLookup -> ModuleNameLookup -> BadAlias -> List (Error {}) -> List (Error {})
foldBadAliasError lookupAlias lookupModuleName badAlias errors =
let
moduleName =
badAlias |> BadAlias.mapModuleName identity
expectedAlias =
badAlias |> BadAlias.mapExpectedName identity
moduleClash =
detectCollision (lookupModuleName expectedAlias) moduleName
aliasClash =
moduleClash |> Maybe.andThen lookupAlias
in
case ( aliasClash, moduleClash ) of
( Just _, _ ) ->
errors
( Nothing, Just collisionName ) ->
Rule.error (collisionAliasMessage collisionName expectedAlias badAlias) (BadAlias.range badAlias)
:: errors
( Nothing, Nothing ) ->
let
badRange =
BadAlias.range badAlias
fixes =
Fix.replaceRangeBy badRange expectedAlias
:: BadAlias.mapUses (fixModuleUse expectedAlias) badAlias
in
Rule.errorWithFix (incorrectAliasMessage expectedAlias badAlias) badRange fixes
:: errors
foldMissingAliasError : MissingAlias -> List (Error {}) -> List (Error {})
foldMissingAliasError missingAlias errors =
if MissingAlias.hasUses missingAlias then
let
expectedAlias =
missingAlias |> MissingAlias.mapExpectedName identity
badRange =
MissingAlias.range missingAlias
fixes =
Fix.insertAt badRange.end (" as " ++ expectedAlias)
:: MissingAlias.mapUses (fixModuleUse expectedAlias) missingAlias
in
Rule.errorWithFix (missingAliasMessage expectedAlias missingAlias) badRange fixes
:: errors
else
errors
detectCollision : Maybe ModuleName -> ModuleName -> Maybe ModuleName
detectCollision maybeCollisionName moduleName =
maybeCollisionName
|> Maybe.andThen
(\collisionName ->
if collisionName == moduleName then
Nothing
else
Just collisionName
)
incorrectAliasMessage : String -> BadAlias -> { message : String, details : List String }
incorrectAliasMessage expectedAlias badAlias =
let
badAliasName =
BadAlias.mapName identity badAlias
moduleName =
BadAlias.mapModuleName formatModuleName badAlias
in
{ message =
"Incorrect alias `" ++ badAliasName ++ "` for module `" ++ moduleName ++ "`."
, details =
[ "This import does not use your preferred alias `" ++ expectedAlias ++ "` for `" ++ moduleName ++ "`."
, "You should update the alias to be consistent with the rest of the project. Remember to change all references to the alias in this module too."
]
}
collisionAliasMessage : ModuleName -> String -> BadAlias -> { message : String, details : List String }
collisionAliasMessage collisionName expectedAlias badAlias =
let
badAliasName =
BadAlias.mapName identity badAlias
moduleName =
BadAlias.mapModuleName formatModuleName badAlias
in
{ message =
"Incorrect alias `" ++ badAliasName ++ "` for module `" ++ moduleName ++ "`."
, details =
[ "This import does not use your preferred alias `" ++ expectedAlias ++ "` for `" ++ moduleName ++ "`."
, "Your preferred alias has already been taken by `" ++ formatModuleName collisionName ++ "`."
, "You should change the alias for both modules to be consistent with the rest of the project. Remember to change all references to the alias in this module too."
]
}
missingAliasMessage : String -> MissingAlias -> { message : String, details : List String }
missingAliasMessage expectedAlias missingAlias =
let
moduleName =
MissingAlias.mapModuleName formatModuleName missingAlias
in
{ message =
"Expected alias `" ++ expectedAlias ++ "` missing for module `" ++ moduleName ++ "`."
, details =
[ "This import does not use your preferred alias `" ++ expectedAlias ++ "` for `" ++ moduleName ++ "`."
, "You should update the alias to be consistent with the rest of the project. Remember to change all references to the alias in this module too."
]
}
fixModuleUse : String -> ModuleUse -> Fix
fixModuleUse expectedAlias use =
Fix.replaceRangeBy (ModuleUse.range use) (ModuleUse.mapFunction (\name -> expectedAlias ++ "." ++ name) use)
formatModuleName : ModuleName -> String
formatModuleName moduleName =
String.join "." moduleName
type alias ModuleNameLookup =
String -> Maybe ModuleName

View File

@ -0,0 +1,21 @@
module NoInconsistentAliases.Visitor.Options exposing (AliasLookup, Options, fromConfig)
import Elm.Syntax.ModuleName exposing (ModuleName)
import NoInconsistentAliases.Config as Config exposing (Config)
type alias Options =
{ lookupAlias : AliasLookup
, canMissAliases : Bool
}
type alias AliasLookup =
ModuleName -> Maybe String
fromConfig : Config -> Options
fromConfig config =
{ lookupAlias = Config.lookupAlias config
, canMissAliases = Config.canMissAliases config
}

View File

@ -1,174 +0,0 @@
module NoLeftPizza exposing (rule, Strictness(..))
{-|
@docs rule, Strictness
-}
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)
import NoLeftPizzaUtil
import Review.Fix as Fix
import Review.Rule as Rule exposing (Error, Rule)
{-| Specify how strict the rule should be.
Specifying `Any` means that _any_ use of `<|` will be flagged, whereas
`Redundant` limits it to cases where `<|` can be removed - without adding any
parenthesis - without changing the semantics.
-}
type Strictness
= Any
| Redundant
{-| Forbids using the left pizza operator (<|) in infix position.
Expressions like `foo <| "hello" ++ world` will be flagged, and a fix will be
proposed to write the expression to `foo ("hello" ++ world)`.
To use this rule, add it to your `elm-review` config like so:
import NoLeftPizza
import Review.Rule exposing (Rule)
config : List Rule
config =
[ NoLeftPizza.rule NoLeftPizza.Any
]
The above configuration results in absolutely any use of `<|` being flagged. If
you'd prefer only flagging redundant usage (such as `foo <| bar`), pass
`NoLeftPizza.Redundant` as the configuration option.
If you would prefer to keep writing tests in the more "traditional" style which
uses `<|`, you can disable the rule for `tests/` like so:
import NoLeftPizza
import Review.Rule exposing (Rule)
config : List Rule
config =
[ NoLeftPizza.rule NoLeftPizza.Any
|> Rule.ignoreErrorsForDirectories
[ -- Test functions are traditionally built up using a left pizza.
-- While we don't want them in our regular code, let's allow them
-- just for tests.
"tests/"
]
]
Or pass `NoLeftPizza.Redundant` which will only apply to redundant usage:
import NoLeftPizza
import Review.Rule exposing (Rule)
config : List Rule
config =
[ NoLeftPizza.rule NoLeftPizza.Redundant
]
-}
rule : Strictness -> Rule
rule strictness =
Rule.newModuleRuleSchema "NoLeftPizza" strictness
|> Rule.withSimpleExpressionVisitor (expressionVisitor strictness)
|> Rule.fromModuleRuleSchema
expressionVisitor : Strictness -> Node Expression -> List (Error {})
expressionVisitor strictness node =
case Node.value node of
Expression.OperatorApplication "<|" _ left right ->
case makeError strictness node left right of
Just error ->
[ error ]
Nothing ->
[]
_ ->
[]
makeError : Strictness -> Node Expression -> Node Expression -> Node Expression -> Maybe (Error {})
makeError strictness node left right =
case ( strictness, isSimpleExpression right ) of
( Any, False ) ->
Just (produceError strictness node left (parenthesized right))
( Redundant, False ) ->
Nothing
_ ->
Just (produceError strictness node left right)
produceError : Strictness -> Node Expression -> Node Expression -> Node Expression -> Error {}
produceError strictness node left right =
Rule.errorWithFix (infoFor strictness)
(Node.range node)
[ Fix.replaceRangeBy (Node.range node)
(NoLeftPizzaUtil.expressionToString (Node.range node)
(Expression.Application [ left, right ])
)
]
infoFor : Strictness -> { message : String, details : List String }
infoFor strictness =
case strictness of
Any ->
{ message = "That's a left pizza (<|) operator application there!"
, details =
[ "We prefer using either parenthesized function application like `Html.text (context.translate Foo.Bar)` or right pizza's like `foo |> bar`."
, "The proposed fix rewrites the expression to a simple parenthesized expression, however, this may not always be what you want. Use your best judgement!"
]
}
Redundant ->
{ message = "Redundant left pizza (<|) operator application"
, details =
[ "This left pizza operator can be removed without any further changes, without changing the semantics of your code."
, "Using `<|` like this adds visual noise to code that can make it harder to read."
]
}
parenthesized : Node Expression -> Node Expression
parenthesized ((Node.Node range _) as node) =
Node.Node range (Expression.ParenthesizedExpression node)
isSimpleExpression : Node Expression -> Bool
isSimpleExpression (Node.Node _ expr) =
case expr of
Expression.Application _ ->
False
Expression.OperatorApplication _ _ _ _ ->
False
Expression.IfBlock _ _ _ ->
False
Expression.Operator _ ->
False
Expression.LetExpression _ ->
False
Expression.CaseExpression _ ->
False
Expression.LambdaExpression _ ->
False
Expression.GLSLExpression _ ->
False
_ ->
True

View File

@ -1,636 +0,0 @@
module NoLeftPizzaTest exposing (tests)
import NoLeftPizza
import Review.Test
import Test exposing (Test, describe, test)
tests : Test
tests =
describe "NoLeftPizza"
[ anyTests
, redundantTests
]
anyTests : Test
anyTests =
describe "NoLeftPizza.Any"
[ test "Simple pizza" <|
\_ ->
"""module A exposing (..)
a = foo <| bar"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError "foo <| bar"
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo bar"""
]
, test "Nested pizza" <|
\_ ->
"""module A exposing (..)
a = foo <| bar <| baz"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError
"foo <| bar <| baz"
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo (bar <| baz)"""
, makeAnyError "bar <| baz"
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo <| bar baz"""
]
, test "Fixes operator precedence" <|
\_ ->
"""module A exposing (..)
a = foo <| 1 + 1"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError
"foo <| 1 + 1"
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo (1 + 1)"""
]
, test "Fixes more operator precedence" <|
\_ ->
"""module A exposing (..)
a = foo <| 1 + 1 / 2"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError
"foo <| 1 + 1 / 2"
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo (1 + 1 / 2)"""
]
, test "Why isn't this fixed?" <|
\_ ->
"""module A exposing (..)
f =
List.map .x <| y
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError
"List.map .x <| y"
|> Review.Test.whenFixed
"""module A exposing (..)
f =
List.map .x y
"""
]
, test "Why isn't _this_ fixed, pt2?" <|
\_ ->
"""module A exposing (..)
f =
String.join " " <|
List.map x y
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError
"""String.join " " <|
List.map x y"""
|> Review.Test.whenFixed
"""module A exposing (..)
f =
String.join " " (List.map x y)
"""
]
, test "Why isn't _this_ fixed, pt3?" <|
\_ ->
"""module A exposing (..)
f =
String.join " " <|
List.map
(\\x ->
x
)
y
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError
"""String.join " " <|
List.map
(\\x ->
x
)
y"""
|> Review.Test.whenFixed
"""module A exposing (..)
f =
String.join " " (List.map (\\x -> x)
y)
"""
]
, test "handle parser operators with pizza" <|
\() ->
"""
module MyParser exposing (..)
numberToken =
Parser.getChompedString <|
Parser.succeed ()
|. Parser.chompIf Char.isDigit
|. Parser.chompWhile Char.isDigit
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError """Parser.getChompedString <|
Parser.succeed ()
|. Parser.chompIf Char.isDigit
|. Parser.chompWhile Char.isDigit"""
|> Review.Test.whenFixed
"""
module MyParser exposing (..)
numberToken =
Parser.getChompedString (Parser.succeed () |. Parser.chompIf Char.isDigit |. Parser.chompWhile Char.isDigit)
"""
]
, test "handle logic operators with pizza" <|
\() ->
"""
module A exposing (..)
f =
if isTrue <| True || False then
True
else
False
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError "isTrue <| True || False"
|> Review.Test.whenFixed
"""
module A exposing (..)
f =
if isTrue (True || False) then
True
else
False
"""
]
, describe "mixed pizzas" mixedPizzaTests
]
mixedPizzaTests : List Test
mixedPizzaTests =
[ test "a <| (b |> c)" <|
\() ->
"""
module A exposing (..)
f =
a <| (b |> c)
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError "a <| (b |> c)"
|> Review.Test.whenFixed
"""
module A exposing (..)
f =
a (b |> c)
"""
]
, test "(a <| b) |> c)" <|
\() ->
"""
module A exposing (..)
f =
(a <| b) |> c
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError "a <| b"
|> Review.Test.whenFixed
"""
module A exposing (..)
f =
(a b) |> c
"""
]
, test "a |> (b <| c)" <|
\() ->
"""
module A exposing (..)
f =
a |> (b <| c)
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError "b <| c"
|> Review.Test.whenFixed
"""
module A exposing (..)
f =
a |> (b c)
"""
]
, test "(a |> b) <| c" <|
\() ->
"""
module A exposing (..)
f =
(a |> b) <| c
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Any)
|> Review.Test.expectErrors
[ makeAnyError "(a |> b) <| c"
|> Review.Test.whenFixed
"""
module A exposing (..)
f =
(a |> b) c
"""
]
]
redundantTests : Test
redundantTests =
describe "NoLeftPizza.Redundant"
[ test "Simple pizza" <|
\_ ->
"""module A exposing (..)
a = foo <| bar"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "foo <| bar"
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo bar"""
]
, test "Nested pizza" <|
\_ ->
"""module A exposing (..)
a = foo <| bar <| baz"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError
"bar <| baz"
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo <| bar baz"""
]
, test "Fixes operator precedence" <|
\_ ->
"""module A exposing (..)
a = foo <| 1 + 1"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectNoErrors
, test "Why isn't this fixed?" <|
\_ ->
"""module A exposing (..)
f =
List.map .x <| y
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError
"List.map .x <| y"
|> Review.Test.whenFixed
"""module A exposing (..)
f =
List.map .x y
"""
]
, test "Why isn't _this_ fixed, pt2?" <|
\_ ->
"""module A exposing (..)
f =
String.join " " <|
List.map x y
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectNoErrors
, test "Why isn't _this_ fixed, pt3?" <|
\_ ->
"""module A exposing (..)
f =
String.join " " <|
List.map
(\\x ->
x
)
y
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectNoErrors
, test "handle parser operators with pizza" <|
\() ->
"""
module MyParser exposing (..)
numberToken =
Parser.getChompedString <|
Parser.succeed ()
|. Parser.chompIf Char.isDigit
|. Parser.chompWhile Char.isDigit
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectNoErrors
, test "handle logic operators with pizza" <|
\() ->
"""
module A exposing (..)
f =
if isTrue <| True || False then
True
else
False
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectNoErrors
, test "Parenthesized expression" <|
\() ->
"""
module A exposing (..)
foo = bar <| (abc xyz)
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| (abc xyz)"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar (abc xyz)
"""
]
, test "Unit expression" <|
\() ->
"""
module A exposing (..)
foo = bar <| ()
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| ()"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar ()
"""
]
, test "Qualified function" <|
\() ->
"""
module A exposing (..)
foo = bar <| Foo.Bar.xyz
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| Foo.Bar.xyz"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar Foo.Bar.xyz
"""
]
, test "Prefix operator" <|
\() ->
"""
module A exposing (..)
foo = bar <| (++)
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| (++)"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar (++)
"""
]
, test "Integer value" <|
\() ->
"""
module A exposing (..)
foo = bar <| 123
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| 123"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar 123
"""
]
, test "Floatable value" <|
\() ->
"""
module A exposing (..)
foo = bar <| 123.123
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| 123.123"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar 123.123
"""
]
, test "Negated value" <|
\() ->
"""
module A exposing (..)
foo = bar <| -123.123
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| -123.123"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar -123.123
"""
]
, test "String literal" <|
\() ->
"""
module A exposing (..)
foo = bar <| "hello there"
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| \"hello there\""
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar "hello there"
"""
]
, test "Character literal" <|
\() ->
"""
module A exposing (..)
foo = bar <| 'a'
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| 'a'"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar 'a'
"""
]
, test "Tupled expression" <|
\() ->
"""
module A exposing (..)
foo = bar <| ( a, b, abc xyz )
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| ( a, b, abc xyz )"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar (a, b, abc xyz)
"""
]
, test "Record expression" <|
\() ->
"""
module A exposing (..)
foo = bar <| { foo = 123 }
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| { foo = 123 }"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar {foo = 123}
"""
]
, test "Record update expression" <|
\() ->
"""
module A exposing (..)
foo = bar <| { r | foo = 123 }
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| { r | foo = 123 }"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar { r | foo = 123 }
"""
]
, test "Record access expression" <|
\() ->
"""
module A exposing (..)
foo = bar <| (foo bar).foo.bar
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| (foo bar).foo.bar"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar (foo bar).foo.bar
"""
]
, test "Record access function" <|
\() ->
"""
module A exposing (..)
foo = bar <| .bar
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| .bar"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar .bar
"""
]
, test "List literal" <|
\() ->
"""
module A exposing (..)
foo = bar <| [ 123, 456 ]
"""
|> Review.Test.run (NoLeftPizza.rule NoLeftPizza.Redundant)
|> Review.Test.expectErrors
[ makeRedundantError "bar <| [ 123, 456 ]"
|> Review.Test.whenFixed """
module A exposing (..)
foo = bar [123, 456]
"""
]
]
makeAnyError : String -> Review.Test.ExpectedError
makeAnyError under =
Review.Test.error
{ message = "That's a left pizza (<|) operator application there!"
, details =
[ "We prefer using either parenthesized function application like `Html.text (context.translate Foo.Bar)` or right pizza's like `foo |> bar`."
, "The proposed fix rewrites the expression to a simple parenthesized expression, however, this may not always be what you want. Use your best judgement!"
]
, under = under
}
makeRedundantError : String -> Review.Test.ExpectedError
makeRedundantError under =
Review.Test.error
{ message = "Redundant left pizza (<|) operator application"
, details =
[ "This left pizza operator can be removed without any further changes, without changing the semantics of your code."
, "Using `<|` like this adds visual noise to code that can make it harder to read."
]
, under = under
}

View File

@ -60,6 +60,15 @@ import Review.Rule as Rule exposing (Error, Rule)
, [ 4, mysteryNumber, 6 ]
]
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/elm-review-simplification/example --rules NoListLiteralsConcat
```
-}
rule : Rule
rule =

View File

@ -52,6 +52,15 @@ This won't fail if `SomeModule` does not define a `subscriptions` function.
subscriptions model =
SomeModule.subscriptions
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-tea/example --rules NoMissingSubscriptionsCall
```
-}
rule : Rule
rule =

View File

@ -43,6 +43,15 @@ For that, enable [`NoMissingTypeAnnotationInLetIn`](./NoMissingTypeAnnotationInL
in
c
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-common/example --rules NoMissingTypeAnnotation
```
-}
rule : Rule
rule =

View File

@ -47,6 +47,15 @@ For that, enable [`NoMissingTypeAnnotation`](./NoMissingTypeAnnotation).
in
b
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-common/example --rules NoMissingTypeAnnotationInLetIn
```
-}
rule : Rule
rule =

View File

@ -0,0 +1,760 @@
module NoMissingTypeExpose exposing (rule)
{-|
@docs rule
-}
import Dict exposing (Dict)
import Elm.Docs exposing (Module)
import Elm.Module
import Elm.Project exposing (Project)
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Exposing as Exposing exposing (Exposing)
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node(..))
import Elm.Syntax.Range as Range
import Elm.Syntax.Signature exposing (Signature)
import Elm.Syntax.Type as Type
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
import Review.Fix as Fix exposing (Fix)
import Review.Project.Dependency as Dependency exposing (Dependency)
import Review.Rule as Rule exposing (Rule)
import Set exposing (Set)
{-| Reports types that should be exposed but are not.
If a type is not exposed then it can be impossible to annotate functions or values that use them outside of the module. Affected types may be used in exposed function signatures, type aliases or other custom types.
import NoMissingTypeExpose
config : List Rule
config =
[ NoMissingTypeExpose.rule
]
## Fail
module Happiness exposing (happy, toString)
-- Type `Happiness` is private because it's not been exposed
type Happiness
= Happy
-- Private type `Happiness` used by exposed function `toString`
toString : Happiness -> String
toString happiness =
"Happy"
-- Private type `Happiness` used by exposed value `happy`
happy : Happiness
happy =
Happy
## Success
module Happiness exposing (Happiness, happy, toString)
type Happiness
= Happy
toString : Happiness -> String
toString happiness =
"Happy"
happy : Happiness
happy =
Happy
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-common/example --rules NoMissingTypeExpose
```
-}
rule : Rule
rule =
Rule.newProjectRuleSchema "NoMissingTypeExpose" initialProjectContext
|> Rule.withElmJsonProjectVisitor elmJsonVisitor
|> Rule.withDependenciesProjectVisitor dependencyDictVisitor
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withContextFromImportedModules
|> Rule.withModuleContext
{ fromProjectToModule = fromProjectToModuleContext
, fromModuleToProject = fromModuleToProjectContext
, foldProjectContexts = foldProjectContexts
}
|> Rule.fromProjectRuleSchema
elmJsonVisitor : Maybe { a | project : Project } -> ProjectContext -> ( List nothing, ProjectContext )
elmJsonVisitor maybeProject context =
case maybeProject of
Just { project } ->
( []
, { context
| exposedModules = exposedModulesForElmJson project
}
)
Nothing ->
( [], context )
exposedModulesForElmJson : Project -> ExposedModules
exposedModulesForElmJson project =
case project of
Elm.Project.Package { exposed } ->
Package (elmProjectExposedList exposed)
Elm.Project.Application _ ->
Application
elmProjectExposedList : Elm.Project.Exposed -> Set String
elmProjectExposedList exposed =
case exposed of
Elm.Project.ExposedList list ->
List.foldl (Elm.Module.toString >> Set.insert) Set.empty list
Elm.Project.ExposedDict dict ->
List.foldl
(\( _, list ) acc ->
List.foldl (Elm.Module.toString >> Set.insert) acc list
)
Set.empty
dict
dependencyDictVisitor : Dict String Dependency -> ProjectContext -> ( List nothing, ProjectContext )
dependencyDictVisitor dependencies context =
( []
, { context
| exposedModules =
Dict.values dependencies
|> List.foldl exposedModulesForDependency context.exposedModules
}
)
exposedModulesForDependency : Dependency -> ExposedModules -> ExposedModules
exposedModulesForDependency dependency exposedModules =
Dependency.modules dependency
|> List.foldl (.name >> addExposedModule) exposedModules
moduleVisitor :
Rule.ModuleRuleSchema state ModuleContext
-> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } ModuleContext
moduleVisitor schema =
schema
|> Rule.withModuleDefinitionVisitor moduleDefinitionVisitor
|> Rule.withImportVisitor importVisitor
|> Rule.withDeclarationListVisitor declarationListVisitor
|> Rule.withFinalModuleEvaluation finalEvaluation
moduleDefinitionVisitor : Node Module -> ModuleContext -> ( List nothing, ModuleContext )
moduleDefinitionVisitor (Node _ mod) context =
case context of
InternalModule data ->
( [], InternalModule { data | exposes = Module.exposingList mod } )
ExposedModule data ->
( []
, ExposedModule
{ data
| exposes = Module.exposingList mod
, exposingListStart = exposingListStartLocation (Module.exposingList mod)
}
)
exposingListStartLocation : Exposing -> Maybe Range.Location
exposingListStartLocation exposes =
case exposes of
Exposing.Explicit ((Node range _) :: _) ->
Just range.start
_ ->
Nothing
importVisitor : Node Import -> ModuleContext -> ( List nothing, ModuleContext )
importVisitor (Node _ { moduleName, moduleAlias, exposingList }) context =
case context of
InternalModule _ ->
( [], context )
ExposedModule data ->
( []
, ExposedModule
{ data
| exposedModules =
exposedModulesForImportAlias (Node.value moduleName) moduleAlias data.exposedModules
, importedTypes =
importedTypesForImportExposing (Node.value moduleName) exposingList data.moduleTypes data.importedTypes
}
)
exposedModulesForImportAlias : ModuleName -> Maybe (Node ModuleName) -> ExposedModules -> ExposedModules
exposedModulesForImportAlias moduleName maybeModuleAlias exposedModules =
case maybeModuleAlias of
Just (Node _ moduleAlias) ->
addExposedModuleAlias moduleName
(String.join "." moduleAlias)
exposedModules
Nothing ->
exposedModules
importedTypesForImportExposing :
ModuleName
-> Maybe (Node Exposing)
-> Dict ModuleName (Set String)
-> Dict String ModuleName
-> Dict String ModuleName
importedTypesForImportExposing moduleName maybeExposing moduleTypes importedTypes =
case maybeExposing of
Just (Node _ (Exposing.Explicit list)) ->
List.foldl (importedTypesForImportExpose moduleName) importedTypes list
Just (Node _ (Exposing.All _)) ->
importedTypesForModule moduleName moduleTypes importedTypes
Nothing ->
importedTypes
importedTypesForImportExpose : ModuleName -> Node Exposing.TopLevelExpose -> Dict String ModuleName -> Dict String ModuleName
importedTypesForImportExpose moduleName (Node _ expose) importedTypes =
case expose of
Exposing.TypeExpose { name } ->
rememberImportedType moduleName name importedTypes
Exposing.TypeOrAliasExpose name ->
rememberImportedType moduleName name importedTypes
Exposing.FunctionExpose _ ->
importedTypes
Exposing.InfixExpose _ ->
importedTypes
importedTypesForModule :
ModuleName
-> Dict ModuleName (Set String)
-> Dict String ModuleName
-> Dict String ModuleName
importedTypesForModule moduleName moduleTypes importedTypes =
case Dict.get moduleName moduleTypes of
Just types ->
Set.foldl (rememberImportedType moduleName) importedTypes types
Nothing ->
importedTypes
rememberImportedType : ModuleName -> String -> Dict String ModuleName -> Dict String ModuleName
rememberImportedType moduleName typeName importedTypes =
Dict.insert typeName moduleName importedTypes
declarationListVisitor : List (Node Declaration) -> ModuleContext -> ( List nothing, ModuleContext )
declarationListVisitor nodes context =
( []
, case context of
InternalModule data ->
InternalModule
{ data
| exposedTypes =
exposedTypesForDeclarationList data.exposes nodes data.exposedTypes
}
ExposedModule data ->
ExposedModule
{ data
| declaredTypes = declaredTypesForDeclarationList nodes data.declaredTypes
, exposedSignatureTypes = exposedSignatureTypesForDeclarationList data.exposes nodes data.exposedSignatureTypes
}
)
exposedTypesForDeclarationList : Exposing -> List (Node Declaration) -> Set String -> Set String
exposedTypesForDeclarationList exposes list exposedTypes =
List.foldl (exposedTypesForDeclaration exposes) exposedTypes list
exposedTypesForDeclaration : Exposing -> Node Declaration -> Set String -> Set String
exposedTypesForDeclaration exposes (Node _ declaration) exposedTypes =
case declaration of
Declaration.CustomTypeDeclaration { name } ->
rememberExposedType exposes name exposedTypes
Declaration.AliasDeclaration { name } ->
rememberExposedType exposes name exposedTypes
_ ->
exposedTypes
rememberExposedType : Exposing -> Node String -> Set String -> Set String
rememberExposedType exposes (Node _ name) exposedTypes =
if isTypeExposed exposes name then
Set.insert name exposedTypes
else
exposedTypes
declaredTypesForDeclarationList : List (Node Declaration) -> Set String -> Set String
declaredTypesForDeclarationList list declaredTypes =
List.foldl declaredTypesForDeclaration declaredTypes list
declaredTypesForDeclaration : Node Declaration -> Set String -> Set String
declaredTypesForDeclaration (Node _ declaration) declaredTypes =
case declaration of
Declaration.CustomTypeDeclaration { name } ->
rememberDeclaredType name declaredTypes
Declaration.AliasDeclaration { name } ->
rememberDeclaredType name declaredTypes
_ ->
declaredTypes
rememberDeclaredType : Node String -> Set String -> Set String
rememberDeclaredType (Node _ name) declaredTypes =
Set.insert name declaredTypes
exposedSignatureTypesForDeclarationList :
Exposing
-> List (Node Declaration)
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForDeclarationList exposes list exposedSignatureTypes =
List.foldl (exposedSignatureTypesForDeclaration exposes) exposedSignatureTypes list
exposedSignatureTypesForDeclaration :
Exposing
-> Node Declaration
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForDeclaration exposes (Node _ declaration) exposedSignatureTypes =
case declaration of
Declaration.CustomTypeDeclaration { name, constructors } ->
exposedSignatureTypesForConstructorList exposes name constructors exposedSignatureTypes
Declaration.AliasDeclaration { name, typeAnnotation } ->
exposedSignatureTypesForAlias exposes name typeAnnotation exposedSignatureTypes
Declaration.FunctionDeclaration { signature } ->
exposedSignatureTypesForSignature exposes signature exposedSignatureTypes
_ ->
exposedSignatureTypes
exposedSignatureTypesForConstructorList :
Exposing
-> Node String
-> List (Node Type.ValueConstructor)
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForConstructorList exposes (Node _ name) list exposedSignatureTypes =
if isTypeExposedOpen exposes name then
List.foldl exposedSignatureTypesForConstructor exposedSignatureTypes list
else
exposedSignatureTypes
exposedSignatureTypesForConstructor :
Node Type.ValueConstructor
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForConstructor (Node _ { arguments }) exposedSignatureTypes =
exposedSignatureTypesForTypeAnnotationList arguments exposedSignatureTypes
exposedSignatureTypesForAlias :
Exposing
-> Node String
-> Node TypeAnnotation
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForAlias exposes (Node _ name) typeAnnotation exposedSignatureTypes =
if isTypeExposed exposes name then
case typeAnnotation of
Node _ (TypeAnnotation.Typed _ list) ->
exposedSignatureTypesForTypeAnnotationList list exposedSignatureTypes
_ ->
exposedSignatureTypesForTypeAnnotation typeAnnotation exposedSignatureTypes
else
exposedSignatureTypes
exposedSignatureTypesForSignature :
Exposing
-> Maybe (Node Signature)
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForSignature exposes maybeSignature exposedSignatureTypes =
case maybeSignature of
Just (Node _ { name, typeAnnotation }) ->
if Exposing.exposesFunction (Node.value name) exposes then
exposedSignatureTypesForTypeAnnotation typeAnnotation exposedSignatureTypes
else
exposedSignatureTypes
Nothing ->
exposedSignatureTypes
exposedSignatureTypesForRecordFieldList :
List (Node TypeAnnotation.RecordField)
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForRecordFieldList fields exposedSignatureTypes =
List.foldl exposedSignatureTypesForRecordField exposedSignatureTypes fields
exposedSignatureTypesForRecordField :
Node TypeAnnotation.RecordField
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForRecordField (Node _ ( _, typeAnnotation )) exposedSignatureTypes =
exposedSignatureTypesForTypeAnnotation typeAnnotation exposedSignatureTypes
exposedSignatureTypesForTypeAnnotationList :
List (Node TypeAnnotation)
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForTypeAnnotationList list exposedSignatureTypes =
List.foldl exposedSignatureTypesForTypeAnnotation exposedSignatureTypes list
exposedSignatureTypesForTypeAnnotation :
Node TypeAnnotation
-> List (Node ( ModuleName, String ))
-> List (Node ( ModuleName, String ))
exposedSignatureTypesForTypeAnnotation (Node _ typeAnnotation) exposedSignatureTypes =
case typeAnnotation of
TypeAnnotation.Typed name list ->
(name :: exposedSignatureTypes)
|> exposedSignatureTypesForTypeAnnotationList list
TypeAnnotation.FunctionTypeAnnotation left right ->
exposedSignatureTypes
|> exposedSignatureTypesForTypeAnnotation left
|> exposedSignatureTypesForTypeAnnotation right
TypeAnnotation.Tupled list ->
exposedSignatureTypes
|> exposedSignatureTypesForTypeAnnotationList list
TypeAnnotation.Record fields ->
exposedSignatureTypes
|> exposedSignatureTypesForRecordFieldList fields
TypeAnnotation.GenericRecord _ (Node _ fields) ->
exposedSignatureTypes
|> exposedSignatureTypesForRecordFieldList fields
TypeAnnotation.Unit ->
exposedSignatureTypes
TypeAnnotation.GenericType _ ->
exposedSignatureTypes
finalEvaluation : ModuleContext -> List (Rule.Error {})
finalEvaluation context =
case context of
InternalModule _ ->
[]
ExposedModule data ->
data.exposedSignatureTypes
|> List.map (Node.map (moduleNameForType data.importedTypes))
|> List.filter (isTypePrivate data)
|> List.map (makeError data.exposingListStart)
isTypePrivate : ExposedModuleData -> Node ( ModuleName, String ) -> Bool
isTypePrivate data (Node _ typeCall) =
case typeCall of
( [], name ) ->
if Set.member name data.declaredTypes then
not (isTypeExposed data.exposes name)
else
False
( moduleName, _ ) ->
not (isModuleExposed data.exposedModules moduleName)
moduleNameForType : Dict String ModuleName -> ( ModuleName, String ) -> ( ModuleName, String )
moduleNameForType importedTypes ( moduleName, typeName ) =
case Dict.get typeName importedTypes of
Just typeModuleName ->
( typeModuleName, typeName )
_ ->
( moduleName, typeName )
isTypeExposed : Exposing -> String -> Bool
isTypeExposed exposes name =
case exposes of
Exposing.All _ ->
True
Exposing.Explicit list ->
List.any (isExposingATypeNamed name) list
isTypeExposedOpen : Exposing -> String -> Bool
isTypeExposedOpen exposes name =
case exposes of
Exposing.All _ ->
True
Exposing.Explicit list ->
List.any (isExposingAnOpenTypeNamed name) list
isExposingATypeNamed : String -> Node Exposing.TopLevelExpose -> Bool
isExposingATypeNamed needle (Node _ topLevelExpose) =
case topLevelExpose of
Exposing.InfixExpose _ ->
False
Exposing.FunctionExpose _ ->
False
Exposing.TypeOrAliasExpose name ->
name == needle
Exposing.TypeExpose { name } ->
name == needle
isExposingAnOpenTypeNamed : String -> Node Exposing.TopLevelExpose -> Bool
isExposingAnOpenTypeNamed needle (Node _ expose) =
case expose of
Exposing.TypeExpose { name, open } ->
name == needle && open /= Nothing
_ ->
False
addExposedModule : String -> ExposedModules -> ExposedModules
addExposedModule moduleName exposedModules =
case exposedModules of
Application ->
exposedModules
Package list ->
Package (Set.insert moduleName list)
addExposedModuleAlias : ModuleName -> String -> ExposedModules -> ExposedModules
addExposedModuleAlias moduleName moduleAlias exposedModules =
case exposedModules of
Application ->
exposedModules
Package list ->
if Set.member (String.join "." moduleName) list then
Package (Set.insert moduleAlias list)
else
exposedModules
isModuleExposed : ExposedModules -> ModuleName -> Bool
isModuleExposed exposedModules moduleName =
case exposedModules of
Application ->
True
Package list ->
Set.member (String.join "." moduleName) list
makeError : Maybe Range.Location -> Node ( ModuleName, String ) -> Rule.Error {}
makeError exposingListStart (Node range typeName) =
let
formattedName : String
formattedName =
formatTypeName typeName
in
Rule.errorWithFix
{ message = "Private type `" ++ formattedName ++ "` should be exposed"
, details =
[ "Users of this module will not be able to annotate a value of this type if they wanted to. You should expose this type or an alias of this type."
]
}
range
(exposeTypeFix exposingListStart typeName)
exposeTypeFix : Maybe Range.Location -> ( ModuleName, String ) -> List Fix
exposeTypeFix exposingListStart ( moduleName, name ) =
case ( exposingListStart, moduleName ) of
( Just start, [] ) ->
[ Fix.insertAt start (name ++ ", ") ]
_ ->
[]
formatTypeName : ( ModuleName, String ) -> String
formatTypeName ( moduleName, name ) =
String.join "." (moduleName ++ [ name ])
fromProjectToModuleContext : Rule.ModuleKey -> Node ModuleName -> ProjectContext -> ModuleContext
fromProjectToModuleContext _ (Node _ moduleName) { exposedModules, moduleTypes } =
if isModuleExposed exposedModules moduleName then
initialExposedModuleContext exposedModules moduleTypes
else
initialInternalModuleContext
fromModuleToProjectContext : Rule.ModuleKey -> Node ModuleName -> ModuleContext -> ProjectContext
fromModuleToProjectContext _ (Node _ moduleName) context =
case context of
InternalModule { exposedTypes } ->
{ initialProjectContext | moduleTypes = Dict.singleton moduleName exposedTypes }
ExposedModule _ ->
initialProjectContext
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts new old =
{ exposedModules = foldExposedModules new.exposedModules old.exposedModules
, moduleTypes = foldModuleTypes new.moduleTypes old.moduleTypes
}
foldExposedModules : ExposedModules -> ExposedModules -> ExposedModules
foldExposedModules newExposedModules oldExposedModules =
case ( oldExposedModules, newExposedModules ) of
( Application, Application ) ->
Application
( Application, Package _ ) ->
newExposedModules
( Package _, Application ) ->
oldExposedModules
( Package oldList, Package newList ) ->
Package (Set.union oldList newList)
foldModuleTypes : Dict ModuleName (Set String) -> Dict ModuleName (Set String) -> Dict ModuleName (Set String)
foldModuleTypes newModuleTypes oldModuleTypes =
Dict.foldl foldModuleTypesHelp newModuleTypes oldModuleTypes
foldModuleTypesHelp : ModuleName -> Set String -> Dict ModuleName (Set String) -> Dict ModuleName (Set String)
foldModuleTypesHelp moduleName newTypes moduleTypes =
case Dict.get moduleName moduleTypes of
Just oldTypes ->
Dict.insert moduleName (Set.union oldTypes newTypes) moduleTypes
Nothing ->
Dict.insert moduleName newTypes moduleTypes
initialProjectContext : ProjectContext
initialProjectContext =
{ exposedModules = Application
, moduleTypes = Dict.empty
}
initialInternalModuleContext : ModuleContext
initialInternalModuleContext =
InternalModule initialAnyModuleData
initialExposedModuleContext : ExposedModules -> Dict ModuleName (Set String) -> ModuleContext
initialExposedModuleContext exposedModules moduleTypes =
ExposedModule
{ declaredTypes = Set.empty
, exposedModules = exposedModules
, exposedSignatureTypes = []
, exposes = Exposing.Explicit []
, exposingListStart = Nothing
, importedTypes = Dict.empty
, moduleTypes = moduleTypes
}
initialAnyModuleData : InternalModuleData
initialAnyModuleData =
{ exposedTypes = Set.empty
, exposes = Exposing.Explicit []
}
type alias ProjectContext =
{ exposedModules : ExposedModules
, moduleTypes : Dict ModuleName (Set String)
}
type ModuleContext
= InternalModule InternalModuleData
| ExposedModule ExposedModuleData
type alias InternalModuleData =
{ exposedTypes : Set String
, exposes : Exposing
}
type alias ExposedModuleData =
{ declaredTypes : Set String
, exposedModules : ExposedModules
, exposedSignatureTypes : List (Node ( ModuleName, String ))
, exposes : Exposing
, exposingListStart : Maybe Range.Location
, importedTypes : Dict String ModuleName
, moduleTypes : Dict ModuleName (Set String)
}
type ExposedModules
= Application
| Package (Set String)

View File

@ -0,0 +1,950 @@
module NoMissingTypeExposeTest exposing (all)
import Elm.Docs
import Elm.Project
import Json.Decode as Decode
import NoMissingTypeExpose exposing (rule)
import Review.Project as Project exposing (Project)
import Review.Project.Dependency as Dependency exposing (Dependency)
import Review.Test
import Test exposing (Test, describe, test)
all : Test
all =
describe "NoMissingTypeExpose"
[ describe "in exposed functions" functionTests
, describe "in exposed types" typeTests
, describe "in exposed type aliases" typeAliasTests
, describe "in package projects" packageTests
]
details : List String
details =
[ "Users of this module will not be able to annotate a value of this type if they wanted to. You should expose this type or an alias of this type."
]
functionTests : List Test
functionTests =
[ test "passes when everything is exposed" <|
\() ->
"""
module Happiness exposing (..)
type Happiness = Happy
toString : Happiness -> String
toString howHappy = "Happy"
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "passes when an exposed type is used" <|
\() ->
"""
module Happiness exposing (Happiness, toString)
type Happiness = Ecstatic
toString : Happiness -> String
toString howHappy = "Very"
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "reports when a private type is used" <|
\() ->
"""
module Happiness exposing (toString)
type Happiness = Ecstatic
toString : Happiness -> String
toString howHappy = "Very"
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 12 }, end = { row = 4, column = 21 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, toString)
type Happiness = Ecstatic
toString : Happiness -> String
toString howHappy = "Very"
"""
]
, test "reports when a private type is returned" <|
\() ->
"""
module Happiness exposing (ecstatic)
type Happiness = Ecstatic
ecstatic : Happiness
ecstatic = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 12 }, end = { row = 4, column = 21 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, ecstatic)
type Happiness = Ecstatic
ecstatic : Happiness
ecstatic = Ecstatic
"""
]
, test "reports when a private type is used in a Maybe" <|
\() ->
"""
module Happiness exposing (toString)
type Happiness = Ecstatic
toString : Maybe Happiness -> String
toString maybeHappy = "Very"
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 18 }, end = { row = 4, column = 27 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, toString)
type Happiness = Ecstatic
toString : Maybe Happiness -> String
toString maybeHappy = "Very"
"""
]
, test "reports when an external private type is used in a Maybe" <|
\() ->
"""
module Happiness exposing (toString)
type Happiness = Ecstatic
toString : Maybe.Maybe Happiness -> String
toString maybeHappy = "Very"
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 24 }, end = { row = 4, column = 33 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, toString)
type Happiness = Ecstatic
toString : Maybe.Maybe Happiness -> String
toString maybeHappy = "Very"
"""
]
, test "reports when a private type is used in a tuple" <|
\() ->
"""
module Happiness exposing (equal)
type Happiness = Ecstatic
equal : ( Happiness, Happiness ) -> Bool
equal ( a, b ) = a == b
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 11 }, end = { row = 4, column = 20 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, equal)
type Happiness = Ecstatic
equal : ( Happiness, Happiness ) -> Bool
equal ( a, b ) = a == b
"""
, Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 22 }, end = { row = 4, column = 31 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, equal)
type Happiness = Ecstatic
equal : ( Happiness, Happiness ) -> Bool
equal ( a, b ) = a == b
"""
]
, test "reports when a private type is used in a record" <|
\() ->
"""
module Happiness exposing (rank)
type Happiness = Ecstatic
rank : { happiness : Happiness } -> Int
rank { happiness } = 0
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 22 }, end = { row = 4, column = 31 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, rank)
type Happiness = Ecstatic
rank : { happiness : Happiness } -> Int
rank { happiness } = 0
"""
]
, test "reports when a private type is used in an extensible record" <|
\() ->
"""
module Happiness exposing (rank)
type Happiness = Ecstatic
rank : { a | happiness : Happiness } -> Int
rank { happiness } = 0
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 26 }, end = { row = 4, column = 35 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, rank)
type Happiness = Ecstatic
rank : { a | happiness : Happiness } -> Int
rank { happiness } = 0
"""
]
]
typeTests : List Test
typeTests =
[ test "passes when an exposed type uses another exposed type" <|
\() ->
"""
module Happiness exposing (Happiness, Mood(..))
type Mood = Happy Happiness
type Happiness = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "passes when an exposed opaque type uses a private type" <|
\() ->
"""
module Happiness exposing (Mood)
type Mood = Happy Happiness
type Happiness = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "reports when an exposed open type uses a private type" <|
\() ->
"""
module Happiness exposing (Mood(..))
type Mood = Happy Happiness
type Happiness = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 3, column = 19 }, end = { row = 3, column = 28 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, Mood(..))
type Mood = Happy Happiness
type Happiness = Ecstatic
"""
]
]
typeAliasTests : List Test
typeAliasTests =
[ test "passes when everything is exposed" <|
\() ->
"""
module Happiness exposing (..)
type alias Happiness = { happiness : Int }
toString : Happiness -> String
toString howHappy = "Very"
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "does not report a function using an exposed type alias of a private type" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (Happiness, toString)
import Mood
type alias Happiness = Mood.Happiness
toString : Happiness -> String
toString happiness = "Happy"
""", """
module Mood exposing (Happiness)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectNoErrors
, test "reports a function using a private type alias" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
"""
module Exposed exposing (toString)
type alias Happiness = { happiness : Int }
toString : Happiness -> String
toString happiness = "Happy"
"""
|> Review.Test.runWithProjectData project rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 12 }, end = { row = 4, column = 21 } }
|> Review.Test.whenFixed
"""
module Exposed exposing (Happiness, toString)
type alias Happiness = { happiness : Int }
toString : Happiness -> String
toString happiness = "Happy"
"""
]
, test "reports a function using an internal type alias" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import Mood
toString : Mood.Happiness -> String
toString happiness = "Happy"
""", """
module Mood exposing (Happiness)
type alias Happiness = { howHappy : Int }
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "Exposed"
, [ Review.Test.error
{ message = "Private type `Mood.Happiness` should be exposed"
, details = details
, under = "Mood.Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 12 }, end = { row = 4, column = 26 } }
]
)
]
, test "reports a function using an internal type alias exposed via (..)" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import Mood exposing (..)
toString : Happiness -> String
toString happiness = "Happy"
""", """
module Mood exposing (..)
type alias Happiness = { howHappy : Int }
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "Exposed"
, [ Review.Test.error
{ message = "Private type `Mood.Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 12 }, end = { row = 4, column = 21 } }
]
)
]
, test "reports a function using an internal type alias exposed explicitly and imported via (..)" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import Mood exposing (..)
toString : Happiness -> String
toString happiness = "Happy"
""", """
module Mood exposing (Happiness)
type alias Happiness = { howHappy : Int }
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "Exposed"
, [ Review.Test.error
{ message = "Private type `Mood.Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 12 }, end = { row = 4, column = 21 } }
]
)
]
, describe "when an exposed type alias uses a private type" <|
[ test "does not report when the type alias is directly a private type" <|
\() ->
"""
module Happiness exposing (Mood)
type alias Mood = Happiness Int
type Happiness a = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "reports a private type in another type" <|
\() ->
"""
module Happiness exposing (Mood)
type alias Mood = Maybe Happiness
type Happiness = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 3, column = 25 }, end = { row = 3, column = 34 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, Mood)
type alias Mood = Maybe Happiness
type Happiness = Ecstatic
"""
]
, test "reports a private type in a record" <|
\() ->
"""
module Happiness exposing (Mood)
type alias Mood = { happiness : Happiness }
type Happiness = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 3, column = 33 }, end = { row = 3, column = 42 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, Mood)
type alias Mood = { happiness : Happiness }
type Happiness = Ecstatic
"""
]
, test "reports private type in an extensible record" <|
\() ->
"""
module Happiness exposing (Mood)
type alias Mood = { a | happiness : Happiness }
type Happiness = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 3, column = 37 }, end = { row = 3, column = 46 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, Mood)
type alias Mood = { a | happiness : Happiness }
type Happiness = Ecstatic
"""
]
, test "reports a private type in a tuple" <|
\() ->
"""
module Happiness exposing (Mood)
type alias Mood = ( Happiness, Int )
type Happiness = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 3, column = 21 }, end = { row = 3, column = 30 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, Mood)
type alias Mood = ( Happiness, Int )
type Happiness = Ecstatic
"""
]
, test "reports a private type in a function" <|
\() ->
"""
module Happiness exposing (Mood)
type alias Mood = Happiness -> Int
type Happiness = Ecstatic
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = "Private type `Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 3, column = 19 }, end = { row = 3, column = 28 } }
|> Review.Test.whenFixed
"""
module Happiness exposing (Happiness, Mood)
type alias Mood = Happiness -> Int
type Happiness = Ecstatic
"""
]
]
]
packageTests : List Test
packageTests =
[ test "reports a function exposed by a package module using a type from an internal module" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import Mood
toString : Mood.Happiness -> String
toString happiness = "Happy"
""", """
module Mood exposing (Happiness)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "Exposed"
, [ Review.Test.error
{ message = "Private type `Mood.Happiness` should be exposed"
, details = details
, under = "Mood.Happiness"
}
]
)
]
, test "does not report an exposed function using a type from an exposed module" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import ExposedMood
toString : ExposedMood.Happiness -> String
toString happiness = "Happy"
""", """
module ExposedMood exposing (Happiness)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectNoErrors
, test "reports an exposed function using an internal type from an aliased module" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import Mood as M
toString : M.Happiness -> String
toString happiness = "Happy"
""", """
module Mood exposing (Happiness)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "Exposed"
, [ Review.Test.error
{ message = "Private type `M.Happiness` should be exposed"
, details = details
, under = "M.Happiness"
}
]
)
]
, test "reports an exposed function using an exposed type from an aliased module" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import ExposedMood as M
toString : M.Happiness -> String
toString happiness = "Happy"
""", """
module ExposedMood exposing (Happiness)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectNoErrors
, test "reports an exposed function using an internal type imported from a module" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import Mood exposing (Happiness)
toString : Happiness -> String
toString happiness = "Happy"
""", """
module Mood exposing (Happiness)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "Exposed"
, [ Review.Test.error
{ message = "Private type `Mood.Happiness` should be exposed"
, details = details
, under = "Happiness"
}
|> Review.Test.atExactly { start = { row = 4, column = 12 }, end = { row = 4, column = 21 } }
]
)
]
, test "reports an exposed function using an internal type imported exposing all" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toRating)
import Mood exposing (..)
import Rating exposing (..)
toRating : Happiness -> Rating
toRating happiness = Rating.five
""", """
module Mood exposing (Happiness)
type Happiness = Ecstatic
""", """
module Rating exposing (Rating, five)
type Rating = Rating Int
five : Rating
five = Rating 5
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectErrorsForModules
[ ( "Exposed"
, [ Review.Test.error
{ message = "Private type `Mood.Happiness` should be exposed"
, details = details
, under = "Happiness"
}
, Review.Test.error
{ message = "Private type `Rating.Rating` should be exposed"
, details = details
, under = "Rating"
}
|> Review.Test.atExactly { start = { row = 5, column = 25 }, end = { row = 5, column = 31 } }
]
)
]
, test "does not report an internal function using a type from another internal module" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Mood exposing (Happiness, toRating)
import Rating exposing (Rating)
type Happiness = Ecstatic
toRating : Happiness -> Rating
toRating happiness = Rating.five
""", """
module Rating exposing (Rating, five)
type Rating = Rating Int
five : Rating
five = Rating 5
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectNoErrors
, test "does not report an exposed function using an exposed imported type" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import ExposedMood exposing (Happiness)
toString : Happiness -> String
toString happiness = "Happy"
""", """
module ExposedMood exposing (Happiness)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectNoErrors
, test "does not report an exposed function using an exposed type imported all" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import ExposedMood exposing (..)
toString : Happiness -> String
toString happiness = "Happy"
""", """
module ExposedMood exposing (Happiness)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectNoErrors
, test "does not report an exposed function using an type exposed all and imported all" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
in
[ """
module Exposed exposing (toString)
import ExposedMood exposing (..)
toString : Happiness -> String
toString happiness = "Happy"
""", """
module ExposedMood exposing (..)
type Happiness = Ecstatic
""" ]
|> Review.Test.runOnModulesWithProjectData project rule
|> Review.Test.expectNoErrors
, test "does not report an exposed function using a dependency's type" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
|> Project.addDependency dependency
in
"""
module Exposed exposing (toString)
import ExternalMood exposing (Happiness)
toString : Happiness -> String
toString happiness = "Happy"
"""
|> Review.Test.runWithProjectData project rule
|> Review.Test.expectNoErrors
, test "does not report an exposed function using a dependency's type exposing all" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
|> Project.addDependency dependency
in
"""
module Exposed exposing (toString)
import ExternalMood exposing (..)
toString : Happiness -> String
toString happiness = "Happy"
"""
|> Review.Test.runWithProjectData project rule
|> Review.Test.expectNoErrors
, test "does not report an exposed function importing a dependency's type" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
|> Project.addDependency dependency
in
"""
module Exposed exposing (toString)
import ExternalMood
toString : ExternalMood.Happiness -> String
toString happiness = "Happy"
"""
|> Review.Test.runWithProjectData project rule
|> Review.Test.expectNoErrors
, test "does not report an exposed function importing a dependency's type with alias" <|
\() ->
let
project : Project
project =
Project.new
|> Project.addElmJson (createElmJson packageElmJson)
|> Project.addDependency dependency
in
"""
module Exposed exposing (toString)
import ExternalMood as M
toString : M.Happiness -> String
toString happiness = "Happy"
"""
|> Review.Test.runWithProjectData project rule
|> Review.Test.expectNoErrors
]
createElmJson : String -> { path : String, raw : String, project : Elm.Project.Project }
createElmJson rawElmJson =
{ path = "elm.json"
, raw = rawElmJson
, project = createElmJsonProject rawElmJson
}
createElmJsonProject : String -> Elm.Project.Project
createElmJsonProject rawElmJson =
case Decode.decodeString Elm.Project.decoder rawElmJson of
Ok project ->
project
Err error ->
Debug.todo ("[elm.json]: " ++ Debug.toString error)
packageElmJson : String
packageElmJson =
"""{
"type": "package",
"name": "jfmengels/review-common-tests",
"summary": "A test package",
"license": "MIT",
"version": "1.0.0",
"exposed-modules": [
"Exposed",
"ExposedMood"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"elm/core": "1.0.2 <= v < 2.0.0"
},
"test-dependencies": {}
}"""
dependency : Dependency
dependency =
Dependency.create
"sparksp/external-mood"
(createElmJsonProject dependencyElmJson)
dependencyModules
dependencyModules : List Elm.Docs.Module
dependencyModules =
[ { name = "ExternalMood"
, comment = ""
, unions =
[ { name = "Happiness"
, comment = ""
, args = []
, tags =
[ ( "Ecstatic", [] )
, ( "FineIGuess", [] )
, ( "Unhappy", [] )
]
}
]
, aliases = []
, values = []
, binops = []
}
]
dependencyElmJson : String
dependencyElmJson =
"""{
"type": "package",
"name": "sparksp/external-mood",
"summary": "Moods for tests",
"license": "MIT",
"version": "1.0.0",
"exposed-modules": [
"ExternalMood"
],
"elm-version": "0.19.0 <= v <= 0.20.0",
"dependencies": {},
"test-dependencies": {}
}"""

View File

@ -0,0 +1,173 @@
module NoModuleOnExposedNames exposing (rule)
{-|
@docs rule
-}
import Elm.Syntax.Import exposing (Import)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import NoModuleOnExposedNames.Context as Context
import Review.Fix as Fix
import Review.Rule as Rule exposing (Error, Rule)
import Vendor.NameVisitor as NameVisitor
{-| Forbid modules on names that have been exposed.
config : List Rule
config =
[ NoModuleOnExposedNames.rule
]
If a function or type has been exposed on import do not allow it to be called via the module name/alias. You should either remove the call from the import exposes or remove the module name from the call.
## Failure
Here `class` is exposed but is still called by `Attr.class`.
import Html.Attributes as Attr exposing (class)
view children =
div [ Attr.class "container" ] children
## Success
Remove the module name from the call:
import Html.Attributes as Attr exposing (class)
view children =
div [ class "container" ] children
## Success
Or remove `class` from `exposing`:
import Html.Attributes as Attr
view children =
div [ Attr.class "container" ] children
## Failure
Here `Attribute` has been exposed but is still called by `Html.Attribute`.
import Html exposing (Attribute, Html)
container : List (Html.Attribute msg) -> List (Html msg) -> Html msg
container attributes children =
div attributes children
## Success
Remove the module name from the call:
import Html exposing (Attribute, Html)
container : List (Attribute msg) -> List (Html msg) -> Html msg
container attributes children =
div attributes children
## Success
Or remove `Attribute` from `exposing`:
import Html exposing (Html)
container : List (Html.Attribute msg) -> List (Html msg) -> Html msg
container attributes children =
div attributes children
-}
rule : Rule
rule =
Rule.newModuleRuleSchema "NoModuleOnExposedNames" Context.initial
|> Rule.withImportVisitor importVisitor
|> NameVisitor.withValueAndTypeVisitors
{ valueVisitor = valueVisitor
, typeVisitor = typeVisitor
}
|> Rule.fromModuleRuleSchema
importVisitor : Node Import -> Context.Module -> ( List (Error {}), Context.Module )
importVisitor node context =
( [], context |> rememberExposedNames (Node.value node) )
rememberExposedNames : Import -> Context.Module -> Context.Module
rememberExposedNames { moduleName, moduleAlias, exposingList } context =
case exposingList of
Nothing ->
context
Just exposes ->
let
moduleNameOrAlias =
moduleAlias
|> Maybe.map Node.value
|> Maybe.withDefault (Node.value moduleName)
in
context |> Context.expose moduleNameOrAlias (Node.value exposes)
valueVisitor : Node ( ModuleName, String ) -> Context.Module -> ( List (Error {}), Context.Module )
valueVisitor node context =
case Node.value node of
( moduleName, name ) ->
if Context.isFunctionExposed context moduleName name then
( [ moduleOnExposedValueError name (Node.range node) ]
, context
)
else
( [], context )
typeVisitor : Node ( ModuleName, String ) -> Context.Module -> ( List (Error {}), Context.Module )
typeVisitor node context =
case Node.value node of
( moduleName, name ) ->
if Context.isTypeExposed context moduleName name then
( [ moduleOnExposedTypeError name (Node.range node) ]
, context
)
else
( [], context )
moduleOnExposedValueError : String -> Range -> Error {}
moduleOnExposedValueError name range =
Rule.errorWithFix
{ message = "Module used on exposed value `" ++ name ++ "`."
, details =
[ "It is not necessary to use the module here as `" ++ name ++ "` was exposed on import."
, "You should remove the module from this call, or remove the name from the import .. exposing list."
]
}
range
[ Fix.replaceRangeBy range name ]
moduleOnExposedTypeError : String -> Range -> Error {}
moduleOnExposedTypeError name range =
Rule.errorWithFix
{ message = "Module used on exposed type `" ++ name ++ "`."
, details =
[ "It is not necessary to use the module here as `" ++ name ++ "` was exposed on import."
, "You should remove the module from this call, or remove the name from the import .. exposing list."
]
}
range
[ Fix.replaceRangeBy range name ]

View File

@ -0,0 +1,68 @@
module NoModuleOnExposedNames.Context exposing (Module, expose, initial, isFunctionExposed, isTypeExposed)
import Dict exposing (Dict)
import Elm.Syntax.Exposing as Exposing exposing (Exposing)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node)
type Module
= Expose (Dict ModuleName Exposing)
initial : Module
initial =
Expose Dict.empty
expose : ModuleName -> Exposing -> Module -> Module
expose moduleName exposer (Expose exposes) =
Expose (Dict.insert moduleName exposer exposes)
isFunctionExposed : Module -> ModuleName -> String -> Bool
isFunctionExposed (Expose exposes) moduleName name =
case Dict.get moduleName exposes of
Nothing ->
False
Just exposer ->
Exposing.exposesFunction name exposer
isTypeExposed : Module -> ModuleName -> String -> Bool
isTypeExposed (Expose exposes) moduleName name =
case Dict.get moduleName exposes of
Nothing ->
False
Just (Exposing.All _) ->
True
Just (Exposing.Explicit list) ->
List.any (isTypeNamed name) list
--- HELPERS
isTypeNamed : String -> Node Exposing.TopLevelExpose -> Bool
isTypeNamed name topLevelExpose =
(topLevelExpose |> Node.value |> exposingTypeName) == Just name
exposingTypeName : Exposing.TopLevelExpose -> Maybe String
exposingTypeName topLevelExpose =
case topLevelExpose of
Exposing.FunctionExpose _ ->
Nothing
Exposing.TypeOrAliasExpose name ->
Just name
Exposing.TypeExpose { name } ->
Just name
Exposing.InfixExpose _ ->
Nothing

86
tests/NoNoOpMsg.elm Normal file
View File

@ -0,0 +1,86 @@
module NoNoOpMsg exposing (rule)
{-|
@docs rule
-}
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Node as Node exposing (Node)
import Review.Rule as Rule exposing (Error, Rule)
{-| Reports NoOp messages
config =
[ NoNoOpMsg.rule
]
## Fail
type Msg
= NoOp
## Success
type Msg
= DomNodeWasFocused
## When (not) to enable this rule
This rule is not useful when you are working on a package, since you don't have an update function.
## Try it out
You can try this rule out by running the following command:
```bash
elm - review --template jfmengels/elm-review-noop/preview --rules NoNoOpMsg
```
-}
rule : Rule
rule =
Rule.newModuleRuleSchema "NoNoOpMsg" ()
|> Rule.withSimpleDeclarationVisitor declarationVisitor
|> Rule.fromModuleRuleSchema
declarationVisitor : Node Declaration -> List (Error {})
declarationVisitor node =
case Node.value node of
Declaration.CustomTypeDeclaration { constructors } ->
constructors
|> List.map
(\constructor ->
constructor
|> Node.value
|> .name
)
|> List.filter
(\constructorName ->
(Node.value constructorName == "NoOp")
|| (Node.value constructorName == "Noop")
)
|> List.map error
_ ->
[]
error : Node String -> Error {}
error node =
Rule.error
{ message = "Don't use NoOp, give it a better name"
, details =
[ "A Msg name should explain what happened. NoOp means tat nothing happened."
, "Even if you don't care about handling the event, give it a name that describes what happened."
, "@noahzgordon's talk on it: https://www.youtube.com/watch?v=w6OVDBqergc"
]
}
(Node.range node)

78
tests/NoNoOpMsgTest.elm Normal file
View File

@ -0,0 +1,78 @@
module NoNoOpMsgTest exposing (all)
import NoNoOpMsg exposing (rule)
import Review.Test
import Test exposing (Test, describe, test)
message : String
message =
"Don't use NoOp, give it a better name"
details : List String
details =
[ "A Msg name should explain what happened. NoOp means tat nothing happened."
, "Even if you don't care about handling the event, give it a name that describes what happened."
, "@noahzgordon's talk on it: https://www.youtube.com/watch?v=w6OVDBqergc"
]
all : Test
all =
describe "NoNoOpMsg"
[ test "should report an error when there is a NoOp Msg constructor" <|
\() ->
"""module A exposing (..)
type Msg
= NoOp
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "NoOp"
}
]
, test "should report an error when there is a Noop Msg constructor" <|
\() ->
"""module A exposing (..)
type Msg
= Noop
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ Review.Test.error
{ message = message
, details = details
, under = "Noop"
}
]
, test "should not report an error when there is no custom type" <|
\() ->
"""module A exposing (..)
a = 1
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report an error when the constructor name is not NoOp" <|
\() ->
"""module A exposing (..)
type Msg
= Foo
| Bar
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
, test "should not report an error when we see NoOp in pattern matching" <|
\() ->
"""module A exposing (..)
update : Msg -> Model -> Model
update msg model =
case msg of
NoOp -> model
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
]

137
tests/NoRedundantConcat.elm Normal file
View File

@ -0,0 +1,137 @@
module NoRedundantConcat exposing (rule)
{-|
@docs rule
-}
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Infix as Infix
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Range exposing (Range)
import Review.Fix as Fix
import Review.Rule as Rule exposing (Error, Rule)
import Util
{-| Forbids using concatenation (`++` and `List.concat`) when it's not needed.
Expressions like `[ a, b ] ++ c` will be flagged, with a fix proposing to
rewrite that to somethine like `a :: b :: c`. This is more performant and makes
it clear that we're talking about consing items to the head of a list. Easy!
Expressions like `[ a, b ] ++ [ c, d ]` could be rewritten to a single literal
list: no need to perform the concatenation at runtime when we can just write it
ourselves! So, the fix will propose writing that as `[ a, b, c, d ]`.
Expression like `List.concat [ [ a ], [ b ], [ c ] ]` could become `[ a, b c ]`.
Finally, expressions like `"foo" ++ "bar"` can also be `"foobar"`.
To use this rule, add it to your `elm-review` config like so:
module ReviewConfig exposing (config)
import NoRedundantConcat
import Review.Rule exposing (Rule)
config : List Rule
config =
[ NoRedundantConcat.rule
]
-}
rule : Rule
rule =
Rule.newModuleRuleSchema "NoRedundantConcat" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
expressionVisitor : Node Expression -> List (Error {})
expressionVisitor (Node.Node range expression) =
case expression of
Expression.OperatorApplication "++" _ (Node.Node _ (Expression.ListExpr leftItems)) (Node.Node _ (Expression.ListExpr rightItems)) ->
[ Rule.errorWithFix
{ message = "Concatenating a literal list with another literal list can be written as a single list literal"
, details =
[ "Expressions like `[ foo ] ++ [ bar ]` can be written as `[ foo, bar ]`."
, "Using 'complex' expressions when not necessary can make code look a lot more complex than it really is. When you need to put two literal lists together, you can just put them together! No need to have that happen at runtime."
]
}
range
[ combineLists range leftItems rightItems ]
]
Expression.OperatorApplication "++" _ (Node.Node _ (Expression.Literal left)) (Node.Node _ (Expression.Literal right)) ->
[ Rule.errorWithFix
{ message = "Concatenating a literal string with another literal string is redundant"
, details =
[ "Expressions like `\"foo\" ++ \"bar\"` are harder to read than `\"foobar\"`. Consider simplifying this expression."
]
}
range
[ Fix.replaceRangeBy range (Util.expressionToString range (Expression.Literal (left ++ right))) ]
]
Expression.OperatorApplication "++" _ (Node.Node _ (Expression.ListExpr items)) right ->
[ Rule.errorWithFix
{ message = "Concatenating a literal list with something else can be written using cons operators"
, details =
[ "Expressions like `[ foo ] ++ b` can be written as `foo :: b`."
, "This preserves the mental model that `List` is a linked list, with the performance considerations associated with those."
]
}
range
[ concatItems range (List.reverse items) right ]
]
Expression.Application [ Node.Node _ (Expression.FunctionOrValue [ "List" ] "concat"), Node.Node _ (Expression.ListExpr items) ] ->
attemptToCombineItems range items []
_ ->
[]
attemptToCombineItems : Range -> List (Node Expression) -> List (Node Expression) -> List (Error {})
attemptToCombineItems range expressions acc =
case expressions of
[] ->
[ Rule.errorWithFix
{ message = "Using List.concat to concatenate list literals is redundant"
, details = [ "Rather than using `List.concat`, consider putting the elements into a single list literal" ]
}
range
[ Fix.replaceRangeBy range
(Util.expressionToString range (Expression.ListExpr acc))
]
]
(Node.Node _ (Expression.ListExpr items)) :: rest ->
attemptToCombineItems range rest (acc ++ items)
_ ->
[]
combineLists : Range -> List (Node Expression) -> List (Node Expression) -> Fix.Fix
combineLists range left right =
(left ++ right)
|> Expression.ListExpr
|> Util.expressionToString range
|> Fix.replaceRangeBy range
concatItems : Range -> List (Node Expression) -> Node Expression -> Fix.Fix
concatItems range items ((Node.Node ontoRange ontoExpr) as onto) =
case items of
[] ->
Fix.replaceRangeBy range (Util.expressionToString ontoRange ontoExpr)
item :: rest ->
concatItems range
rest
(Node.Node ontoRange
(Expression.OperatorApplication "::" Infix.Non item onto)
)

View File

@ -0,0 +1,161 @@
module NoRedundantConcatTest exposing (..)
import NoRedundantConcat
import Review.Test
import Test exposing (Test, describe, test)
tests : Test
tests =
describe "NoRedundantConcat"
[ test "Simple one" <|
\_ ->
"""module A exposing (..)
a = [ foo ] ++ b
"""
|> Review.Test.run NoRedundantConcat.rule
|> Review.Test.expectErrors
[ useConsError
"""[ foo ] ++ b"""
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo :: b
"""
]
, test "Multiple" <|
\_ ->
"""module A exposing (..)
a = [ foo, bar ] ++ b
"""
|> Review.Test.run NoRedundantConcat.rule
|> Review.Test.expectErrors
[ useConsError
"""[ foo, bar ] ++ b"""
|> Review.Test.whenFixed
"""module A exposing (..)
a = foo :: bar :: b
"""
]
, test "Does not mention concat of non-list literals" <|
\_ ->
"""module A exposing (..)
a = foo ++ bar ++ b
"""
|> Review.Test.run NoRedundantConcat.rule
|> Review.Test.expectNoErrors
, test "Concat of list literals can be single list" <|
\_ ->
"""module A exposing (..)
a = [ foo ] ++ [ bar ]
"""
|> Review.Test.run NoRedundantConcat.rule
|> Review.Test.expectErrors
[ useSingleListError
"""[ foo ] ++ [ bar ]"""
|> Review.Test.whenFixed
"""module A exposing (..)
a = [foo, bar]
"""
]
, test "List.concat of literal lists can be single list" <|
\_ ->
"""module A exposing (..)
a = List.concat [ [ a ], [ b ] ]
"""
|> Review.Test.run NoRedundantConcat.rule
|> Review.Test.expectErrors
[ redundantListConcatError
"""List.concat [ [ a ], [ b ] ]"""
|> Review.Test.whenFixed
"""module A exposing (..)
a = [a, b]
"""
]
, test "List.concat of multiple literal lists can be single list" <|
\_ ->
"""module A exposing (..)
a = List.concat [ [ a, b ], [ c, d ], [ e, f ] ]
"""
|> Review.Test.run NoRedundantConcat.rule
|> Review.Test.expectErrors
[ redundantListConcatError
"""List.concat [ [ a, b ], [ c, d ], [ e, f ] ]"""
|> Review.Test.whenFixed
"""module A exposing (..)
a = [a, b, c, d, e, f]
"""
]
, test "Concatenation of literal strings can be single string" <|
\_ ->
"""module A exposing (..)
a = "hello" ++ " world"
"""
|> Review.Test.run NoRedundantConcat.rule
|> Review.Test.expectErrors
[ redundantStringConcatError
""""hello" ++ " world\""""
|> Review.Test.whenFixed
"""module A exposing (..)
a = "hello world"
"""
]
]
redundantStringConcatError : String -> Review.Test.ExpectedError
redundantStringConcatError under =
Review.Test.error
{ message = "Concatenating a literal string with another literal string is redundant"
, details =
[ "Expressions like `\"foo\" ++ \"bar\"` are harder to read than `\"foobar\"`. Consider simplifying this expression."
]
, under = under
}
redundantListConcatError : String -> Review.Test.ExpectedError
redundantListConcatError under =
Review.Test.error
{ message = "Using List.concat to concatenate list literals is redundant"
, details =
[ "Rather than using `List.concat`, consider putting the elements into a single list literal"
]
, under = under
}
useConsError : String -> Review.Test.ExpectedError
useConsError under =
Review.Test.error
{ message = "Concatenating a literal list with something else can be written using cons operators"
, details =
[ "Expressions like `[ foo ] ++ b` can be written as `foo :: b`."
, "This preserves the mental model that `List` is a linked list, with the performance considerations associated with those."
]
, under = under
}
useSingleListError : String -> Review.Test.ExpectedError
useSingleListError under =
Review.Test.error
{ message = "Concatenating a literal list with another literal list can be written as a single list literal"
, details =
[ "Expressions like `[ foo ] ++ [ bar ]` can be written as `[ foo, bar ]`."
, "Using 'complex' expressions when not necessary can make code look a lot more complex than it really is. When you need to put two literal lists together, you can just put them together! No need to have that happen at runtime."
]
, under = under
}

View File

@ -18,7 +18,6 @@ import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Module as Module exposing (Module)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node)
import Elm.Syntax.Signature exposing (Signature)
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
import Review.Rule as Rule exposing (Error, Rule)
import Scope
@ -104,6 +103,15 @@ I would love help with improving this :)
| B
| C
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-unused/example --rules NoUnused.CustomTypeConstructors
```
-}
rule : List { moduleName : String, typeName : String, index : Int } -> Rule
rule phantomTypes =
@ -418,7 +426,10 @@ declarationVisitor node context =
)
Declaration.FunctionDeclaration function ->
( [], markPhantomTypesFromTypeSignatureAsUsed function.signature context )
( [], markPhantomTypesFromTypeAnnotationAsUsed (Maybe.map (Node.value >> .typeAnnotation) function.signature) context )
Declaration.AliasDeclaration { typeAnnotation } ->
( [], markPhantomTypesFromTypeAnnotationAsUsed (Just typeAnnotation) context )
_ ->
( [], context )
@ -441,12 +452,12 @@ expressionVisitor node moduleContext =
(\declaration ->
case Node.value declaration of
Expression.LetFunction function ->
Just function.signature
Just (Maybe.map (Node.value >> .typeAnnotation) function.signature)
Expression.LetDestructuring _ _ ->
Nothing
)
|> List.foldl markPhantomTypesFromTypeSignatureAsUsed moduleContext
|> List.foldl markPhantomTypesFromTypeAnnotationAsUsed moduleContext
)
_ ->
@ -533,17 +544,17 @@ errorForModule moduleKey node =
-- TYPE ANNOTATION UTILITY FUNCTIONS
markPhantomTypesFromTypeSignatureAsUsed : Maybe (Node Signature) -> ModuleContext -> ModuleContext
markPhantomTypesFromTypeSignatureAsUsed maybeSignature moduleContext =
markPhantomTypesFromTypeAnnotationAsUsed : Maybe (Node TypeAnnotation) -> ModuleContext -> ModuleContext
markPhantomTypesFromTypeAnnotationAsUsed maybeTypeAnnotation moduleContext =
let
used : List ( ModuleName, CustomTypeName )
used =
case maybeSignature of
Just signature ->
signature
|> Node.value
|> .typeAnnotation
|> collectTypesUsedAsPhantomVariables moduleContext.scope moduleContext.phantomVariables
case maybeTypeAnnotation of
Just typeAnnotation ->
collectTypesUsedAsPhantomVariables
moduleContext.scope
moduleContext.phantomVariables
typeAnnotation
Nothing ->
[]

View File

@ -376,6 +376,25 @@ type User = User Something
id : Id User
id = Id
"""
|> Review.Test.runWithProjectData project
(rule
[ { moduleName = "IdModule"
, typeName = "Id"
, index = 0
}
]
)
|> Review.Test.expectNoErrors
, test "should not report a custom type, when it is used aliased by a type alias" <|
\() ->
"""
module MyModule exposing (Thing)
import IdModule exposing (Id)
type ThingType = ThingType
type alias ThingId = Id ThingType
type alias Thing = { id : Id ThingType }
"""
|> Review.Test.runWithProjectData project
(rule

View File

@ -29,6 +29,15 @@ A dependency is considered unused if none of its modules are imported in the pro
[ NoUnused.Dependencies.rule
]
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-unused/example --rules NoUnused.Dependencies
```
-}
rule : Rule
rule =

View File

@ -40,6 +40,15 @@ then nothing will be reported.
[ NoUnused.Exports.rule
]
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-unused/example --rules NoUnused.Exports
```
-}
rule : Rule
rule =

View File

@ -37,6 +37,15 @@ config =
]
```
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-unused/example --rules NoUnused.Modules
```
-}
rule : Rule
rule =

View File

@ -17,7 +17,7 @@ import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range as Range exposing (Range)
import Elm.Writer as Writer
import NoUnused.Patterns.NameVisitor as NameVisitor
import Review.Fix as Fix
import Review.Fix as Fix exposing (Fix)
import Review.Rule as Rule exposing (Rule)
import Set exposing (Set)
@ -49,6 +49,15 @@ Value `something` is not used:
add1 number =
number + 1
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-unused/example --rules NoUnused.Parameters
```
-}
rule : Rule
rule =
@ -310,6 +319,7 @@ errorsForPattern use (Node range pattern) context =
errorsForUselessNamePattern : PatternUse -> Range -> Context -> ( List (Rule.Error {}), Context )
errorsForUselessNamePattern use range context =
let
fix : List Fix
fix =
case use of
Lambda ->
@ -332,6 +342,7 @@ errorsForUselessNamePattern use range context =
errorsForUselessTuple : PatternUse -> Range -> Context -> ( List (Rule.Error {}), Context )
errorsForUselessTuple use range context =
let
fix : List Fix
fix =
case use of
Lambda ->
@ -363,15 +374,19 @@ errorsForRecordValueList use recordRange list context =
firstNode :: restNodes ->
let
first : String
first =
firstNode |> Node.value
Node.value firstNode
rest : List String
rest =
List.map Node.value restNodes
errorRange : Range
errorRange =
Range.combine (List.map Node.range unused)
fix : List Fix
fix =
case ( use, used ) of
( Lambda, [] ) ->
@ -427,6 +442,7 @@ errorsForAsPattern : PatternUse -> Range -> Node Pattern -> Node String -> Conte
errorsForAsPattern use patternRange inner (Node range name) context =
if Set.member name context then
let
fix : List Fix
fix =
case use of
Lambda ->
@ -496,6 +512,7 @@ errorsForValue : PatternUse -> String -> Range -> Context -> ( List (Rule.Error
errorsForValue use value range context =
if Set.member value context then
let
fix : List Fix
fix =
case use of
Lambda ->

View File

@ -16,7 +16,7 @@ import Elm.Syntax.Pattern as Pattern exposing (Pattern)
import Elm.Syntax.Range as Range exposing (Range)
import Elm.Writer as Writer
import NoUnused.Patterns.NameVisitor as NameVisitor
import Review.Fix as Fix
import Review.Fix as Fix exposing (Fix)
import Review.Rule as Rule exposing (Rule)
import Set exposing (Set)
@ -51,6 +51,15 @@ Value `something` is not used:
Nothing ->
False
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-unused/example --rules NoUnused.Patterns
```
-}
rule : Rule
rule =
@ -325,9 +334,11 @@ errorsForRecordValueList recordRange list context =
firstNode :: restNodes ->
let
first : String
first =
firstNode |> Node.value
Node.value firstNode
rest : List String
rest =
List.map Node.value restNodes
@ -384,6 +395,7 @@ errorsForAsPattern : Range -> Node Pattern -> Node String -> Context -> ( List (
errorsForAsPattern patternRange inner (Node range name) context =
if Set.member name context then
let
fix : List Fix
fix =
[ inner
|> Writer.writePattern

View File

@ -50,6 +50,15 @@ import Set exposing (Set)
a n =
n + 1
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-unused/example --rules NoUnused.Variables
```
-}
rule : Rule
rule =
@ -234,6 +243,7 @@ moduleDefinitionVisitor (Node _ moduleNode) context =
Exposing.Explicit list ->
let
names : List String
names =
List.filterMap
(\(Node _ node) ->
@ -430,6 +440,7 @@ expressionExitVisitor node context =
( errors, remainingUsed ) =
makeReport (NonemptyList.head context.scopes)
contextWithPoppedScope : Context
contextWithPoppedScope =
{ context | scopes = NonemptyList.pop context.scopes }
in

View File

@ -41,6 +41,15 @@ that turn out to be unnecessary later.
, view = view
}
## Try it out
You can try this rule out by running the following command:
```bash
elm-review --template jfmengels/review-tea/example --rules NoUselessSubscriptions
```
-}
rule : Rule
rule =

View File

@ -0,0 +1,478 @@
module Tests.NoInconsistentAliases exposing (all)
import NoInconsistentAliases as Rule exposing (rule)
import Review.Test
import Test exposing (Test, describe, test)
all : Test
all =
describe "NoInconsistentAliases"
[ describe "preferred aliases" preferredAliasTests
, describe "noMissingAliases" noMissingAliasesTests
]
noMissingAliasesTests : List Test
noMissingAliasesTests =
[ test "reports missing alias when one should be used" <|
\_ ->
"""
module Page exposing (view)
import Html
import Html.Attributes
view = Html.div [ Html.Attributes.class "container" ] []
"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> Rule.noMissingAliases
|> rule
)
|> Review.Test.expectErrors
[ missingAliasError "Attr" "Html.Attributes"
|> Review.Test.atExactly { start = { row = 4, column = 8 }, end = { row = 4, column = 23 } }
|> Review.Test.whenFixed
"""
module Page exposing (view)
import Html
import Html.Attributes as Attr
view = Html.div [ Attr.class "container" ] []
"""
]
, test "does not report missing aliases when not used" <|
\_ ->
"""
module Page exposing (view)
import Html
import Html.Attributes exposing (class)
view = Html.div [ class "container" ] []
"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> Rule.noMissingAliases
|> rule
)
|> Review.Test.expectNoErrors
, test "does not report missing aliases when the option is not set" <|
\_ ->
"""
module Page exposing (view)
import Html
import Html.Attributes
view = Html.div [ Html.Attributes.class "container" ] []
"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> rule
)
|> Review.Test.expectNoErrors
]
preferredAliasTests : List Test
preferredAliasTests =
[ test "reports incorrect aliases" <|
\_ ->
"""
module Main exposing (main)
import Html
import Html.Attributes as A
main = Html.div [ A.class "container" ] []
"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> rule
)
|> Review.Test.expectErrors
[ incorrectAliasError "Attr" "Html.Attributes" "A"
|> Review.Test.atExactly { start = { row = 4, column = 27 }, end = { row = 4, column = 28 } }
|> Review.Test.whenFixed
"""
module Main exposing (main)
import Html
import Html.Attributes as Attr
main = Html.div [ Attr.class "container" ] []
"""
]
, test "fixes incorrect aliases in a function signature" <|
\_ ->
"""
module Main exposing (main)
import Json.Encode as E
import Page
main : Program E.Value Page.Model Page.Msg
main =
Page.program
"""
|> Review.Test.run
(Rule.config
[ ( "Json.Encode", "Encode" )
]
|> rule
)
|> Review.Test.expectErrors
[ incorrectAliasError "Encode" "Json.Encode" "E"
|> Review.Test.atExactly { start = { row = 3, column = 23 }, end = { row = 3, column = 24 } }
|> Review.Test.whenFixed
"""
module Main exposing (main)
import Json.Encode as Encode
import Page
main : Program Encode.Value Page.Model Page.Msg
main =
Page.program
"""
]
, test "fixes incorrect aliases in a type alias" <|
\_ ->
"""
module Main exposing (main)
import Json.Encode as E
import Page
type alias JsonValue = E.Value
main : Program JsonValue Page.Model Page.Msg
main =
Page.program
"""
|> Review.Test.run
(Rule.config
[ ( "Json.Encode", "Encode" )
]
|> rule
)
|> Review.Test.expectErrors
[ incorrectAliasError "Encode" "Json.Encode" "E"
|> Review.Test.atExactly { start = { row = 3, column = 23 }, end = { row = 3, column = 24 } }
|> Review.Test.whenFixed
"""
module Main exposing (main)
import Json.Encode as Encode
import Page
type alias JsonValue = Encode.Value
main : Program JsonValue Page.Model Page.Msg
main =
Page.program
"""
]
, test "fixes incorrect aliases in a custom type constructor" <|
\_ ->
"""
module Main exposing (main)
import Json.Encode as E
import Page
type JsonValue = JsonValue E.Value
main = Page.main
"""
|> Review.Test.run
(Rule.config
[ ( "Json.Encode", "Encode" )
]
|> rule
)
|> Review.Test.expectErrors
[ incorrectAliasError "Encode" "Json.Encode" "E"
|> Review.Test.atExactly { start = { row = 3, column = 23 }, end = { row = 3, column = 24 } }
|> Review.Test.whenFixed
"""
module Main exposing (main)
import Json.Encode as Encode
import Page
type JsonValue = JsonValue Encode.Value
main = Page.main
"""
]
, test "fixes incorrect aliases in case expressions" <|
\_ ->
"""
module Visitor exposing (expressionVisitor)
import Elm.Syntax.Expression as ESE
import Elm.Syntax.Node as ESN
expressionVisitor : ESN.Node ESE.Expression -> List (Error {})
expressionVisitor node =
case ESN.value node of
ESE.FunctionOrValue _ _ ->
[]
_ ->
[]
"""
|> Review.Test.run
(Rule.config
[ ( "Elm.Syntax.Expression", "Expression" )
, ( "Elm.Syntax.Node", "Node" )
]
|> rule
)
|> Review.Test.expectErrors
[ incorrectAliasError "Expression" "Elm.Syntax.Expression" "ESE"
|> Review.Test.atExactly { start = { row = 3, column = 33 }, end = { row = 3, column = 36 } }
|> Review.Test.whenFixed
"""
module Visitor exposing (expressionVisitor)
import Elm.Syntax.Expression as Expression
import Elm.Syntax.Node as ESN
expressionVisitor : ESN.Node Expression.Expression -> List (Error {})
expressionVisitor node =
case ESN.value node of
Expression.FunctionOrValue _ _ ->
[]
_ ->
[]
"""
, incorrectAliasError "Node" "Elm.Syntax.Node" "ESN"
|> Review.Test.atExactly { start = { row = 4, column = 27 }, end = { row = 4, column = 30 } }
|> Review.Test.whenFixed
"""
module Visitor exposing (expressionVisitor)
import Elm.Syntax.Expression as ESE
import Elm.Syntax.Node as Node
expressionVisitor : Node.Node ESE.Expression -> List (Error {})
expressionVisitor node =
case Node.value node of
ESE.FunctionOrValue _ _ ->
[]
_ ->
[]
"""
]
, test "fixes incorrect aliases in function arguments" <|
\_ ->
"""
module Visitor exposing (getRange)
import Elm.Syntax.Node as ESN
getRange ((ESN.Node range _) as node) = range
"""
|> Review.Test.run
(Rule.config
[ ( "Elm.Syntax.Node", "Node" )
]
|> rule
)
|> Review.Test.expectErrors
[ incorrectAliasError "Node" "Elm.Syntax.Node" "ESN"
|> Review.Test.atExactly { start = { row = 3, column = 27 }, end = { row = 3, column = 30 } }
|> Review.Test.whenFixed
"""
module Visitor exposing (getRange)
import Elm.Syntax.Node as Node
getRange ((Node.Node range _) as node) = range
"""
]
, test "fixes incorrect aliases in let blocks" <|
\_ ->
"""
module Visitor exposing (shiftRange)
import Elm.Syntax.Node as ESN
shiftRange : ( ESN.Node String, Int ) -> { node : ESN.Node String } -> { a | node : ESN.Node String } -> Range
shiftRange input _ _ =
let
asList : ( ESN.Node, Int ) -> List ESN.Node
asList ( node, _ ) = [ node ]
( ESN.Node range1 value1, length1 ) = input
[ ESN.Node range2 value2 ] = asList input
ESN.Node range3 value3 :: [] = asList input
in
range
"""
|> Review.Test.run
(Rule.config
[ ( "Elm.Syntax.Node", "Node" )
]
|> rule
)
|> Review.Test.expectErrors
[ incorrectAliasError "Node" "Elm.Syntax.Node" "ESN"
|> Review.Test.atExactly { start = { row = 3, column = 27 }, end = { row = 3, column = 30 } }
|> Review.Test.whenFixed
"""
module Visitor exposing (shiftRange)
import Elm.Syntax.Node as Node
shiftRange : ( Node.Node String, Int ) -> { node : Node.Node String } -> { a | node : Node.Node String } -> Range
shiftRange input _ _ =
let
asList : ( Node.Node, Int ) -> List Node.Node
asList ( node, _ ) = [ node ]
( Node.Node range1 value1, length1 ) = input
[ Node.Node range2 value2 ] = asList input
Node.Node range3 value3 :: [] = asList input
in
range
"""
]
, test "fixes incorrect aliases in a lambda function" <|
\_ ->
"""
module NoCode exposing (visitor)
import Elm.Syntax.Node as ESN
import Review.Rule as Review
visitor : List (ESN.Node String) -> List (Review.Error {})
visitor list =
List.map (\\ESN.Node range value -> Review.error value) list
"""
|> Review.Test.run
(Rule.config
[ ( "Elm.Syntax.Node", "Node" )
, ( "Review.Rule", "Rule" )
]
|> rule
)
|> Review.Test.expectErrors
[ incorrectAliasError "Node" "Elm.Syntax.Node" "ESN"
|> Review.Test.atExactly { start = { row = 3, column = 27 }, end = { row = 3, column = 30 } }
|> Review.Test.whenFixed
"""
module NoCode exposing (visitor)
import Elm.Syntax.Node as Node
import Review.Rule as Review
visitor : List (Node.Node String) -> List (Review.Error {})
visitor list =
List.map (\\Node.Node range value -> Review.error value) list
"""
, incorrectAliasError "Rule" "Review.Rule" "Review"
|> Review.Test.atExactly { start = { row = 4, column = 23 }, end = { row = 4, column = 29 } }
|> Review.Test.whenFixed
"""
module NoCode exposing (visitor)
import Elm.Syntax.Node as ESN
import Review.Rule as Rule
visitor : List (ESN.Node String) -> List (Rule.Error {})
visitor list =
List.map (\\ESN.Node range value -> Rule.error value) list
"""
]
, test "does not offer a fix when there's an alias collision" <|
\_ ->
"""
module Page exposing (view)
import Html.Attributes as A
import Svg.Attributes as Attr
view = div [ A.class "container" ] []
"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> rule
)
|> Review.Test.expectErrors
[ aliasCollisionError "Attr" "Html.Attributes" "A" "Svg.Attributes"
|> Review.Test.atExactly { start = { row = 3, column = 27 }, end = { row = 3, column = 28 } }
]
, test "does not offer a fix when there's a module collision" <|
\_ ->
"""
module Page exposing (view)
import Html.Attributes as A
import Attr
view = div [ A.class "container" ] []
"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> rule
)
|> Review.Test.expectErrors
[ aliasCollisionError "Attr" "Html.Attributes" "A" "Attr"
|> Review.Test.atExactly { start = { row = 3, column = 27 }, end = { row = 3, column = 28 } }
]
, test "does not report when there's a collision with another preferred alias" <|
\_ ->
"""
module Page exposing (view)
import Html.Attributes as A
import Svg.Attributes as Attr
view = div [ A.class "container" ] []
"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
, ( "Svg.Attributes", "Attr" )
]
|> rule
)
|> Review.Test.expectNoErrors
, test "does not report modules imported with no alias" <|
\_ ->
"""
module Main exposing (main)
import Html.Attributes
main = 1"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> rule
)
|> Review.Test.expectNoErrors
, test "does not report modules with the correct alias" <|
\_ ->
"""
module Main exposing (main)
import Html.Attributes as Attr
main = 1"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> rule
)
|> Review.Test.expectNoErrors
, test "does not report modules with no preferred alias" <|
\_ ->
"""
module Main exposing (main)
import Json.Encode as E
main = 1"""
|> Review.Test.run
(Rule.config
[ ( "Html.Attributes", "Attr" )
]
|> rule
)
|> Review.Test.expectNoErrors
]
incorrectAliasError : String -> String -> String -> Review.Test.ExpectedError
incorrectAliasError expectedAlias moduleName wrongAlias =
Review.Test.error
{ message = "Incorrect alias `" ++ wrongAlias ++ "` for module `" ++ moduleName ++ "`."
, details =
[ "This import does not use your preferred alias `" ++ expectedAlias ++ "` for `" ++ moduleName ++ "`."
, "You should update the alias to be consistent with the rest of the project. Remember to change all references to the alias in this module too."
]
, under = wrongAlias
}
aliasCollisionError : String -> String -> String -> String -> Review.Test.ExpectedError
aliasCollisionError expectedAlias moduleName wrongAlias collisionName =
Review.Test.error
{ message = "Incorrect alias `" ++ wrongAlias ++ "` for module `" ++ moduleName ++ "`."
, details =
[ "This import does not use your preferred alias `" ++ expectedAlias ++ "` for `" ++ moduleName ++ "`."
, "Your preferred alias has already been taken by `" ++ collisionName ++ "`."
, "You should change the alias for both modules to be consistent with the rest of the project. Remember to change all references to the alias in this module too."
]
, under = wrongAlias
}
missingAliasError : String -> String -> Review.Test.ExpectedError
missingAliasError expectedAlias moduleName =
Review.Test.error
{ message = "Expected alias `" ++ expectedAlias ++ "` missing for module `" ++ moduleName ++ "`."
, details =
[ "This import does not use your preferred alias `" ++ expectedAlias ++ "` for `" ++ moduleName ++ "`."
, "You should update the alias to be consistent with the rest of the project. Remember to change all references to the alias in this module too."
]
, under = moduleName
}

View File

@ -0,0 +1,85 @@
module Tests.NoModuleOnExposedNames exposing (all)
import NoModuleOnExposedNames exposing (rule)
import Review.Test
import Test exposing (Test, describe, test)
all : Test
all =
describe "NoModuleOnExposedNames"
[ test "reports modules used on exposed values" <|
\_ ->
"""
module Page exposing (view)
import Html.Attributes as Attr exposing (class)
view children =
div [ Attr.class "container" ] children
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ moduleOnExposedValueError "Attr.class" "class"
|> Review.Test.whenFixed
"""
module Page exposing (view)
import Html.Attributes as Attr exposing (class)
view children =
div [ class "container" ] children
"""
]
, test "reports modules used on exposed types" <|
\_ ->
"""
module Page exposing (view)
import Html exposing (Html, Attribute)
view : List (Html.Attribute msg) -> Html msg
view children =
Html.div [] children
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ moduleOnExposedTypeError "Html.Attribute" "Attribute"
|> Review.Test.whenFixed
"""
module Page exposing (view)
import Html exposing (Html, Attribute)
view : List (Attribute msg) -> Html msg
view children =
Html.div [] children
"""
]
, test "does not report names not exposed" <|
\_ ->
"""
module Page exposing (view)
import Html.Attributes as Attr
view children =
div [ Attr.class "container" ] children
"""
|> Review.Test.run rule
|> Review.Test.expectNoErrors
]
moduleOnExposedValueError : String -> String -> Review.Test.ExpectedError
moduleOnExposedValueError call name =
Review.Test.error
{ message = "Module used on exposed value `" ++ name ++ "`."
, details =
[ "It is not necessary to use the module here as `" ++ name ++ "` was exposed on import."
, "You should remove the module from this call, or remove the name from the import .. exposing list."
]
, under = call
}
moduleOnExposedTypeError : String -> String -> Review.Test.ExpectedError
moduleOnExposedTypeError call name =
Review.Test.error
{ message = "Module used on exposed type `" ++ name ++ "`."
, details =
[ "It is not necessary to use the module here as `" ++ name ++ "` was exposed on import."
, "You should remove the module from this call, or remove the name from the import .. exposing list."
]
, under = call
}

View File

@ -0,0 +1,81 @@
module Tests.NoModuleOnExposedNames.Context exposing (all)
import Elm.Syntax.Exposing as Exposing
import Elm.Syntax.Node exposing (Node(..))
import Elm.Syntax.Range as Range
import Expect
import NoModuleOnExposedNames.Context as Context
import Test exposing (Test, describe, test)
all : Test
all =
describe "NoModuleOnExposedNames.Context"
[ describe "isTypeExposed"
[ test "is False with no imports" <|
\_ ->
Context.isTypeExposed Context.initial [ "Html" ] "Attribute"
|> Expect.equal False
, test "is True with Exposing.All" <|
\_ ->
let
context =
Context.initial
|> Context.expose [ "Html" ] (Exposing.All Range.emptyRange)
in
Context.isTypeExposed context [ "Html" ] "Attribute"
|> Expect.equal True
, test "is True with matching TypeExpose" <|
\_ ->
let
context =
Context.initial
|> Context.expose [ "Html" ] (Exposing.Explicit [ typeExpose "Attribute" ])
in
Context.isTypeExposed context [ "Html" ] "Attribute"
|> Expect.equal True
, test "is True with matching TypeOrAliasExpose" <|
\_ ->
let
context =
Context.initial
|> Context.expose [ "Html" ] (Exposing.Explicit [ typeOrAliasExpose "Attribute" ])
in
Context.isTypeExposed context [ "Html" ] "Attribute"
|> Expect.equal True
, test "is False with matching FunctionExpose" <|
\_ ->
let
context =
Context.initial
|> Context.expose [ "Html" ] (Exposing.Explicit [ functionExpose "div" ])
in
-- Functions are not types
Context.isTypeExposed context [ "Html" ] "div"
|> Expect.equal False
, test "is False with no matching TypeExpose" <|
\_ ->
let
context =
Context.initial
|> Context.expose [ "Html" ] (Exposing.Explicit [ typeExpose "Html" ])
in
Context.isTypeExposed context [ "Html" ] "Attribute"
|> Expect.equal False
]
]
functionExpose : String -> Node Exposing.TopLevelExpose
functionExpose name =
Node Range.emptyRange (Exposing.FunctionExpose name)
typeExpose : String -> Node Exposing.TopLevelExpose
typeExpose name =
Node Range.emptyRange (Exposing.TypeExpose { name = name, open = Nothing })
typeOrAliasExpose : String -> Node Exposing.TopLevelExpose
typeOrAliasExpose name =
Node Range.emptyRange (Exposing.TypeOrAliasExpose name)

618
tests/Tests/Vendor/NameVisitor.elm vendored Normal file
View File

@ -0,0 +1,618 @@
module Tests.Vendor.NameVisitor exposing (all)
import Elm.Syntax.ModuleName exposing (ModuleName)
import Elm.Syntax.Node as Node exposing (Node)
import Fuzz
import Review.Rule as Rule exposing (Error, Rule)
import Review.Test
import Test exposing (Test, describe, fuzz, test)
import Vendor.NameVisitor as NameVisitor
all : Test
all =
describe "NameVisitor"
[ describe "Context" contextTests
, describe "Declarations" declarationTests
, describe "Expressions" expressionTests
, describe "Patterns" patternTests
, describe "Type Annotations" typeAnnotationTests
, describe "Visitors" visitorTests
, describe "Project Rules" projectRuleTests
]
declarationTests : List Test
declarationTests =
[ fuzz Fuzz.string "FunctionDeclaration" <|
\context ->
"""
module Page exposing (view)
view : Node (Html.Attribute msg) -> Html msg
view (Node attribute) = Html.div [ attribute ] []
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Node"
|> Review.Test.atExactly { start = { row = 3, column = 8 }, end = { row = 3, column = 12 } }
, expectedTypeError context "Html.Attribute"
, expectedTypeError context "Html"
|> Review.Test.atExactly { start = { row = 3, column = 37 }, end = { row = 3, column = 41 } }
, expectedValueError context "Node"
|> Review.Test.atExactly { start = { row = 4, column = 7 }, end = { row = 4, column = 11 } }
, expectedValueError context "Html.div"
, expectedValueError context "attribute"
|> Review.Test.atExactly { start = { row = 4, column = 36 }, end = { row = 4, column = 45 } }
]
, fuzz Fuzz.string "AliasDeclaration" <|
\context ->
"""
module Page exposing (Page)
type alias Page =
{ title : String
, body : List (Html msg)
}
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "String"
, expectedTypeError context "List"
, expectedTypeError context "Html"
]
, fuzz Fuzz.string "CustomTypeDeclaration" <|
\context ->
"""
module Page exposing (Page)
type Page
= Home Route.Home
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Route.Home"
]
, fuzz Fuzz.string "PortDeclaration" <|
\context ->
"""
port module Ports exposing (alarm)
port alarm : Json.Encode.Value -> Cmd msg
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Json.Encode.Value"
, expectedTypeError context "Cmd"
]
]
expressionTests : List Test
expressionTests =
[ fuzz Fuzz.string "FunctionOrValue" <|
\context ->
"""
module Page exposing (view)
view = Html.div [] []
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Html.div"
]
, fuzz Fuzz.string "LetDestructuring" <|
\context ->
"""
module Page exposing (view)
view =
let
html = Html.span [] []
in
html
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Html.span"
, expectedValueError context "html"
|> Review.Test.atExactly { start = { row = 7, column = 5 }, end = { row = 7, column = 9 } }
]
, fuzz Fuzz.string "LetFunction" <|
\context ->
"""
module Page exposing (view)
view =
let
html : Nested.Html msg
html (Nested.Node name) = span [] []
in
html
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Nested.Html"
, expectedValueError context "Nested.Node"
, expectedValueError context "span"
, expectedValueError context "html"
|> Review.Test.atExactly { start = { row = 8, column = 5 }, end = { row = 8, column = 9 } }
]
, fuzz Fuzz.string "CaseExpression" <|
\context ->
"""
module Rule exposing (visitor)
visitor node =
case Node.value node of
Expression.FunctionOrValue _ _ ->
[]
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Node.value"
, expectedValueError context "node"
|> Review.Test.atExactly { start = { row = 4, column = 21 }, end = { row = 4, column = 25 } }
, expectedValueError context "Expression.FunctionOrValue"
]
, fuzz Fuzz.string "LambdaExpression" <|
\context ->
"""
module Page exposing (view)
view nodes =
Html.div [] (List.map (\\(Node name) -> Html.text name))
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Html.div"
, expectedValueError context "List.map"
, expectedValueError context "Node"
, expectedValueError context "Html.text"
, expectedValueError context "name"
|> Review.Test.atExactly { start = { row = 4, column = 54 }, end = { row = 4, column = 58 } }
]
, fuzz Fuzz.string "RecordAccess" <|
\context ->
"""
module Page exposing (view)
value model =
model.value
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "model"
|> Review.Test.atExactly { start = { row = 4, column = 5 }, end = { row = 4, column = 10 } }
]
, fuzz Fuzz.string "RecordUpdateExpression" <|
\context ->
"""
module Page exposing (view)
update model =
{ model | key = value }
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "model"
|> Review.Test.atExactly { start = { row = 4, column = 7 }, end = { row = 4, column = 12 } }
, expectedValueError context "value"
]
]
patternTests : List Test
patternTests =
[ fuzz Fuzz.string "TuplePattern" <|
\context ->
"""
module Page exposing (view)
view ( Nested.Node name, Nested.Value value ) =
Html.div [] [ Html.text (name ++ value) ]
"""
|> Review.Test.run (valueVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Nested.Node"
, expectedValueError context "Nested.Value"
, expectedValueError context "Html.div"
, expectedValueError context "Html.text"
, expectedValueError context "name"
|> Review.Test.atExactly { start = { row = 4, column = 30 }, end = { row = 4, column = 34 } }
, expectedValueError context "value"
|> Review.Test.atExactly { start = { row = 4, column = 38 }, end = { row = 4, column = 43 } }
]
, fuzz Fuzz.string "UnConsPattern" <|
\context ->
"""
module Page exposing (list)
list children =
case children of
(Page.First first) :: (Page.Second second) :: _ ->
Html.text ""
"""
|> Review.Test.run (valueVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "children"
|> Review.Test.atExactly { start = { row = 4, column = 10 }, end = { row = 4, column = 18 } }
, expectedValueError context "Page.First"
, expectedValueError context "Page.Second"
, expectedValueError context "Html.text"
]
, fuzz Fuzz.string "ListPattern" <|
\context ->
"""
module Page exposing (list)
list children =
case children of
[ Page.First first, Page.Second second ] ->
Html.text ""
"""
|> Review.Test.run (valueVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "children"
|> Review.Test.atExactly { start = { row = 4, column = 10 }, end = { row = 4, column = 18 } }
, expectedValueError context "Page.First"
, expectedValueError context "Page.Second"
, expectedValueError context "Html.text"
]
, fuzz Fuzz.string "NamedPattern" <|
\context ->
"""
module Page exposing (view)
view (Node name) = Html.text name
"""
|> Review.Test.run (valueVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Node"
, expectedValueError context "Html.text"
, expectedValueError context "name"
|> Review.Test.atExactly { start = { row = 3, column = 30 }, end = { row = 3, column = 34 } }
]
, fuzz Fuzz.string "AsPattern" <|
\context ->
"""
module Page exposing (view)
view ((Page.Document title _) as doc) = Html.text title
"""
|> Review.Test.run (valueVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Page.Document"
, expectedValueError context "Html.text"
, expectedValueError context "title"
|> Review.Test.atExactly { start = { row = 3, column = 51 }, end = { row = 3, column = 56 } }
]
, fuzz Fuzz.string "ParenthesizedPattern" <|
\context ->
"""
module Page exposing (view)
view (Nested.Node name) = Html.text name
"""
|> Review.Test.run (valueVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Nested.Node"
, expectedValueError context "Html.text"
, expectedValueError context "name"
|> Review.Test.atExactly { start = { row = 3, column = 37 }, end = { row = 3, column = 41 } }
]
]
typeAnnotationTests : List Test
typeAnnotationTests =
[ fuzz Fuzz.string "Types" <|
\context ->
"""
module Page exposing (view)
view : Nested.Node Page.Document msg -> Html msg
view node = div [] []
"""
|> Review.Test.run (typeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Nested.Node"
, expectedTypeError context "Page.Document"
, expectedTypeError context "Html"
]
, fuzz Fuzz.string "Tupled" <|
\context ->
"""
module Page exposing (Page)
type Page
= Page ( Document.Title, Document.List Document.Item )
"""
|> Review.Test.run (typeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Document.Title"
, expectedTypeError context "Document.List"
, expectedTypeError context "Document.Item"
]
, fuzz Fuzz.string "Record" <|
\context ->
"""
module Page exposing (Page)
type Page
= Page { title : Document.Title, body : Document.List Document.Item }
"""
|> Review.Test.run (typeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Document.Title"
, expectedTypeError context "Document.List"
, expectedTypeError context "Document.Item"
]
, fuzz Fuzz.string "GenericRecord" <|
\context ->
"""
module Page exposing (Page)
type Page
= Page { page | title : Document.Title, body : Document.List Document.Item }
"""
|> Review.Test.run (typeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Document.Title"
, expectedTypeError context "Document.List"
, expectedTypeError context "Document.Item"
]
, fuzz Fuzz.string "FunctionTypeAnnotation" <|
\context ->
"""
module Page exposing (Viewer)
type Viewer msg
= Viewer (Document.Title -> Document.List Document.Item -> Html msg)
"""
|> Review.Test.run (typeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Document.Title"
, expectedTypeError context "Document.List"
, expectedTypeError context "Document.Item"
, expectedTypeError context "Html"
]
]
visitorTests : List Test
visitorTests =
[ fuzz Fuzz.string "withNameVisitor" <|
\context ->
"""
module Page exposing (view)
view : Page -> Html msg
view page =
Html.div [] (Page.body page)
"""
|> Review.Test.run (nameVisitorRule context)
|> Review.Test.expectErrors
[ expectedNameError context "Page"
|> Review.Test.atExactly { start = { row = 3, column = 8 }, end = { row = 3, column = 12 } }
, expectedNameError context "Html"
|> Review.Test.atExactly { start = { row = 3, column = 16 }, end = { row = 3, column = 20 } }
, expectedNameError context "Html.div"
, expectedNameError context "Page.body"
, expectedNameError context "page"
|> Review.Test.atExactly { start = { row = 5, column = 28 }, end = { row = 5, column = 32 } }
]
, fuzz Fuzz.string "withValueVisitor" <|
\context ->
"""
module Page exposing (view)
view : Page -> Html msg
view page =
Html.div [] (Page.body page)
"""
|> Review.Test.run (valueVisitorRule context)
|> Review.Test.expectErrors
[ expectedValueError context "Html.div"
, expectedValueError context "Page.body"
, expectedValueError context "page"
|> Review.Test.atExactly { start = { row = 5, column = 28 }, end = { row = 5, column = 32 } }
]
, fuzz Fuzz.string "withTypeVisitor" <|
\context ->
"""
module Page exposing (view)
view : Page -> Html msg
view page =
Html.div [] (Page.body page)
"""
|> Review.Test.run (typeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Page"
|> Review.Test.atExactly { start = { row = 3, column = 8 }, end = { row = 3, column = 12 } }
, expectedTypeError context "Html"
|> Review.Test.atExactly { start = { row = 3, column = 16 }, end = { row = 3, column = 20 } }
]
, fuzz Fuzz.string "withValueOrTypeVisitor" <|
\context ->
"""
module Page exposing (view)
view : Page -> Html msg
view page =
Html.div [] (Page.body page)
"""
|> Review.Test.run (valueOrTypeVisitorRule context)
|> Review.Test.expectErrors
[ expectedTypeError context "Page"
|> Review.Test.atExactly { start = { row = 3, column = 8 }, end = { row = 3, column = 12 } }
, expectedTypeError context "Html"
|> Review.Test.atExactly { start = { row = 3, column = 16 }, end = { row = 3, column = 20 } }
, expectedValueError context "Html.div"
, expectedValueError context "Page.body"
, expectedValueError context "page"
|> Review.Test.atExactly { start = { row = 5, column = 28 }, end = { row = 5, column = 32 } }
]
]
contextTests : List Test
contextTests =
[ test "mutates context" <|
\_ ->
"""
module Page exposing (Page)
view : Page -> Html msg
view page =
Html.div [] (Page.body page)
"""
|> Review.Test.run (countingVisitorRule 0)
|> Review.Test.expectErrors
[ expectedNameError "0" "Page"
|> Review.Test.atExactly { start = { row = 3, column = 8 }, end = { row = 3, column = 12 } }
, expectedNameError "1" "Html"
|> Review.Test.atExactly { start = { row = 3, column = 16 }, end = { row = 3, column = 20 } }
, expectedNameError "2" "Html.div"
, expectedNameError "3" "Page.body"
, expectedNameError "4" "page"
|> Review.Test.atExactly { start = { row = 5, column = 28 }, end = { row = 5, column = 32 } }
]
]
projectRuleTests : List Test
projectRuleTests =
[ test "works in a project rule schema" <|
\() ->
let
rule : Rule
rule =
Rule.newProjectRuleSchema "TestProjectRule" ()
|> Rule.withModuleVisitor
(\schema ->
schema
|> NameVisitor.withNameVisitor nameVisitor
)
|> Rule.withModuleContext
{ foldProjectContexts = \() () -> ()
, fromProjectToModule = \_ _ () -> "context"
, fromModuleToProject = \_ _ _ -> ()
}
|> Rule.fromProjectRuleSchema
in
"""
module A exposing (..)
foo = bar
"""
|> Review.Test.run rule
|> Review.Test.expectErrors
[ expectedNameError "context" "bar"
]
]
-- TEST HELPERS
expectedNameError : String -> String -> Review.Test.ExpectedError
expectedNameError context name =
Review.Test.error
{ message = "Error for name `" ++ name ++ "`."
, details = [ "The context of this error was:", context ]
, under = name
}
expectedValueError : String -> String -> Review.Test.ExpectedError
expectedValueError context name =
Review.Test.error
{ message = "Error for value `" ++ name ++ "`."
, details = [ "The context of this error was:", context ]
, under = name
}
expectedTypeError : String -> String -> Review.Test.ExpectedError
expectedTypeError context name =
Review.Test.error
{ message = "Error for type `" ++ name ++ "`."
, details = [ "The context of this error was:", context ]
, under = name
}
-- RULE HELPERS
nameVisitorRule : String -> Rule
nameVisitorRule context =
Rule.newModuleRuleSchema "NameVisitor" context
|> NameVisitor.withNameVisitor nameVisitor
|> Rule.fromModuleRuleSchema
valueOrTypeVisitorRule : String -> Rule
valueOrTypeVisitorRule context =
Rule.newModuleRuleSchema "ValueOrTypeVisitor" context
|> NameVisitor.withValueAndTypeVisitors
{ valueVisitor = valueVisitor
, typeVisitor = typeVisitor
}
|> Rule.fromModuleRuleSchema
valueVisitorRule : String -> Rule
valueVisitorRule context =
Rule.newModuleRuleSchema "ValueVisitor" context
|> NameVisitor.withValueVisitor valueVisitor
|> Rule.fromModuleRuleSchema
typeVisitorRule : String -> Rule
typeVisitorRule context =
Rule.newModuleRuleSchema "TypeVisitor" context
|> NameVisitor.withTypeVisitor typeVisitor
|> Rule.fromModuleRuleSchema
nameVisitor : Node ( ModuleName, String ) -> String -> ( List (Error {}), String )
nameVisitor node context =
( [ nameError context node ], context )
valueVisitor : Node ( ModuleName, String ) -> String -> ( List (Error {}), String )
valueVisitor node context =
( [ valueError context node ], context )
typeVisitor : Node ( ModuleName, String ) -> String -> ( List (Error {}), String )
typeVisitor node context =
( [ typeError context node ], context )
countingVisitorRule : Int -> Rule
countingVisitorRule counter =
Rule.newModuleRuleSchema "NameVisitor" counter
|> NameVisitor.withNameVisitor countingVisitor
|> Rule.fromModuleRuleSchema
countingVisitor : Node ( ModuleName, String ) -> Int -> ( List (Error {}), Int )
countingVisitor node counter =
( [ nameError (String.fromInt counter) node ], counter + 1 )
nameError : String -> Node ( ModuleName, String ) -> Error {}
nameError context node =
Rule.error
{ message = "Error for name `" ++ formatNode node ++ "`."
, details = [ "The context of this error was:", context ]
}
(Node.range node)
valueError : String -> Node ( ModuleName, String ) -> Error {}
valueError context node =
Rule.error
{ message = "Error for value `" ++ formatNode node ++ "`."
, details = [ "The context of this error was:", context ]
}
(Node.range node)
typeError : String -> Node ( ModuleName, String ) -> Error {}
typeError context node =
Rule.error
{ message = "Error for type `" ++ formatNode node ++ "`."
, details = [ "The context of this error was:", context ]
}
(Node.range node)
formatNode : Node ( ModuleName, String ) -> String
formatNode node =
node
|> Node.value
|> formatTuple
formatTuple : ( ModuleName, String ) -> String
formatTuple ( moduleName, name ) =
String.join "." (moduleName ++ [ name ])

View File

@ -1,4 +1,4 @@
module NoLeftPizzaUtil exposing (expressionToString)
module Util exposing (expressionToString)
import Elm.Syntax.Expression as Expression exposing (Expression)
import Elm.Syntax.Node as Node exposing (Node)

475
tests/Vendor/NameVisitor.elm vendored Normal file
View File

@ -0,0 +1,475 @@
module Vendor.NameVisitor exposing (withNameVisitor, withValueVisitor, withTypeVisitor, withValueAndTypeVisitors)
{-| Visit each name in the module.
A "name" is a `Node ( ModuleName, String )` and represents a value or type reference. Here are some examples:
- `Json.Encode.Value` -> `( [ "Json", "Encode" ], "Value" )`
- `Html.Attributes.class` -> `( [ "Html", "Attributes" ], "class" )`
- `Page` -> `( [], "Page" )`
- `view` -> `( [], "view" )`
These can appear in many places throughout declarations and expressions, and picking them out each time is a lot of work. Instead of writing 1000 lines of code and tests each time, you can write one `nameVisitor` and plug it straight into your module schema, or separate `valueVisitor` and `typeVisitor`s.
@docs withNameVisitor, withValueVisitor, withTypeVisitor, withValueAndTypeVisitors
## Scope
This makes no attempt to resolve module names from imports, it just returns what's written in the code. It would be trivial to connect [elm-review-scope] with the name visitor if you want to do this.
[elm-review-scope]: http://github.com/jfmengels/elm-review-scope/
## Version
Version: 0.3.0
-}
import Elm.Syntax.Declaration as Declaration exposing (Declaration)
import Elm.Syntax.Expression as Expression exposing (Expression)
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.Signature exposing (Signature)
import Elm.Syntax.Type as Type
import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation)
import Review.Rule as Rule exposing (Error)
type Visitor context
= NameVisitor (VisitorFunction context)
| ValueVisitor (VisitorFunction context)
| TypeVisitor (VisitorFunction context)
| ValueAndTypeVisitor (VisitorFunction context) (VisitorFunction context)
type alias VisitorFunction context =
Node ( ModuleName, String ) -> context -> ( List (Error {}), context )
type Name
= Value (Node ( ModuleName, String ))
| Type (Node ( ModuleName, String ))
{-| This will apply the `nameVisitor` to every value and type in the module, you will get no information about whether the name is a value or type.
rule : Rule
rule =
Rule.newModuleRuleSchema "NoInconsistentAliases" initialContext
|> NameVisitor.withNameVisitor nameVisitor
|> Rule.fromModuleRuleSchema
nameVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context )
nameVisitor node context =
-- Do what you want with the name
( [], context )
-}
withNameVisitor :
(Node ( ModuleName, String ) -> context -> ( List (Error {}), context ))
-> Rule.ModuleRuleSchema state context
-> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } context
withNameVisitor nameVisitor rule =
let
visitor =
NameVisitor nameVisitor
in
rule
|> Rule.withDeclarationListVisitor (declarationListVisitor visitor)
|> Rule.withExpressionEnterVisitor (expressionVisitor visitor)
{-| This will apply the `valueVisitor` to every value in the module, and ignore any types.
rule : Rule
rule =
Rule.newModuleRuleSchema "NoInconsistentAliases" initialContext
|> NameVisitor.withValueVisitor valueVisitor
|> Rule.fromModuleRuleSchema
valueVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context )
valueVisitor node context =
-- Do what you want with the value
( [], context )
-}
withValueVisitor :
(Node ( ModuleName, String ) -> context -> ( List (Error {}), context ))
-> Rule.ModuleRuleSchema state context
-> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } context
withValueVisitor valueVisitor rule =
let
visitor =
ValueVisitor valueVisitor
in
rule
|> Rule.withDeclarationListVisitor (declarationListVisitor visitor)
|> Rule.withExpressionEnterVisitor (expressionVisitor visitor)
{-| This will apply the `typeVisitor` to every type in the module, and ignore any values.
rule : Rule
rule =
Rule.newModuleRuleSchema "NoInconsistentAliases" initialContext
|> NameVisitor.withTypeVisitor typeVisitor
|> Rule.fromModuleRuleSchema
typeVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context )
typeVisitor node context =
-- Do what you want with the type
( [], context )
-}
withTypeVisitor :
(Node ( ModuleName, String ) -> context -> ( List (Error {}), context ))
-> Rule.ModuleRuleSchema state context
-> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } context
withTypeVisitor typeVisitor rule =
let
visitor =
TypeVisitor typeVisitor
in
rule
|> Rule.withDeclarationListVisitor (declarationListVisitor visitor)
|> Rule.withExpressionEnterVisitor (expressionVisitor visitor)
{-| This will apply the `valueVisitor` to every value and the `typeVisitor` to every type in the module.
rule : Rule
rule =
Rule.newModuleRuleSchema "NoInconsistentAliases" initialContext
|> NameVisitor.withValueAndTypeVisitors
{ valueVisitor = valueVisitor
, typeVisitor = typeVisitor
}
|> Rule.fromModuleRuleSchema
valueVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context )
valueVisitor node context =
-- Do what you want with the value
( [], context )
typeVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context )
typeVisitor node context =
-- Do what you want with the type
( [], context )
-}
withValueAndTypeVisitors :
{ valueVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context )
, typeVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context )
}
-> Rule.ModuleRuleSchema state context
-> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } context
withValueAndTypeVisitors { valueVisitor, typeVisitor } rule =
let
visitor =
ValueAndTypeVisitor valueVisitor typeVisitor
in
rule
|> Rule.withDeclarationListVisitor (declarationListVisitor visitor)
|> Rule.withExpressionEnterVisitor (expressionVisitor visitor)
--- VISITORS
declarationListVisitor :
Visitor context
-> (List (Node Declaration) -> context -> ( List (Error {}), context ))
declarationListVisitor visitor list context =
visitDeclarationList list
|> folder visitor context
expressionVisitor : Visitor context -> (Node Expression -> context -> ( List (Error {}), context ))
expressionVisitor visitor node context =
visitExpression node
|> folder visitor context
--- FOLDER
folder :
Visitor context
-> context
-> List Name
-> ( List (Error {}), context )
folder visitor context list =
List.foldl (folderHelper visitor) ( [], context ) list
folderHelper :
Visitor context
-> Name
-> ( List (Error {}), context )
-> ( List (Error {}), context )
folderHelper visitor name ( errors, context ) =
let
( newErrors, newContext ) =
applyVisitor visitor name context
in
( newErrors ++ errors, newContext )
applyVisitor : Visitor context -> Name -> context -> ( List (Error {}), context )
applyVisitor visitor name context =
case name of
Value node ->
applyValueVisitor visitor node context
Type node ->
applyTypeVisitor visitor node context
applyValueVisitor : Visitor context -> VisitorFunction context
applyValueVisitor visitor =
case visitor of
NameVisitor function ->
function
ValueVisitor function ->
function
TypeVisitor _ ->
noopVisitor
ValueAndTypeVisitor function _ ->
function
applyTypeVisitor : Visitor context -> VisitorFunction context
applyTypeVisitor visitor =
case visitor of
NameVisitor function ->
function
ValueVisitor _ ->
noopVisitor
TypeVisitor function ->
function
ValueAndTypeVisitor _ function ->
function
noopVisitor : VisitorFunction context
noopVisitor _ context =
( [], context )
--- PRIVATE
visitDeclarationList : List (Node Declaration) -> List Name
visitDeclarationList nodes =
fastConcatMap visitDeclaration nodes
visitDeclaration : Node Declaration -> List Name
visitDeclaration node =
case Node.value node of
Declaration.FunctionDeclaration { signature, declaration } ->
visitMaybeSignature signature
++ visitFunctionImplementation declaration
Declaration.AliasDeclaration { typeAnnotation } ->
visitTypeAnnotation typeAnnotation
Declaration.CustomTypeDeclaration { constructors } ->
visitValueConstructorList constructors
Declaration.PortDeclaration { typeAnnotation } ->
visitTypeAnnotation typeAnnotation
_ ->
[]
visitMaybeSignature : Maybe (Node Signature) -> List Name
visitMaybeSignature maybeNode =
case maybeNode of
Just node ->
visitSignature node
Nothing ->
[]
visitSignature : Node Signature -> List Name
visitSignature node =
visitTypeAnnotation (node |> Node.value |> .typeAnnotation)
visitFunctionImplementation : Node Expression.FunctionImplementation -> List Name
visitFunctionImplementation node =
visitPatternList (node |> Node.value |> .arguments)
visitValueConstructorList : List (Node Type.ValueConstructor) -> List Name
visitValueConstructorList list =
fastConcatMap visitValueConstructor list
visitValueConstructor : Node Type.ValueConstructor -> List Name
visitValueConstructor node =
visitTypeAnnotationList (node |> Node.value |> .arguments)
visitTypeAnnotationList : List (Node TypeAnnotation) -> List Name
visitTypeAnnotationList list =
fastConcatMap visitTypeAnnotation list
visitTypeAnnotation : Node TypeAnnotation -> List Name
visitTypeAnnotation node =
case Node.value node of
TypeAnnotation.GenericType _ ->
[]
TypeAnnotation.Typed call types ->
visitType call
++ visitTypeAnnotationList types
TypeAnnotation.Unit ->
[]
TypeAnnotation.Tupled list ->
visitTypeAnnotationList list
TypeAnnotation.Record list ->
visitRecordFieldList list
TypeAnnotation.GenericRecord _ list ->
visitRecordFieldList (Node.value list)
TypeAnnotation.FunctionTypeAnnotation argument return ->
visitTypeAnnotation argument
++ visitTypeAnnotation return
visitRecordFieldList : List (Node TypeAnnotation.RecordField) -> List Name
visitRecordFieldList list =
fastConcatMap visitRecordField list
visitRecordField : Node TypeAnnotation.RecordField -> List Name
visitRecordField node =
visitTypeAnnotation (node |> Node.value |> Tuple.second)
visitExpression : Node Expression -> List Name
visitExpression (Node range expression) =
case expression of
Expression.FunctionOrValue moduleName function ->
visitValue (Node range ( moduleName, function ))
Expression.LetExpression { declarations } ->
visitLetDeclarationList declarations
Expression.CaseExpression { cases } ->
visitCaseList cases
Expression.LambdaExpression { args } ->
visitPatternList args
Expression.RecordUpdateExpression name _ ->
visitValue (Node.map (\function -> ( [], function )) name)
_ ->
[]
visitLetDeclarationList : List (Node Expression.LetDeclaration) -> List Name
visitLetDeclarationList list =
fastConcatMap visitLetDeclaration list
visitLetDeclaration : Node Expression.LetDeclaration -> List Name
visitLetDeclaration node =
case Node.value node of
Expression.LetFunction { signature, declaration } ->
visitMaybeSignature signature
++ visitFunctionImplementation declaration
Expression.LetDestructuring pattern _ ->
visitPattern pattern
visitCaseList : List Expression.Case -> List Name
visitCaseList list =
fastConcatMap visitCase list
visitCase : Expression.Case -> List Name
visitCase ( pattern, _ ) =
visitPattern pattern
visitPatternList : List (Node Pattern) -> List Name
visitPatternList list =
fastConcatMap visitPattern list
visitPattern : Node Pattern -> List Name
visitPattern node =
case Node.value node of
Pattern.TuplePattern patterns ->
visitPatternList patterns
Pattern.UnConsPattern head rest ->
visitPattern head ++ visitPattern rest
Pattern.ListPattern list ->
visitPatternList list
Pattern.NamedPattern { moduleName, name } _ ->
let
{ start } =
Node.range node
newEnd =
{ start | column = start.column + (name :: moduleName |> String.join "." |> String.length) }
range =
{ start = start, end = newEnd }
in
visitValue (Node range ( moduleName, name ))
Pattern.AsPattern pattern _ ->
visitPattern pattern
Pattern.ParenthesizedPattern pattern ->
visitPattern pattern
_ ->
[]
visitValue : Node ( ModuleName, String ) -> List Name
visitValue node =
[ Value node ]
visitType : Node ( ModuleName, String ) -> List Name
visitType node =
[ Type node ]
--- High Performance List
fastConcatMap : (a -> List b) -> List a -> List b
fastConcatMap fn =
List.foldr (fn >> (++)) []