Move error messages out of Lint.Test and into Lint.Test.ErrorMessage

This commit is contained in:
Jeroen Engels 2019-07-11 08:43:56 +02:00
parent 1d3333f489
commit b6464dbff2
4 changed files with 397 additions and 962 deletions

View File

@ -1,638 +0,0 @@
module Lint.Internal.Test exposing
( LintResult, run
, ExpectedError, expectErrors, expectNoErrors, error, atExactly
, parsingErrorMessage, messageMismatchError, wrongLocationError, didNotExpectErrors
, underMismatchError, notEnoughErrors, tooManyErrors, locationIsAmbiguousInSourceCodeError
)
{-| Module that helps you test your linting rules, using [`elm-test`](https://package.elm-lang.org/packages/elm-explorations/test/latest).
import Lint.Test exposing (LintResult)
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
testRule : String -> LintResult
testRule string =
Lint.Test.run rule string
-- In this example, the rule we're testing is `NoDebug`
tests : Test
tests =
describe "NoDebug"
[ test "should not report calls to normal functions" <|
\() ->
testRule """module A exposing (..)
a = foo n"""
|> Lint.Test.expectNoErrors
, test "should report Debug.log use" <|
\() ->
testRule """module A exposing (..)
a = Debug.log "some" "message\""""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = "Forbidden use of Debug"
, under = "Debug.log"
}
]
]
# Running tests
@docs LintResult, run
# Making assertions
@docs ExpectedError, expectErrors, expectNoErrors, error, atExactly
# Error messages
@docs parsingErrorMessage, messageMismatchError, wrongLocationError, didNotExpectErrors
@docs underMismatchError, notEnoughErrors, tooManyErrors, locationIsAmbiguousInSourceCodeError
# Tips on testing
## What should you test?
TODO Add helpful tips
-}
import Array exposing (Array)
import Elm.Syntax.Range exposing (Range)
import Expect exposing (Expectation)
import Lint exposing (Severity(..), lintSource)
import Lint.Rule as Rule exposing (Error, Rule)
import List.Extra
{-| The result of running a rule on a `String` containing source code.
-}
type LintResult
= ParseFailure
| SuccessfulRun CodeInspector (List Error)
type alias CodeInspector =
{ getCodeAtLocation : Range -> Maybe String
, checkIfLocationIsAmbiguous : Error -> String -> Expectation
}
{-| An expectation for an error. Use [`error`](#error) to create one.
-}
type ExpectedError
= ExpectedError
{ message : String
, under : Under
}
type Under
= Under String
| UnderExactly String Range
type alias SourceCode =
String
{-| Run a `Rule` on a `String` containing source code. You can then use
[`expectNoErrors`](#expectNoErrors) or [`expectErrors`](#expectErrors) to assert
the errors reported by the rule.
The source code needs to be syntactically valid Elm code. If the code
can't be parsed, the test will fail regardless of the expectations you set on it.
Note that t be syntactically valid, you need at least a module declaration at the
top of the file (like `module A exposing (..)`) and one declaration (like `a = 1`).
You can't just have an expression like `1 + 2`.
-}
run : Rule -> String -> LintResult
run rule sourceCode =
case lintSource [ ( Critical, rule ) ] sourceCode of
Ok errors ->
SuccessfulRun
{ getCodeAtLocation = getCodeAtLocationInSourceCode sourceCode
, checkIfLocationIsAmbiguous = checkIfLocationIsAmbiguousInSourceCode sourceCode
}
(List.map (\( _, error_ ) -> Rule.error error_.message error_.range) errors)
Err _ ->
ParseFailure
{-| Assert that the rule reprted no errors. Note, this is equivalent to using [`expectErrors`](#expectErrors)
like `expectErrors []`.
import Lint.Test exposing (LintResult)
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
testRule : String -> LintResult
testRule string =
Lint.Test.run rule string
-- In this example, the rule we're testing is `NoDebug`
tests : Test
tests =
describe "NoDebug"
[ test "should not report calls to normal functions" <|
\() ->
testRule """module A exposing (..)
a = foo n"""
|> Lint.Test.expectNoErrors
]
-}
expectNoErrors : LintResult -> Expectation
expectNoErrors lintResult =
case lintResult of
ParseFailure ->
Expect.fail parsingErrorMessage
SuccessfulRun _ errors ->
Expect.true
(didNotExpectErrors errors)
(List.isEmpty errors)
didNotExpectErrors : List Error -> String
didNotExpectErrors errors =
"""I expected no errors but found:
""" ++ (List.map errorToString errors |> String.join "\n ")
{-| Assert that the rule reprted some errors, by specifying which one.
Assert which errors are reported using [`error`](#error). The test will fail if
a different number of errors than expected are reported, or if the message or the
location is incorrect.
The errors should be in the order of where they appear in the source code. An error
at the start of the source code should appear earlier in the list than
an error at the end of the source code.
import Lint.Test exposing (LintResult)
import Test exposing (Test, describe, test)
import The.Rule.You.Want.To.Test exposing (rule)
testRule : String -> LintResult
testRule string =
Lint.Test.run rule string
-- In this example, the rule we're testing is `NoDebug`
tests : Test
tests =
describe "NoDebug"
[ test "should report Debug.log use" <|
\() ->
testRule """module A exposing (..)
a = Debug.log "some" "message\""""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = "Forbidden use of Debug"
, under = "Debug.log"
}
]
]
-}
expectErrors : List ExpectedError -> LintResult -> Expectation
expectErrors expectedErrors lintResult =
case lintResult of
ParseFailure ->
Expect.fail parsingErrorMessage
SuccessfulRun codeInspector errors ->
checkAllErrorsMatch codeInspector expectedErrors errors
{-| Create an expectation for an error.
`message` should be the message you're expecting to be shown to the user.
`under` is the part of the code where you are expecting the error to be shown to
the user. If it helps, imagine `under` to be the text under which the squiggly
lines will appear if the error appeared in an editor.
tests : Test
tests =
describe "NoDebug"
[ test "should report Debug.log use" <|
\() ->
testRule """module A exposing (..)
a = Debug.log "some" "message\""""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = "Forbidden use of Debug"
, under = "Debug.log"
}
]
]
If there are multiple locations where the value of `under` appears, the test will
fail unless you use [`atExactly`](#atExactly) to remove any ambiguity of where the
error should be used.
-}
error : { message : String, under : String } -> ExpectedError
error input =
ExpectedError
{ message = input.message
, under = Under input.under
}
getUnder : ExpectedError -> String
getUnder (ExpectedError expectedError) =
case expectedError.under of
Under str ->
str
UnderExactly str _ ->
str
{-| Precise the exact position where the error should be shown to the user. This
is only necessary when the `under` field is ambiguous.
`atExactly` takes a record with start and end positions.
tests : Test
tests =
describe "NoDebug"
[ test "should report multiple Debug.log calls" <|
\() ->
testRule """
a = Debug.log z
b = Debug.log z
"""
|> Lint.Test.expectErrors
[ Lint.Test.error
{ message = message
, under = "Debug.log"
}
|> Lint.Test.atExactly { start = { row = 4, column = 5 }, end = { row = 4, column = 14 } }
, Lint.Test.error
{ message = message
, under = "Debug.log"
}
|> Lint.Test.atExactly { start = { row = 5, column = 5 }, end = { row = 5, column = 14 } }
]
]
Tip: By default, do not provide this field. If the test fails because there is some
ambiguity, the test error will give you a recommendation of what to use as a parameter
of `atExactly`, so you do not have to bother writing this hard to write argument.
-}
atExactly : { start : { row : Int, column : Int }, end : { row : Int, column : Int } } -> ExpectedError -> ExpectedError
atExactly range ((ExpectedError expectedError_) as expectedError) =
ExpectedError { expectedError_ | under = UnderExactly (getUnder expectedError) range }
checkAllErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> Expectation
checkAllErrorsMatch codeInspector expectedErrors errors =
checkErrorsMatch codeInspector expectedErrors errors
|> List.reverse
|> (\expectations -> Expect.all expectations ())
checkErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> List (() -> Expectation)
checkErrorsMatch codeInspector expectedErrors errors =
case ( expectedErrors, errors ) of
( [], [] ) ->
[ always Expect.pass ]
( expected :: restOfExpectedErrors, error_ :: restOfErrors ) ->
checkErrorMatch codeInspector expected error_ :: checkErrorsMatch codeInspector restOfExpectedErrors restOfErrors
( expected :: restOfExpectedErrors, [] ) ->
[ always <| Expect.fail <| notEnoughErrors (expected :: restOfExpectedErrors) ]
( [], error_ :: restOfErrors ) ->
[ always <| Expect.fail <| tooManyErrors (error_ :: restOfErrors) ]
checkErrorMatch : CodeInspector -> ExpectedError -> Error -> (() -> Expectation)
checkErrorMatch codeInspector ((ExpectedError expectedError_) as expectedError) error_ =
Expect.all
[ always <| Expect.true (messageMismatchError expectedError error_) (expectedError_.message == Rule.errorMessage error_)
, checkMessageAppearsUnder codeInspector error_ expectedError
]
checkMessageAppearsUnder : CodeInspector -> Error -> ExpectedError -> (() -> Expectation)
checkMessageAppearsUnder codeInspector error_ (ExpectedError expectedError) =
case codeInspector.getCodeAtLocation (Rule.errorRange error_) of
Just codeAtLocation ->
case expectedError.under of
Under under ->
Expect.all
[ always <|
Expect.true
(underMismatchError error_ { under = under, codeAtLocation = codeAtLocation })
(codeAtLocation == under)
, always <| codeInspector.checkIfLocationIsAmbiguous error_ under
]
UnderExactly under range ->
Expect.all
[ always <|
Expect.true
(underMismatchError error_ { under = under, codeAtLocation = codeAtLocation })
(codeAtLocation == under)
, always <|
Expect.true
(wrongLocationError error_ range under)
(Rule.errorRange error_ == range)
]
Nothing ->
always <| Expect.fail impossibleStateError
getCodeAtLocationInSourceCode : SourceCode -> Range -> Maybe String
getCodeAtLocationInSourceCode sourceCode =
let
lines : Array String
lines =
String.lines sourceCode
|> Array.fromList
in
\{ start, end } ->
if start.row == end.row then
Array.get (start.row - 1) lines
|> Maybe.map (String.slice (start.column - 1) (end.column - 1))
else
let
firstLine : Maybe String
firstLine =
Array.get (start.row - 1) lines
|> Maybe.map (String.dropLeft (start.column - 1))
lastLine : Maybe String
lastLine =
Array.get (end.row - 1) lines
|> Maybe.map (String.dropRight end.column)
in
[ [ firstLine ]
, Array.slice start.row (end.row - 1) lines
|> Array.toList
|> List.map Just
, [ lastLine ]
]
|> List.concat
|> List.filterMap identity
|> String.join "\n"
|> Just
formatSourceCode : String -> String
formatSourceCode string =
let
lines =
String.lines string
in
if List.length lines == 1 then
"`" ++ string ++ "`"
else
lines
|> List.map (\str -> " " ++ str)
|> String.join "\n"
|> (\str -> "```\n" ++ str ++ "\n ```")
checkIfLocationIsAmbiguousInSourceCode : SourceCode -> Error -> String -> Expectation
checkIfLocationIsAmbiguousInSourceCode sourceCode error_ under =
let
occurrencesInSourceCode : List Int
occurrencesInSourceCode =
String.indexes under sourceCode
in
Expect.true
(locationIsAmbiguousInSourceCodeError sourceCode error_ under occurrencesInSourceCode)
(List.length occurrencesInSourceCode == 1)
-- ERROR MESSAGES
parsingErrorMessage : String
parsingErrorMessage =
"""I could not parse the test source code, because it was not syntactically valid Elm code.
Maybe you forgot to add the module definition at the top, like:
`module A exposing (..)`"""
messageMismatchError : ExpectedError -> Error -> String
messageMismatchError (ExpectedError expectedError) error_ =
"""I was looking for the error with the following message:
`""" ++ expectedError.message ++ """`
but I found the following error message:
`""" ++ Rule.errorMessage error_ ++ "`"
underMismatchError : Error -> { under : String, codeAtLocation : String } -> String
underMismatchError error_ { under, codeAtLocation } =
"""I found an error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
which I was expecting, but I found it under:
""" ++ formatSourceCode codeAtLocation ++ """
when I was expecting it under:
""" ++ formatSourceCode under ++ """
Hint: Maybe you're passing the `Range` of a wrong node when calling `Rule.error`"""
wrongLocationError : Error -> Range -> String -> String
wrongLocationError error_ range under =
"""I was looking for the error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
under the following code:
""" ++ formatSourceCode under ++ """
and I found it, but the exact location you specified is not the one I found. I was expecting the error at:
""" ++ rangeAsString range ++ """
but I found it at:
""" ++ rangeAsString (Rule.errorRange error_)
listOccurrencesAsLocations : SourceCode -> String -> List Int -> String
listOccurrencesAsLocations sourceCode under occurrences =
occurrences
|> List.map
(\occurrence ->
occurrence
|> positionAsRange sourceCode under
|> rangeAsString
|> (++) " - "
)
|> String.join "\n"
positionAsRange : SourceCode -> String -> Int -> Range
positionAsRange sourceCode under position =
let
linesBeforeAndIncludingPosition : List String
linesBeforeAndIncludingPosition =
sourceCode
|> String.slice 0 position
|> String.lines
startRow : Int
startRow =
List.length linesBeforeAndIncludingPosition
startColumn : Int
startColumn =
linesBeforeAndIncludingPosition
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
linesInUnder : List String
linesInUnder =
String.lines under
endRow : Int
endRow =
startRow + List.length linesInUnder - 1
endColumn : Int
endColumn =
if startRow == endRow then
startColumn + String.length under
else
linesInUnder
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
in
{ start =
{ row = startRow
, column = startColumn
}
, end =
{ row = endRow
, column = endColumn
}
}
errorToString : Error -> String
errorToString error_ =
"- \"" ++ Rule.errorMessage error_ ++ "\" at " ++ rangeAsString (Rule.errorRange error_)
rangeAsString : Range -> String
rangeAsString { start, end } =
"{ start = { row = " ++ String.fromInt start.row ++ ", column = " ++ String.fromInt start.column ++ " }, end = { row = " ++ String.fromInt end.row ++ ", column = " ++ String.fromInt end.column ++ " } }"
notEnoughErrors : List ExpectedError -> String
notEnoughErrors missingExpectedErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length missingExpectedErrors
in
"I expected to see "
++ String.fromInt numberOfErrors
++ " more "
++ pluralizeErrors numberOfErrors
++ ":\n\n"
++ (missingExpectedErrors
|> List.map expectedErrorToString
|> String.join "\n"
)
wrapInQuotes : String -> String
wrapInQuotes string =
"\"" ++ string ++ "\""
tooManyErrors : List Error -> String
tooManyErrors extraErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length extraErrors
in
"I found "
++ String.fromInt numberOfErrors
++ " "
++ pluralizeErrors numberOfErrors
++ " too many:\n\n"
++ (extraErrors
|> List.map errorToString
|> String.join "\n"
)
locationIsAmbiguousInSourceCodeError : SourceCode -> Error -> String -> List Int -> String
locationIsAmbiguousInSourceCodeError sourceCode error_ under occurrencesInSourceCode =
"""Your test passes, but where the message appears is ambiguous.
You are looking for the following error message:
`""" ++ Rule.errorMessage error_ ++ """`
and expecting to see it under:
""" ++ formatSourceCode under ++ """
I found """ ++ String.fromInt (List.length occurrencesInSourceCode) ++ """ locations where that code appeared. Please use `Lint.Rule.atExactly` to make the part you were targetting unambiguous.
Tip: I found them at:
""" ++ listOccurrencesAsLocations sourceCode under occurrencesInSourceCode
impossibleStateError : String
impossibleStateError =
"Oh no! I'm in an impossible state. I found an error at a location that I could not find back. Please let me know and give me an SSCCE (http://sscce.org/) here: https://github.com/jfmengels/elm-lint/issues."
pluralizeErrors : Int -> String
pluralizeErrors n =
case n of
1 ->
"error"
_ ->
"errors"
expectedErrorToString : ExpectedError -> String
expectedErrorToString (ExpectedError expectedError) =
"- " ++ wrapInQuotes expectedError.message

View File

@ -59,7 +59,7 @@ import Elm.Syntax.Range exposing (Range)
import Expect exposing (Expectation) import Expect exposing (Expectation)
import Lint exposing (Severity(..), lintSource) import Lint exposing (Severity(..), lintSource)
import Lint.Rule as Rule exposing (Error, Rule) import Lint.Rule as Rule exposing (Error, Rule)
import List.Extra import Lint.Test.ErrorMessage as ErrorMessage
{-| The result of running a rule on a `String` containing source code. {-| The result of running a rule on a `String` containing source code.
@ -89,8 +89,8 @@ type Under
| UnderExactly String Range | UnderExactly String Range
type SourceCode type alias SourceCode =
= SourceCode String String
{-| Run a `Rule` on a `String` containing source code. You can then use {-| Run a `Rule` on a `String` containing source code. You can then use
@ -110,8 +110,8 @@ run rule sourceCode =
case lintSource [ ( Critical, rule ) ] sourceCode of case lintSource [ ( Critical, rule ) ] sourceCode of
Ok errors -> Ok errors ->
SuccessfulRun SuccessfulRun
{ getCodeAtLocation = getCodeAtLocationInSourceCode (SourceCode sourceCode) { getCodeAtLocation = getCodeAtLocationInSourceCode sourceCode
, checkIfLocationIsAmbiguous = checkIfLocationIsAmbiguousInSourceCode (SourceCode sourceCode) , checkIfLocationIsAmbiguous = checkIfLocationIsAmbiguousInSourceCode sourceCode
} }
(List.map (\( _, error_ ) -> Rule.error error_.message error_.range) errors) (List.map (\( _, error_ ) -> Rule.error error_.message error_.range) errors)
@ -119,7 +119,7 @@ run rule sourceCode =
ParseFailure ParseFailure
{-| Assert that the rule reprted no errors. Note, this is equivalent to using [`expectErrors`](#expectErrors) {-| Assert that the rule reported no errors. Note, this is equivalent to using [`expectErrors`](#expectErrors)
like `expectErrors []`. like `expectErrors []`.
import Lint.Test exposing (LintResult) import Lint.Test exposing (LintResult)
@ -146,15 +146,15 @@ expectNoErrors : LintResult -> Expectation
expectNoErrors lintResult = expectNoErrors lintResult =
case lintResult of case lintResult of
ParseFailure -> ParseFailure ->
Expect.fail parsingErrorMessage Expect.fail ErrorMessage.parsingFailure
SuccessfulRun _ errors -> SuccessfulRun _ errors ->
Expect.true Expect.true
("I expected no errors but found:\n\n" ++ (List.map errorToString errors |> String.join "\n")) (ErrorMessage.didNotExpectErrors errors)
(List.isEmpty errors) (List.isEmpty errors)
{-| Assert that the rule reprted some errors, by specifying which one. {-| Assert that the rule reported some errors, by specifying which one.
Assert which errors are reported using [`error`](#error). The test will fail if Assert which errors are reported using [`error`](#error). The test will fail if
a different number of errors than expected are reported, or if the message or the a different number of errors than expected are reported, or if the message or the
@ -193,7 +193,7 @@ expectErrors : List ExpectedError -> LintResult -> Expectation
expectErrors expectedErrors lintResult = expectErrors expectedErrors lintResult =
case lintResult of case lintResult of
ParseFailure -> ParseFailure ->
Expect.fail parsingErrorMessage Expect.fail ErrorMessage.parsingFailure
SuccessfulRun codeInspector errors -> SuccessfulRun codeInspector errors ->
checkAllErrorsMatch codeInspector expectedErrors errors checkAllErrorsMatch codeInspector expectedErrors errors
@ -235,16 +235,6 @@ error input =
} }
getUnder : ExpectedError -> String
getUnder (ExpectedError expectedError) =
case expectedError.under of
Under str ->
str
UnderExactly str _ ->
str
{-| Precise the exact position where the error should be shown to the user. This {-| Precise the exact position where the error should be shown to the user. This
is only necessary when the `under` field is ambiguous. is only necessary when the `under` field is ambiguous.
@ -283,60 +273,18 @@ atExactly range ((ExpectedError expectedError_) as expectedError) =
ExpectedError { expectedError_ | under = UnderExactly (getUnder expectedError) range } ExpectedError { expectedError_ | under = UnderExactly (getUnder expectedError) range }
checkAllErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> Expectation getUnder : ExpectedError -> String
checkAllErrorsMatch codeInspector expectedErrors errors = getUnder (ExpectedError expectedError) =
checkErrorsMatch codeInspector expectedErrors errors case expectedError.under of
|> List.reverse Under str ->
|> (\expectations -> Expect.all expectations ()) str
UnderExactly str _ ->
checkErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> List (() -> Expectation) str
checkErrorsMatch codeInspector expectedErrors errors =
case ( expectedErrors, errors ) of
( [], [] ) ->
[ always Expect.pass ]
( expected :: restOfExpectedErrors, error_ :: restOfErrors ) ->
checkErrorMatch codeInspector expected error_ :: checkErrorsMatch codeInspector restOfExpectedErrors restOfErrors
( expected :: restOfExpectedErrors, [] ) ->
[ always <| Expect.fail <| notEnoughErrors expected restOfExpectedErrors ]
( [], error_ :: restOfErrors ) ->
[ always <| Expect.fail <| tooManyErrors error_ restOfErrors ]
checkErrorMatch : CodeInspector -> ExpectedError -> Error -> (() -> Expectation)
checkErrorMatch codeInspector ((ExpectedError expectedError_) as expectedError) error_ =
Expect.all
[ always <| Expect.true (messageMismatchError expectedError error_) (expectedError_.message == Rule.errorMessage error_)
, checkMessageAppearsUnder codeInspector error_ expectedError
]
checkMessageAppearsUnder : CodeInspector -> Error -> ExpectedError -> (() -> Expectation)
checkMessageAppearsUnder codeInspector error_ (ExpectedError expectedError) =
case codeInspector.getCodeAtLocation (Rule.errorRange error_) of
Just codeAtLocation ->
case expectedError.under of
Under under ->
Expect.all
[ always <| Expect.true (underMismatchError error_ under codeAtLocation) (codeAtLocation == under)
, always <| codeInspector.checkIfLocationIsAmbiguous error_ under
]
UnderExactly under range ->
Expect.all
[ always <| Expect.true (underMismatchError error_ under codeAtLocation) (codeAtLocation == under)
, always <| Expect.true (wrongLocationError error_ range under) (Rule.errorRange error_ == range)
]
Nothing ->
always <| Expect.fail impossibleStateError
getCodeAtLocationInSourceCode : SourceCode -> Range -> Maybe String getCodeAtLocationInSourceCode : SourceCode -> Range -> Maybe String
getCodeAtLocationInSourceCode (SourceCode sourceCode) = getCodeAtLocationInSourceCode sourceCode =
let let
lines : Array String lines : Array String
lines = lines =
@ -372,233 +320,91 @@ getCodeAtLocationInSourceCode (SourceCode sourceCode) =
|> Just |> Just
formatSourceCode : String -> String
formatSourceCode string =
let
lines =
String.lines string
in
if List.length lines == 1 then
"`" ++ string ++ "`"
else
lines
|> List.map (\str -> " " ++ str)
|> String.join "\n"
|> (\str -> "\n\n```\n" ++ str ++ "\n```")
checkIfLocationIsAmbiguousInSourceCode : SourceCode -> Error -> String -> Expectation checkIfLocationIsAmbiguousInSourceCode : SourceCode -> Error -> String -> Expectation
checkIfLocationIsAmbiguousInSourceCode ((SourceCode sourceCodeContent) as sourceCode) error_ under = checkIfLocationIsAmbiguousInSourceCode sourceCode error_ under =
let let
occurrencesInSourceCode : List Int occurrencesInSourceCode : List Int
occurrencesInSourceCode = occurrencesInSourceCode =
String.indexes under sourceCodeContent String.indexes under sourceCode
in in
Expect.true Expect.true
(locationIsAmbiguousInSourceCodeError sourceCode error_ under occurrencesInSourceCode) (ErrorMessage.locationIsAmbiguousInSourceCode sourceCode error_ under occurrencesInSourceCode)
(List.length occurrencesInSourceCode == 1) (List.length occurrencesInSourceCode == 1)
-- ERROR MESSAGES -- RUNNING THE CHECKS
parsingErrorMessage : String checkAllErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> Expectation
parsingErrorMessage = checkAllErrorsMatch codeInspector expectedErrors errors =
"""I could not parse the test source code, because it was not syntactically valid Elm code. checkErrorsMatch codeInspector expectedErrors errors
|> List.reverse
Maybe you forgot to add the module definition at the top, like: |> (\expectations -> Expect.all expectations ())
module A exposing (..)"""
messageMismatchError : ExpectedError -> Error -> String checkErrorsMatch : CodeInspector -> List ExpectedError -> List Error -> List (() -> Expectation)
messageMismatchError (ExpectedError expectedError) error_ = checkErrorsMatch codeInspector expectedErrors errors =
"""I was looking for the error with the following message: case ( expectedErrors, errors ) of
( [], [] ) ->
[ always Expect.pass ]
`""" ++ expectedError.message ++ """` ( expected :: restOfExpectedErrors, error_ :: restOfErrors ) ->
checkErrorMatch codeInspector expected error_ :: checkErrorsMatch codeInspector restOfExpectedErrors restOfErrors
but I found the following error message: ( expected :: restOfExpectedErrors, [] ) ->
[ always <| Expect.fail <| ErrorMessage.expectedMoreErrors <| List.map extractExpectedErrorData (expected :: restOfExpectedErrors) ]
`""" ++ Rule.errorMessage error_ ++ "`" ( [], error_ :: restOfErrors ) ->
[ always <| Expect.fail <| ErrorMessage.tooManyErrors (error_ :: restOfErrors) ]
wrongLocationError : Error -> Range -> String -> String checkErrorMatch : CodeInspector -> ExpectedError -> Error -> (() -> Expectation)
wrongLocationError error_ range under = checkErrorMatch codeInspector ((ExpectedError expectedError_) as expectedError) error_ =
"""I was looking for the error with the following message: Expect.all
[ \_ ->
`""" ++ Rule.errorMessage error_ ++ """` (expectedError_.message == Rule.errorMessage error_)
|> Expect.true
under the following code: (ErrorMessage.messageMismatch
(extractExpectedErrorData expectedError)
""" ++ formatSourceCode under ++ """ error_
)
and I found it, but the exact location you specified is not the one I found. I was expecting the error at: , checkMessageAppearsUnder codeInspector error_ expectedError
]
""" ++ rangeAsString range ++ """
but I found it at:
""" ++ rangeAsString (Rule.errorRange error_)
underMismatchError : Error -> String -> String -> String checkMessageAppearsUnder : CodeInspector -> Error -> ExpectedError -> (() -> Expectation)
underMismatchError error_ under codeAtLocation = checkMessageAppearsUnder codeInspector error_ (ExpectedError expectedError) =
"""I found an error with the right message, but at the wrong location: case codeInspector.getCodeAtLocation (Rule.errorRange error_) of
Just codeAtLocation ->
case expectedError.under of
Under under ->
Expect.all
[ always <|
Expect.true
(ErrorMessage.underMismatch error_ { under = under, codeAtLocation = codeAtLocation })
(codeAtLocation == under)
, always <| codeInspector.checkIfLocationIsAmbiguous error_ under
]
Message: `""" ++ Rule.errorMessage error_ ++ """` UnderExactly under range ->
Expect.all
[ always <|
Expect.true
(ErrorMessage.underMismatch error_ { under = under, codeAtLocation = codeAtLocation })
(codeAtLocation == under)
, always <|
Expect.true
(ErrorMessage.wrongLocation error_ range under)
(Rule.errorRange error_ == range)
]
I saw it under: """ ++ formatSourceCode codeAtLocation ++ """ Nothing ->
always <| Expect.fail ErrorMessage.impossibleState
But I expected to see it under: """ ++ formatSourceCode under
listOccurrencesAsLocations : SourceCode -> String -> List Int -> String extractExpectedErrorData : ExpectedError -> ErrorMessage.ExpectedErrorData
listOccurrencesAsLocations sourceCode under occurrences = extractExpectedErrorData ((ExpectedError expectedErrorContent) as expectedError) =
occurrences { message = expectedErrorContent.message
|> List.map , under = getUnder expectedError
(\occurrence ->
occurrence
|> positionAsRange sourceCode under
|> rangeAsString
|> (++) " - "
)
|> String.join "\n"
positionAsRange : SourceCode -> String -> Int -> Range
positionAsRange (SourceCode sourceCode) under position =
let
linesBeforeAndIncludingPosition : List String
linesBeforeAndIncludingPosition =
sourceCode
|> String.slice 0 position
|> String.lines
startRow : Int
startRow =
List.length linesBeforeAndIncludingPosition
startColumn : Int
startColumn =
linesBeforeAndIncludingPosition
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
linesInUnder : List String
linesInUnder =
String.lines under
endRow : Int
endRow =
startRow + List.length linesInUnder - 1
endColumn : Int
endColumn =
if startRow == endRow then
startColumn + String.length under
else
linesInUnder
|> Debug.log "linesInUnder"
|> List.Extra.last
|> Debug.log "last"
|> Maybe.withDefault ""
|> String.length
|> (+) 1
in
{ start =
{ row = startRow
, column = startColumn
}
, end =
{ row = endRow
, column = endColumn
}
} }
errorToString : Error -> String
errorToString error_ =
"- \"" ++ Rule.errorMessage error_ ++ "\" at " ++ rangeAsString (Rule.errorRange error_)
rangeAsString : Range -> String
rangeAsString { start, end } =
"{ start = { row = " ++ String.fromInt start.row ++ ", column = " ++ String.fromInt start.column ++ " }, end = { row = " ++ String.fromInt end.row ++ ", column = " ++ String.fromInt end.column ++ " } }"
notEnoughErrors : ExpectedError -> List ExpectedError -> String
notEnoughErrors expected restOfExpectedErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length restOfExpectedErrors + 1
in
"I expected to see "
++ String.fromInt numberOfErrors
++ " more "
++ pluralizeErrors numberOfErrors
++ ":\n\n"
++ (List.map expectedErrorToString (expected :: restOfExpectedErrors) |> String.join "\n")
wrapInQuotes : String -> String
wrapInQuotes string =
"\"" ++ string ++ "\""
tooManyErrors : Error -> List Error -> String
tooManyErrors error_ restOfErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length restOfErrors + 1
in
"I found "
++ String.fromInt numberOfErrors
++ " "
++ pluralizeErrors numberOfErrors
++ " too many:\n"
++ (List.map errorToString (error_ :: restOfErrors) |> String.join "\n")
locationIsAmbiguousInSourceCodeError : SourceCode -> Error -> String -> List Int -> String
locationIsAmbiguousInSourceCodeError sourceCode error_ under occurrencesInSourceCode =
"""Your test passes, but where the message appears is ambiguous.
You are looking for the following error message:
`""" ++ Rule.errorMessage error_ ++ """`
and expecting to see it under:
""" ++ formatSourceCode under ++ """
I found """ ++ String.fromInt (List.length occurrencesInSourceCode) ++ """ locations where that code appeared. Please use `Lint.Rule.atExactly` to make the part you were targetting unambiguous.
Tip: I found them at:
""" ++ listOccurrencesAsLocations sourceCode under occurrencesInSourceCode
impossibleStateError : String
impossibleStateError =
"Oh no! I'm in an impossible state. I found an error at a location that I could not find back. Please let me know and give me an SSCCE (http://sscce.org/) here: https://github.com/jfmengels/elm-lint/issues."
pluralizeErrors : Int -> String
pluralizeErrors n =
case n of
1 ->
"error"
_ ->
"errors"
expectedErrorToString : ExpectedError -> String
expectedErrorToString (ExpectedError expectedError) =
"- " ++ wrapInQuotes expectedError.message

View File

@ -0,0 +1,270 @@
module Lint.Test.ErrorMessage exposing
( ExpectedErrorData
, parsingFailure, messageMismatch, wrongLocation, didNotExpectErrors
, underMismatch, expectedMoreErrors, tooManyErrors, locationIsAmbiguousInSourceCode
, impossibleState
)
{-| Error messages for the `Lint.Test` module.
# Error messages
@docs ExpectedErrorData
@docs parsingFailure, messageMismatch, wrongLocation, didNotExpectErrors
@docs underMismatch, expectedMoreErrors, tooManyErrors, locationIsAmbiguousInSourceCode
@docs impossibleState
-}
import Elm.Syntax.Range exposing (Range)
import Lint.Rule as Rule exposing (Error, Rule)
import List.Extra
{-| An expectation for an error. Use [`error`](#error) to create one.
-}
type alias ExpectedErrorData =
{ message : String
, under : String
}
type alias SourceCode =
String
-- ERROR MESSAGES
didNotExpectErrors : List Error -> String
didNotExpectErrors errors =
"""I expected no errors but found:
""" ++ (List.map errorToString errors |> String.join "\n ")
parsingFailure : String
parsingFailure =
"""I could not parse the test source code, because it was not syntactically valid Elm code.
Maybe you forgot to add the module definition at the top, like:
`module A exposing (..)`"""
messageMismatch : ExpectedErrorData -> Error -> String
messageMismatch expectedError error_ =
"""I was looking for the error with the following message:
`""" ++ expectedError.message ++ """`
but I found the following error message:
`""" ++ Rule.errorMessage error_ ++ "`"
underMismatch : Error -> { under : String, codeAtLocation : String } -> String
underMismatch error_ { under, codeAtLocation } =
"""I found an error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
which I was expecting, but I found it under:
""" ++ formatSourceCode codeAtLocation ++ """
when I was expecting it under:
""" ++ formatSourceCode under ++ """
Hint: Maybe you're passing the `Range` of a wrong node when calling `Rule.error`"""
wrongLocation : Error -> Range -> String -> String
wrongLocation error_ range under =
"""I was looking for the error with the following message:
`""" ++ Rule.errorMessage error_ ++ """`
under the following code:
""" ++ formatSourceCode under ++ """
and I found it, but the exact location you specified is not the one I found. I was expecting the error at:
""" ++ rangeAsString range ++ """
but I found it at:
""" ++ rangeAsString (Rule.errorRange error_)
listOccurrencesAsLocations : SourceCode -> String -> List Int -> String
listOccurrencesAsLocations sourceCode under occurrences =
occurrences
|> List.map
(\occurrence ->
occurrence
|> positionAsRange sourceCode under
|> rangeAsString
|> (++) " - "
)
|> String.join "\n"
positionAsRange : SourceCode -> String -> Int -> Range
positionAsRange sourceCode under position =
let
linesBeforeAndIncludingPosition : List String
linesBeforeAndIncludingPosition =
sourceCode
|> String.slice 0 position
|> String.lines
startRow : Int
startRow =
List.length linesBeforeAndIncludingPosition
startColumn : Int
startColumn =
linesBeforeAndIncludingPosition
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
linesInUnder : List String
linesInUnder =
String.lines under
endRow : Int
endRow =
startRow + List.length linesInUnder - 1
endColumn : Int
endColumn =
if startRow == endRow then
startColumn + String.length under
else
linesInUnder
|> List.Extra.last
|> Maybe.withDefault ""
|> String.length
|> (+) 1
in
{ start =
{ row = startRow
, column = startColumn
}
, end =
{ row = endRow
, column = endColumn
}
}
errorToString : Error -> String
errorToString error_ =
"- \"" ++ Rule.errorMessage error_ ++ "\" at " ++ rangeAsString (Rule.errorRange error_)
rangeAsString : Range -> String
rangeAsString { start, end } =
"{ start = { row = " ++ String.fromInt start.row ++ ", column = " ++ String.fromInt start.column ++ " }, end = { row = " ++ String.fromInt end.row ++ ", column = " ++ String.fromInt end.column ++ " } }"
expectedMoreErrors : List ExpectedErrorData -> String
expectedMoreErrors missingExpectedErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length missingExpectedErrors
in
"I expected to see "
++ String.fromInt numberOfErrors
++ " more "
++ pluralizeErrors numberOfErrors
++ ":\n\n"
++ (missingExpectedErrors
|> List.map expectedErrorToString
|> String.join "\n"
)
wrapInQuotes : String -> String
wrapInQuotes string =
"\"" ++ string ++ "\""
tooManyErrors : List Error -> String
tooManyErrors extraErrors =
let
numberOfErrors : Int
numberOfErrors =
List.length extraErrors
in
"I found "
++ String.fromInt numberOfErrors
++ " "
++ pluralizeErrors numberOfErrors
++ " too many:\n\n"
++ (extraErrors
|> List.map errorToString
|> String.join "\n"
)
locationIsAmbiguousInSourceCode : SourceCode -> Error -> String -> List Int -> String
locationIsAmbiguousInSourceCode sourceCode error_ under occurrencesInSourceCode =
"""Your test passes, but where the message appears is ambiguous.
You are looking for the following error message:
`""" ++ Rule.errorMessage error_ ++ """`
and expecting to see it under:
""" ++ formatSourceCode under ++ """
I found """ ++ String.fromInt (List.length occurrencesInSourceCode) ++ """ locations where that code appeared. Please use `Lint.Rule.atExactly` to make the part you were targetting unambiguous.
Tip: I found them at:
""" ++ listOccurrencesAsLocations sourceCode under occurrencesInSourceCode
impossibleState : String
impossibleState =
"Oh no! I'm in an impossible state. I found an error at a location that I could not find back. Please let me know and give me an SSCCE (http://sscce.org/) here: https://github.com/jfmengels/elm-lint/issues."
pluralizeErrors : Int -> String
pluralizeErrors n =
if n == 1 then
"error"
else
"errors"
expectedErrorToString : ExpectedErrorData -> String
expectedErrorToString expectedError =
"- " ++ wrapInQuotes expectedError.message
formatSourceCode : String -> String
formatSourceCode string =
let
lines =
String.lines string
in
if List.length lines == 1 then
"`" ++ string ++ "`"
else
lines
|> List.map (\str -> " " ++ str)
|> String.join "\n"
|> (\str -> "```\n" ++ str ++ "\n ```")

View File

@ -2,29 +2,29 @@ module ErrorMessageTest exposing (all)
import Elm.Syntax.Range exposing (Range) import Elm.Syntax.Range exposing (Range)
import Expect import Expect
import Lint.Internal.Test exposing (ExpectedError, LintResult)
import Lint.Rule as Rule exposing (Error) import Lint.Rule as Rule exposing (Error)
import Lint.Test.ErrorMessage as ErrorMessage exposing (ExpectedErrorData)
import Test exposing (Test, describe, test) import Test exposing (Test, describe, test)
all : Test all : Test
all = all =
describe "Test.ErrorMessage" describe "Test.ErrorMessage"
[ parsingErrorMessageTest [ parsingFailureTest
, didNotExpectErrorsTest , didNotExpectErrorsTest
, messageMismatchErrorTest , messageMismatchTest
, underMismatchErrorTest , underMismatchTest
, wrongLocationErrorTest , wrongLocationTest
, notEnoughErrorsTest , expectedMoreErrorsTest
, tooManyErrorsTest , tooManyErrorsTest
] ]
parsingErrorMessageTest : Test parsingFailureTest : Test
parsingErrorMessageTest = parsingFailureTest =
test "parsingErrorMessage" <| test "parsingFailure" <|
\() -> \() ->
Lint.Internal.Test.parsingErrorMessage ErrorMessage.parsingFailure
|> Expect.equal (String.trim """ |> Expect.equal (String.trim """
I could not parse the test source code, because it was not syntactically valid Elm code. I could not parse the test source code, because it was not syntactically valid Elm code.
@ -44,7 +44,7 @@ didNotExpectErrorsTest =
, Rule.error "Some other error" dummyRange , Rule.error "Some other error" dummyRange
] ]
in in
Lint.Internal.Test.didNotExpectErrors errors ErrorMessage.didNotExpectErrors errors
|> Expect.equal (String.trim """ |> Expect.equal (String.trim """
I expected no errors but found: I expected no errors but found:
@ -53,23 +53,22 @@ I expected no errors but found:
""") """)
messageMismatchErrorTest : Test messageMismatchTest : Test
messageMismatchErrorTest = messageMismatchTest =
test "messageMismatchError" <| test "messageMismatch" <|
\() -> \() ->
let let
expectedError : ExpectedError expectedError : ExpectedErrorData
expectedError = expectedError =
Lint.Internal.Test.error { message = "Forbidden use of Debug"
{ message = "Forbidden use of Debug" , under = "Debug.log"
, under = "Debug.log" }
}
error : Error error : Error
error = error =
Rule.error "Forbidden use of Debu" dummyRange Rule.error "Forbidden use of Debu" dummyRange
in in
Lint.Internal.Test.messageMismatchError expectedError error ErrorMessage.messageMismatch expectedError error
|> Expect.equal (String.trim """ |> Expect.equal (String.trim """
I was looking for the error with the following message: I was looking for the error with the following message:
@ -80,9 +79,9 @@ but I found the following error message:
`Forbidden use of Debu`""") `Forbidden use of Debu`""")
underMismatchErrorTest : Test underMismatchTest : Test
underMismatchErrorTest = underMismatchTest =
describe "underMismatchError" describe "underMismatch"
[ test "with single-line extracts" <| [ test "with single-line extracts" <|
\() -> \() ->
let let
@ -90,7 +89,7 @@ underMismatchErrorTest =
error = error =
Rule.error "Some error" dummyRange Rule.error "Some error" dummyRange
in in
Lint.Internal.Test.underMismatchError ErrorMessage.underMismatch
error error
{ under = "abcd" { under = "abcd"
, codeAtLocation = "abcd = 1" , codeAtLocation = "abcd = 1"
@ -116,7 +115,7 @@ Hint: Maybe you're passing the `Range` of a wrong node when calling `Rule.error`
error = error =
Rule.error "Some other error" dummyRange Rule.error "Some other error" dummyRange
in in
Lint.Internal.Test.underMismatchError ErrorMessage.underMismatch
error error
{ under = "abcd =\n 1\n + 2" { under = "abcd =\n 1\n + 2"
, codeAtLocation = "abcd =\n 1" , codeAtLocation = "abcd =\n 1"
@ -145,9 +144,9 @@ Hint: Maybe you're passing the `Range` of a wrong node when calling `Rule.error`
] ]
wrongLocationErrorTest : Test wrongLocationTest : Test
wrongLocationErrorTest = wrongLocationTest =
describe "wrongLocationError" describe "wrongLocation"
[ test "with single-line extracts" <| [ test "with single-line extracts" <|
\() -> \() ->
let let
@ -157,7 +156,7 @@ wrongLocationErrorTest =
"Some error" "Some error"
{ start = { row = 3, column = 1 }, end = { row = 3, column = 5 } } { start = { row = 3, column = 1 }, end = { row = 3, column = 5 } }
in in
Lint.Internal.Test.wrongLocationError ErrorMessage.wrongLocation
error error
{ start = { row = 2, column = 1 }, end = { row = 2, column = 5 } } { start = { row = 2, column = 1 }, end = { row = 2, column = 5 } }
"abcd" "abcd"
@ -187,7 +186,7 @@ but I found it at:
"Some other error" "Some other error"
{ start = { row = 4, column = 1 }, end = { row = 5, column = 3 } } { start = { row = 4, column = 1 }, end = { row = 5, column = 3 } }
in in
Lint.Internal.Test.wrongLocationError ErrorMessage.wrongLocation
error error
{ start = { row = 2, column = 1 }, end = { row = 3, column = 3 } } { start = { row = 2, column = 1 }, end = { row = 3, column = 3 } }
"abcd =\n 1" "abcd =\n 1"
@ -214,24 +213,22 @@ but I found it at:
] ]
notEnoughErrorsTest : Test expectedMoreErrorsTest : Test
notEnoughErrorsTest = expectedMoreErrorsTest =
test "notEnoughErrors" <| test "expectedMoreErrors" <|
\() -> \() ->
let let
missingErrors : List ExpectedError missingErrors : List ExpectedErrorData
missingErrors = missingErrors =
[ Lint.Internal.Test.error [ { message = "Forbidden use of Debug"
{ message = "Forbidden use of Debug" , under = "Debug.log"
, under = "Debug.log" }
} , { message = "Forbidden use of Debug"
, Lint.Internal.Test.error , under = "Debug.log"
{ message = "Forbidden use of Debug" }
, under = "Debug.log"
}
] ]
in in
Lint.Internal.Test.notEnoughErrors missingErrors ErrorMessage.expectedMoreErrors missingErrors
|> Expect.equal (String.trim """ |> Expect.equal (String.trim """
I expected to see 2 more errors: I expected to see 2 more errors:
@ -253,7 +250,7 @@ tooManyErrorsTest =
{ start = { row = 2, column = 1 }, end = { row = 2, column = 5 } } { start = { row = 2, column = 1 }, end = { row = 2, column = 5 } }
] ]
in in
Lint.Internal.Test.tooManyErrors extraErrors ErrorMessage.tooManyErrors extraErrors
|> Expect.equal (String.trim """ |> Expect.equal (String.trim """
I found 1 error too many: I found 1 error too many:
@ -272,7 +269,7 @@ I found 1 error too many:
{ start = { row = 3, column = 1 }, end = { row = 3, column = 5 } } { start = { row = 3, column = 1 }, end = { row = 3, column = 5 } }
] ]
in in
Lint.Internal.Test.tooManyErrors extraErrors ErrorMessage.tooManyErrors extraErrors
|> Expect.equal (String.trim """ |> Expect.equal (String.trim """
I found 2 errors too many: I found 2 errors too many:
@ -282,9 +279,9 @@ I found 2 errors too many:
] ]
locationIsAmbiguousInSourceCodeErrorTest : Test locationIsAmbiguousInSourceCodeTest : Test
locationIsAmbiguousInSourceCodeErrorTest = locationIsAmbiguousInSourceCodeTest =
describe "locationIsAmbiguousInSourceCodeError" describe "locationIsAmbiguousInSourceCode"
[ test "with single-line extracts" <| [ test "with single-line extracts" <|
\() -> \() ->
let let
@ -302,7 +299,7 @@ locationIsAmbiguousInSourceCodeErrorTest =
"Some error" "Some error"
{ start = { row = 3, column = 1 }, end = { row = 3, column = 5 } } { start = { row = 3, column = 1 }, end = { row = 3, column = 5 } }
in in
Lint.Internal.Test.locationIsAmbiguousInSourceCodeError ErrorMessage.locationIsAmbiguousInSourceCode
sourceCode sourceCode
error error
under under
@ -341,7 +338,7 @@ Tip: I found them at:
"Some other error" "Some other error"
{ start = { row = 3, column = 1 }, end = { row = 4, column = 3 } } { start = { row = 3, column = 1 }, end = { row = 4, column = 3 } }
in in
Lint.Internal.Test.locationIsAmbiguousInSourceCodeError ErrorMessage.locationIsAmbiguousInSourceCode
sourceCode sourceCode
error error
under under