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 Name
s can then end up in code generated using TH.
I'd suggest that emitting code that refers to Name
s 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