├── .ghci ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Setup.hs ├── should-not-typecheck.cabal ├── src └── Test │ └── ShouldNotTypecheck.hs ├── stack.yaml └── test └── ShouldNotTypecheckSpec.hs /.ghci: -------------------------------------------------------------------------------- 1 | :l src/Test/ShouldNotTypecheck.hs 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | dist/ 3 | .cabal-sandbox/ 4 | cabal.sandbox.config 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/hvr/multi-ghc-travis 2 | 3 | env: 4 | - CABALVER=1.18 GHCVER=7.6.3 5 | - CABALVER=1.18 GHCVER=7.8.4 6 | - CABALVER=1.22 GHCVER=7.10.1 7 | - CABALVER=1.24 GHCVER=8.0.1 8 | 9 | before_install: 10 | - travis_retry sudo add-apt-repository -y ppa:hvr/ghc 11 | - travis_retry sudo apt-get update 12 | - travis_retry sudo apt-get install cabal-install-$CABALVER ghc-$GHCVER 13 | - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH 14 | 15 | install: 16 | - cabal --version 17 | - echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo '?')]" 18 | - travis_retry cabal update 19 | - cabal install --only-dependencies --enable-tests --enable-benchmarks 20 | 21 | # Here starts the actual work to be performed for the package under test; any command which exits with a non-zero exit code causes the build to fail. 22 | script: 23 | - if [ -f configure.ac ]; then autoreconf -i; fi 24 | - cabal configure --enable-tests --enable-benchmarks -v2 # -v2 provides useful information for debugging 25 | - cabal build # this builds all libraries and executables (including tests/benchmarks) 26 | - cabal test 27 | - cabal check 28 | - cabal sdist # tests that a source-distribution can be generated 29 | 30 | # Check that the resulting source distribution can be built & installed. 31 | # If there are no other `.tar.gz` files in `dist`, this can be even simpler: 32 | # `cabal install --force-reinstalls dist/*-*.tar.gz` 33 | - SRC_TGZ=$(cabal info . | awk '{print $2;exit}').tar.gz && 34 | (cd dist && cabal install --force-reinstalls "$SRC_TGZ") 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `should-not-typecheck` changelog 2 | 3 | ## 2.1.0 4 | * Support GHC 8.0.1 (see https://github.com/CRogers/should-not-typecheck/pull/6). 5 | 6 | ## 2.0.1 7 | * Support HUnit 1.3 8 | 9 | ## 2.0 10 | * Changed API to require `NFData a` so we can fully evaluate expressions, rather than just converting to WHNF. 11 | 12 | ## 1.0.1 13 | * Use `throwIO` instead of `throw` for exception ordering safety. 14 | 15 | ## 1.0 16 | * Stabilise API at 1.0 release. 17 | * Allow building on 7.6.3. 18 | 19 | ## 0.1.0.0 20 | * Initial version. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Callum Rogers 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Callum Rogers nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | `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`). 4 | 5 | ## Example (hspec) 6 | 7 | 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. 8 | 9 | ```haskell 10 | {-# OPTIONS_GHC -fdefer-type-errors #-} -- Very important! 11 | 12 | module Main where 13 | 14 | import Test.Hspec (hspec, describe, it) 15 | import Test.ShouldNotTypecheck (shouldNotTypecheck) 16 | 17 | main :: IO () 18 | main = hspec $ do 19 | describe "Type Tests" $ do 20 | it "should not allow an Int to be a String" $ 21 | shouldNotTypecheck (4 :: String) 22 | ``` 23 | 24 | It can be used similarly with HUnit. 25 | 26 | ### `NFData a` constraint 27 | 28 | 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: 29 | 30 | ```haskell 31 | {-# LANGUAGE DeriveGeneric #-} 32 | 33 | import GHC.Generics (Generic) 34 | 35 | data SomeType a = WithSome | DataConstructors a 36 | deriving Generic 37 | 38 | instance NFData a => NFData (SomeType a) 39 | ``` 40 | 41 | 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. 42 | 43 | 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. 44 | 45 | #### GADTs 46 | 47 | 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: 48 | 49 | ```haskell 50 | {-# LANGUAGE GADTs #-} 51 | 52 | import Control.DeepSeq (NFData) 53 | 54 | data Expr t where 55 | IntVal :: Int -> Expr Int 56 | BoolVal :: Bool -> Expr Bool 57 | Add :: Expr Int -> Expr Int -> Expr Int 58 | 59 | instance NFData (Expr t) where 60 | rnf expr = case expr of 61 | IntVal i -> rnf i -- call rnf on every subvalue 62 | BoolVal b -> rnf b 63 | Add l r -> rnf l `seq` rnf r -- and `seq` multiple values together 64 | 65 | -- Now we can test expressions like: 66 | badExpr = Add (IntVal 4) (BoolVal True) 67 | -- do not typecheck! 68 | ``` 69 | 70 | If you forget to specify an `NFData` instance for a type `should-not-typecheck` should warn you. 71 | 72 | ## Motivation 73 | 74 | 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: 75 | 76 | ```haskell 77 | {-# LANGUAGE GADTs #-} 78 | 79 | data Expr t where 80 | IntVal :: Int -> Expr Int 81 | BoolVal :: Bool -> Expr Bool 82 | Add :: Expr Int -> Expr Int -> Expr Int 83 | -- ... 84 | ``` 85 | 86 | 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. 87 | 88 | ## Limitations 89 | 90 | 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: 91 | 92 | ```haskell 93 | {-# OPTIONS_GHC -fdefer-type-errors #-} 94 | 95 | -- ... 96 | 97 | main :: IO () 98 | main = hspec $ do 99 | describe 4 $ do -- Oops! 100 | -- ... 101 | ``` 102 | 103 | 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. 104 | 105 | ### Workaround 106 | 107 | 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. 108 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /should-not-typecheck.cabal: -------------------------------------------------------------------------------- 1 | name: should-not-typecheck 2 | version: 2.1.0 3 | synopsis: A HUnit/hspec assertion library to verify that an expression does not typecheck 4 | description: 5 | For examples and an introduction to the library please take a look at the on github. 6 | homepage: http://github.com/CRogers/should-not-typecheck 7 | license: BSD3 8 | license-file: LICENSE 9 | author: Callum Rogers 10 | maintainer: message.me.on@github.com 11 | -- copyright: 12 | category: Testing 13 | build-type: Simple 14 | extra-source-files: 15 | README.md 16 | , CHANGELOG.md 17 | , stack.yaml 18 | cabal-version: >=1.10 19 | tested-with: GHC == 7.6.3, GHC == 7.8.4, GHC == 7.10.1, GHC == 8.0.1 20 | 21 | library 22 | hs-source-dirs: src 23 | exposed-modules: Test.ShouldNotTypecheck 24 | build-depends: base >= 4.6 && < 5 25 | , HUnit >= 1.2 26 | , deepseq >= 1.3 27 | default-language: Haskell2010 28 | 29 | test-suite tests 30 | type: exitcode-stdio-1.0 31 | hs-source-dirs: test 32 | main-is: ShouldNotTypecheckSpec.hs 33 | build-depends: base 34 | , should-not-typecheck 35 | , HUnit >= 1.2 36 | , hspec >= 2.1 37 | , hspec-expectations >= 0.6 38 | , deepseq 39 | default-language: Haskell2010 40 | 41 | source-repository head 42 | type: git 43 | location: git://github.com/CRogers/should-not-typecheck.git 44 | -------------------------------------------------------------------------------- /src/Test/ShouldNotTypecheck.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP, RankNTypes, GADTs #-} 2 | #if __GLASGOW_HASKELL__ >= 800 3 | #define TheExc TypeError 4 | #else 5 | #define TheExc ErrorCall 6 | #endif 7 | module Test.ShouldNotTypecheck (shouldNotTypecheck) where 8 | 9 | import Control.DeepSeq (force, NFData) 10 | import Control.Exception (evaluate, try, throwIO, TheExc(..)) 11 | import Data.List (isSuffixOf, isInfixOf) 12 | import Test.HUnit.Lang (Assertion, assertFailure) 13 | 14 | {-| 15 | Takes one argument, an expression that should not typecheck. 16 | It will fail the test if the expression does typecheck. 17 | Requires Deferred Type Errors to be enabled for the file it is called in. 18 | See the 19 | for examples and more information. 20 | -} 21 | #if __GLASGOW_HASKELL__ >= 800 22 | shouldNotTypecheck :: NFData a => (() ~ () => a) -> Assertion 23 | #else 24 | shouldNotTypecheck :: NFData a => a -> Assertion 25 | #endif 26 | -- The type for GHC-8.0.1 is a hack, see https://github.com/CRogers/should-not-typecheck/pull/6#issuecomment-211520177 27 | shouldNotTypecheck a = do 28 | result <- try (evaluate $ force a) 29 | case result of 30 | Right _ -> assertFailure "Expected expression to not compile but it did compile" 31 | Left e@(TheExc msg) -> case isSuffixOf "(deferred type error)" msg of 32 | True -> case isInfixOf "No instance for" msg && isInfixOf "NFData" msg of 33 | 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 34 | False -> return () 35 | False -> throwIO e 36 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | flags: {} 2 | packages: 3 | - '.' 4 | extra-deps: [] 5 | resolver: nightly-2015-09-07 6 | -------------------------------------------------------------------------------- /test/ShouldNotTypecheckSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs, TemplateHaskell, CPP #-} 2 | {-# OPTIONS_GHC -fdefer-type-errors #-} 3 | 4 | module Main where 5 | 6 | import Control.DeepSeq 7 | import Control.Exception 8 | import GHC.Generics (Generic) 9 | import Test.Hspec 10 | import Test.Hspec.Expectations (expectationFailure) 11 | import qualified Test.HUnit.Lang as HL 12 | import Test.ShouldNotTypecheck 13 | 14 | data Result 15 | = Success 16 | | Failure 17 | | Error String 18 | 19 | #if MIN_VERSION_HUnit(1,3,0) 20 | toResult :: HL.Result -> Result 21 | toResult result = case result of 22 | HL.Success -> Success 23 | HL.Failure _ _ -> Failure 24 | HL.Error _ msg -> Error msg 25 | #else 26 | toResult :: Maybe (Bool, String) -> Result 27 | toResult result = case result of 28 | Nothing -> Success 29 | Just (True, _) -> Failure 30 | Just (False, msg) -> Error msg 31 | #endif 32 | 33 | shouldFailAssertion :: IO () -> IO () 34 | shouldFailAssertion value = do 35 | result <- HL.performTestCase value 36 | case toResult result of 37 | Success -> expectationFailure "Did not throw an assertion error" 38 | Failure -> return () 39 | Error msg -> expectationFailure $ "Raised an error " ++ msg 40 | 41 | shouldThrowException :: Exception e => e -> IO () -> IO () 42 | shouldThrowException exception value = do 43 | result <- HL.performTestCase value 44 | case toResult result of 45 | Success -> expectationFailure "Did not throw exception: assertion succeeded" 46 | Failure -> expectationFailure "Did not throw exception: assertion failed" 47 | Error msg -> case msg == show exception of 48 | True -> return () 49 | False -> expectationFailure "Incorrect exception propagated" 50 | 51 | data Expr t where 52 | IntVal :: Int -> Expr Int 53 | BoolVal :: Bool -> Expr Bool 54 | Add :: Expr Int -> Expr Int -> Expr Int 55 | 56 | instance NFData (Expr t) where 57 | rnf expr = case expr of 58 | IntVal i -> rnf i 59 | BoolVal b -> rnf b 60 | Add l r -> rnf l `seq` rnf r 61 | 62 | data NoNFDataInstance = NoNFDataInstance 63 | 64 | main :: IO () 65 | main = hspec $ do 66 | describe "shouldNotCompile" $ do 67 | it "should not throw an assertion error when an expression is ill typed" $ do 68 | shouldNotTypecheck ("foo" :: Int) 69 | 70 | it "should throw an assertion error when an expression is well typed" $ do 71 | shouldFailAssertion (shouldNotTypecheck ("foo" :: String)) 72 | 73 | it "should throw an actual exception and not fail the assertion if the expression contains an non-HUnitFailure exception" $ do 74 | let exception = NoMethodError "lol" 75 | shouldThrowException exception (shouldNotTypecheck (throw exception :: Int)) 76 | 77 | it "should propagate an actual exception and not fail the assertion if the expression contains a non-deferred ErrorCall exception" $ do 78 | let exception = ErrorCall "yay" 79 | shouldThrowException exception (shouldNotTypecheck (throw exception :: Int)) 80 | 81 | it "should not throw an assertion when an expression with more than one level of constructors is ill typed" $ do 82 | shouldNotTypecheck (Add (BoolVal True) (IntVal 4)) 83 | 84 | it "should warn if an expression had a type error due to lack of NFData instance" $ do 85 | shouldFailAssertion (shouldNotTypecheck NoNFDataInstance) 86 | --------------------------------------------------------------------------------