should-not-typecheck-2.1.0/0000755000000000000000000000000012705357230013755 5ustar0000000000000000should-not-typecheck-2.1.0/CHANGELOG.md0000644000000000000000000000070212705357230015565 0ustar0000000000000000# `should-not-typecheck` changelog ## 2.1.0 * Support GHC 8.0.1 (see https://github.com/CRogers/should-not-typecheck/pull/6). ## 2.0.1 * Support HUnit 1.3 ## 2.0 * Changed API to require `NFData a` so we can fully evaluate expressions, rather than just converting to WHNF. ## 1.0.1 * Use `throwIO` instead of `throw` for exception ordering safety. ## 1.0 * Stabilise API at 1.0 release. * Allow building on 7.6.3. ## 0.1.0.0 * Initial version. should-not-typecheck-2.1.0/LICENSE0000644000000000000000000000276312705357230014772 0ustar0000000000000000Copyright (c) 2015 Callum Rogers All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Callum Rogers nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. should-not-typecheck-2.1.0/README.md0000644000000000000000000001151412705357230015236 0ustar0000000000000000# should-not-typecheck [![Build Status](https://travis-ci.org/CRogers/should-not-typecheck.svg?branch=master)](https://travis-ci.org/CRogers/should-not-typecheck) [![Hackage](https://img.shields.io/hackage/v/should-not-typecheck.svg)](https://hackage.haskell.org/package/should-not-typecheck) `should-not-typecheck` is a Haskell library which allows you to assert that an expression does not typecheck in your tests. It provides one function, `shouldNotTypecheck`, which takes an expression and will fail the test if it typechecks. `shouldNotTypecheck` returns an HUnit `Assertion` (so it can be used with both `HUnit` and `hspec`). ## Example (hspec) The secret sauce is the [Deferred Type Errors GHC extension](https://downloads.haskell.org/~ghc/7.10.1/docs/html/users_guide/defer-type-errors.html). This allows you to write an ill-typed expression which will throw an exception at run time (rather than erroring out at compile time). `shouldNotTypecheck` tries to catch that exception and fails the test if no deferred type error is caught. ```haskell {-# OPTIONS_GHC -fdefer-type-errors #-} -- Very important! module Main where import Test.Hspec (hspec, describe, it) import Test.ShouldNotTypecheck (shouldNotTypecheck) main :: IO () main = hspec $ do describe "Type Tests" $ do it "should not allow an Int to be a String" $ shouldNotTypecheck (4 :: String) ``` It can be used similarly with HUnit. ### `NFData a` constraint Haskell is a lazy language - deferred type errors will not get evaluated unless we explicitly and deeply force (evaluate) the value. [`NFData`](https://hackage.haskell.org/package/deepseq-1.4.1.1/docs/Control-DeepSeq.html#t:NFData) is a typeclass from the [`deepseq`](https://hackage.haskell.org/package/deepseq) library which allows you to describe how to fully evaluate an expression (convert it to Normal Form). `shouldNotTypecheck` uses this typeclass to fully evaluate expressions passed to it. For vanilla Haskell types you only need to derive `Generic` and the `deepseq` class will handle it for you: ```haskell {-# LANGUAGE DeriveGeneric #-} import GHC.Generics (Generic) data SomeType a = WithSome | DataConstructors a deriving Generic instance NFData a => NFData (SomeType a) ``` In GHC 7.10 [`DeriveAnyClass` can be used](https://hackage.haskell.org/package/deepseq-1.4.1.1/docs/Control-DeepSeq.html#v:rnf) to make it even more succinct. With `deepseq >= 1.4`, this autoderiving `Generic` option is included with the library. With `deepseq <= 1.3` you'll have to use the [`deepseq-generics`](https://hackage.haskell.org/package/deepseq-generics) library as well. #### GADTs With more complex datatypes, like GADTs and those existentially quantified, `DeriveGeneric` does not work. You will need to provide an instance for `NFData` yourself, but not to worry as it follows a pattern: ```haskell {-# LANGUAGE GADTs #-} import Control.DeepSeq (NFData) data Expr t where IntVal :: Int -> Expr Int BoolVal :: Bool -> Expr Bool Add :: Expr Int -> Expr Int -> Expr Int instance NFData (Expr t) where rnf expr = case expr of IntVal i -> rnf i -- call rnf on every subvalue BoolVal b -> rnf b Add l r -> rnf l `seq` rnf r -- and `seq` multiple values together -- Now we can test expressions like: badExpr = Add (IntVal 4) (BoolVal True) -- do not typecheck! ``` If you forget to specify an `NFData` instance for a type `should-not-typecheck` should warn you. ## Motivation Sometimes you want to ensure that it is impossible to type a particular expression. For example, imagine if we were making a typesafe Abstract Syntax Tree of mathematical expressions: ```haskell {-# LANGUAGE GADTs #-} data Expr t where IntVal :: Int -> Expr Int BoolVal :: Bool -> Expr Bool Add :: Expr Int -> Expr Int -> Expr Int -- ... ``` We might want to make sure that `Add (BoolVal True) (IntVal 4)` is not well typed. However, we can't even compile code like this to put in a unit test! This is where `should-not-typecheck` steps in. ## Limitations Unfortunately, we can only turn on deferred type errors for the entire test file rather than just specific expressions. This means that any type error will compile but fail at runtime. For example: ```haskell {-# OPTIONS_GHC -fdefer-type-errors #-} -- ... main :: IO () main = hspec $ do describe 4 $ do -- Oops! -- ... ``` Will create a warning at compile time but not an error. All of the ill-typed expressions we are testing will also produce warnings and it will be hard to immediately see which ones matter. The upside is that the test-suite will still fail if there are errors. ### Workaround You can separate out the ill-typed expressions we are testing and test boilerplate into separate files and only turn on deferred type errors for the expressions. This means that type errors in test code will still be found at compile time. The downside is your tests may now be harder to read. should-not-typecheck-2.1.0/Setup.hs0000644000000000000000000000005612705357230015412 0ustar0000000000000000import Distribution.Simple main = defaultMain should-not-typecheck-2.1.0/should-not-typecheck.cabal0000644000000000000000000000276312705357230021022 0ustar0000000000000000name: should-not-typecheck version: 2.1.0 synopsis: A HUnit/hspec assertion library to verify that an expression does not typecheck description: For examples and an introduction to the library please take a look at the on github. homepage: http://github.com/CRogers/should-not-typecheck license: BSD3 license-file: LICENSE author: Callum Rogers maintainer: message.me.on@github.com -- copyright: category: Testing build-type: Simple extra-source-files: README.md , CHANGELOG.md , stack.yaml cabal-version: >=1.10 tested-with: GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.1, GHC == 8.0.1 library hs-source-dirs: src exposed-modules: Test.ShouldNotTypecheck build-depends: base >= 4.6 && < 5 , HUnit >= 1.2 , deepseq >= 1.3 default-language: Haskell2010 test-suite tests type: exitcode-stdio-1.0 hs-source-dirs: test main-is: ShouldNotTypecheckSpec.hs build-depends: base , should-not-typecheck , HUnit >= 1.2 , hspec >= 2.1 , hspec-expectations >= 0.6 , deepseq default-language: Haskell2010 source-repository head type: git location: git://github.com/CRogers/should-not-typecheck.git should-not-typecheck-2.1.0/stack.yaml0000644000000000000000000000010612705357230015743 0ustar0000000000000000flags: {} packages: - '.' extra-deps: [] resolver: nightly-2015-09-07 should-not-typecheck-2.1.0/src/0000755000000000000000000000000012705357230014544 5ustar0000000000000000should-not-typecheck-2.1.0/src/Test/0000755000000000000000000000000012705357230015463 5ustar0000000000000000should-not-typecheck-2.1.0/src/Test/ShouldNotTypecheck.hs0000644000000000000000000000351012705357230021575 0ustar0000000000000000{-# LANGUAGE CPP, RankNTypes, GADTs #-} #if __GLASGOW_HASKELL__ >= 800 #define TheExc TypeError #else #define TheExc ErrorCall #endif module Test.ShouldNotTypecheck (shouldNotTypecheck) where import Control.DeepSeq (force, NFData) import Control.Exception (evaluate, try, throwIO, TheExc(..)) import Data.List (isSuffixOf) import Test.HUnit.Lang (Assertion, assertFailure) -- Taken from base-4.8.0.0:Data.List isSubsequenceOf :: (Eq a) => [a] -> [a] -> Bool isSubsequenceOf [] _ = True isSubsequenceOf _ [] = False isSubsequenceOf a@(x:a') (y:b) | x == y = isSubsequenceOf a' b | otherwise = isSubsequenceOf a b {-| Takes one argument, an expression that should not typecheck. It will fail the test if the expression does typecheck. Requires Deferred Type Errors to be enabled for the file it is called in. See the for examples and more infomation. -} #if __GLASGOW_HASKELL__ >= 800 shouldNotTypecheck :: NFData a => (() ~ () => a) -> Assertion #else shouldNotTypecheck :: NFData a => a -> Assertion #endif -- The type for GHC-8.0.1 is a hack, see https://github.com/CRogers/should-not-typecheck/pull/6#issuecomment-211520177 shouldNotTypecheck a = do result <- try (evaluate $ force a) case result of Right _ -> assertFailure "Expected expression to not compile but it did compile" Left e@(TheExc msg) -> case isSuffixOf "(deferred type error)" msg of True -> case isSubsequenceOf "No instance for" msg && isSubsequenceOf "NFData" msg of True -> assertFailure $ "Make sure the expression has an NFData instance! See docs at https://github.com/CRogers/should-not-typecheck#nfdata-a-constraint. Full error:\n" ++ msg False -> return () False -> throwIO e should-not-typecheck-2.1.0/test/0000755000000000000000000000000012705357230014734 5ustar0000000000000000should-not-typecheck-2.1.0/test/ShouldNotTypecheckSpec.hs0000644000000000000000000000563112705357230021667 0ustar0000000000000000{-# LANGUAGE GADTs, TemplateHaskell, CPP #-} {-# OPTIONS_GHC -fdefer-type-errors #-} module Main where import Control.DeepSeq import Control.Exception import GHC.Generics (Generic) import Test.Hspec import Test.Hspec.Expectations (expectationFailure) import qualified Test.HUnit.Lang as HL import Test.ShouldNotTypecheck data Result = Success | Failure | Error String #if MIN_VERSION_HUnit(1,3,0) toResult :: HL.Result -> Result toResult result = case result of HL.Success -> Success HL.Failure _ _ -> Failure HL.Error _ msg -> Error msg #else toResult :: Maybe (Bool, String) -> Result toResult result = case result of Nothing -> Success Just (True, _) -> Failure Just (False, msg) -> Error msg #endif shouldFailAssertion :: IO () -> IO () shouldFailAssertion value = do result <- HL.performTestCase value case toResult result of Success -> expectationFailure "Did not throw an assertion error" Failure -> return () Error msg -> expectationFailure $ "Raised an error " ++ msg shouldThrowException :: Exception e => e -> IO () -> IO () shouldThrowException exception value = do result <- HL.performTestCase value case toResult result of Success -> expectationFailure "Did not throw exception: assertion succeeded" Failure -> expectationFailure "Did not throw exception: assertion failed" Error msg -> case msg == show exception of True -> return () False -> expectationFailure "Incorrect exception propagated" data Expr t where IntVal :: Int -> Expr Int BoolVal :: Bool -> Expr Bool Add :: Expr Int -> Expr Int -> Expr Int instance NFData (Expr t) where rnf expr = case expr of IntVal i -> rnf i BoolVal b -> rnf b Add l r -> rnf l `seq` rnf r data NoNFDataInstance = NoNFDataInstance main :: IO () main = hspec $ do describe "shouldNotCompile" $ do it "should not throw an assertion error when an expression is ill typed" $ do shouldNotTypecheck ("foo" :: Int) it "should throw an assertion error when an expression is well typed" $ do shouldFailAssertion (shouldNotTypecheck ("foo" :: String)) it "should throw an actual exception and not fail the assertion if the expression contains an non-HUnitFailure exception" $ do let exception = NoMethodError "lol" shouldThrowException exception (shouldNotTypecheck (throw exception :: Int)) it "should propagate an actual exception and not fail the assertion if the expression contains a non-deferred ErrorCall exception" $ do let exception = ErrorCall "yay" shouldThrowException exception (shouldNotTypecheck (throw exception :: Int)) it "should not throw an assertion when an expression with more than one level of constructors is ill typed" $ do shouldNotTypecheck (Add (BoolVal True) (IntVal 4)) it "should warn if an expression had a type error due to lack of NFData instance" $ do shouldFailAssertion (shouldNotTypecheck NoNFDataInstance)