Template Haskell allows breaking encapsulation
Summary
Use of reify from template-haskell allows one to trivially break encapsulation by accessing un-exported constructors/fields.
This is distinct from uses of template-haskell that make use of unsafeCoerce to access internal compiler state.
This happens through the normal reify interface.
reify allows us to see all constructors/fields whether they are exported or not.
This, I think, is fine. The issue is that these Names can then end up in code generated using TH.
I'd suggest that emitting code that refers to Names that aren't imported in the module should at least lead to a warning if not an error.
We discovered this at work years ago by moving a call to makeLenses into another module, and found that un-exported fields were still being given lenses, and that they still worked.
Steps to reproduce
Take the following two modules:
❯ cat A.hs
module A (Foo) where
data Foo = MkFoo { unexported :: Int }
deriving (Show)
❯ cat B.hs
{-# LANGUAGE TemplateHaskell #-}
module B where
import A (Foo)
import Language.Haskell.TH.Syntax
import Language.Haskell.TH.Lib
foo :: Foo
foo = $(do {
TyConI (DataD _ _ _ _ [RecC constrName _ ] _) <- reify ''Foo;
[| $(conE constrName) 100 |]
})
main = print foo
Then compile and run with:
❯ ghc --make A.hs B.hs -main-is B
❯ ./B
MkFoo {unexported = 100}
Expected behavior
The compiler should at least warn us that TH is producing code that refers to values that aren't imported, and can't be imported in this module.
Environment
- GHC version used: 9.4, but I'm pretty sure it happens with any recent version of GHC