Linter that spots passing PS objects to the FFI

Does anyone have a linter they use that spots when a PS object is passed to the FFI, either directly or as part of a Record/Array? Say for example one does:

data Foo = A | B
foreign foo :: Foo -> Int
z = foo A

This of course is perfectly valid PureScript, but often reflects an error where someone assumed a PureScript object would be converted to a string or, if it is a constructor, its value would be passed to the FFI (ie if it was data MyString = MyString String that String would go to the FFI if one passed a MyString String). That sort of programming error is difficult to spot. If anyone has a linter that could pick up on those things, it’d be super helpful!

1 Like

Is it possible to do this robustly without typechecking first? For example, polymorphic FFI declarations.

I’m not sure how polymorphic FFI declarations would help in this case, although I’m curious to hear more.

The goal is to have a linting rule prohibiting someone from writing:

newtype MyInt = MyInt Int
foreign doSomething :: MyInt -> Boolean

and perhaps suggesting:

foreign doSomething :: Int -> Boolean

So basically, with this rule applied, the FFI would only be allowed plain types like Int or Boolean, arrays/records, and types that are foreign themselves, like Foreign or Object or a Window. I realize that in many cases folks do want to pass PureScript-created types to the FFI (ie passing a Maybe Int to the FFI), but it would be nice to be able to prohibit these with a linter.

I think Nate meant if you had a polymorphic type like

foreign import myIdentity :: forall a. a -> a

what would the linter do? Would it allow or disallow such a type? a could be instantiated with a JS primitive type or a PS type

I don’t think this is worth doing because I want to pass PS types often, but this could be achieved with some language support I think?

-- compiler only instances class, or requires unsafeCoerce for users to implement
class Foreign a

instance foreignInt :: Foreign Int

-- ...

instance foreignFn ::
  ( Foreign a
  , Foreign b
  ) => Foreign (a -> b)

data Foo = Foo Int

foreign import foo :: Foo -> Int

-- the actual type of foo (the dict discard is added automatically, or we use ErasedConstraint)
-- foo :: Foreign (Foo -> Int) => Foo -> Int

-- Error: No instance found for Foreign Foo
test1 :: Int
test1 = foo (Foo 0)

unsafeForeign :: forall t r. Proxy t => (Foreign t => r) -> r
unsafeForeign _ f = (coerce f) {}
  where
    coerce :: (Foreign t => r) -> ({} -> r)
    coerce = unsafeCoerce

-- Works
test2 :: Int
test2 = unsafeForeign (Proxy :: Proxy Foo) (foo (Foo 0))

You could do it without compiler support actually, but it’s a little more verbose

foreign import data Foreign :: Type -> Type

class IsForeign a

instance isForeignInt :: IsForeign Int

-- ...

instance isForeignFn ::
  ( IsForeign a
  , IsForeign b
  ) => IsForeign (a -> b)


runForeign :: forall a. IsForeign a => Foreign a -> a
runForeign frn = unsafeCoerce frn

unsafeForeign :: forall t r. Proxy t => (IsForeign t => r) -> r
unsafeForeign _ f = (coerce f) {}
  where
    coerce :: (IsForeign  => r) -> ({} -> r)
    coerce = unsafeCoerce

data Foo = Foo Int

foreign import foo :: Foreign (Foo -> Int)

-- No instance found error
test1 :: Int
test1 = runForeign foo (Foo 0)

test2 :: Int
test2 = unsafeForeign (Proxy :: Proxy Foo) (runForeign foo (Foo 0))

You could then run a linter that just warns on using unsafeForeign and writing foreign imports that aren’t explicitly wrapped in the Foreign constructor

1 Like

The motivation comes from our code base at Meeshkan - while we try to catch these in code review, sometimes they slip through and cause nasty bugs. So if I can make a linter that can run as part of the CI pipeline, that’d help.

This looks great! Thanks a lot for the suggestion.

2 Likes

Ah sorry @natefaubion I misread your comment. Yeah, the linter would also have to forbid polymorphic FFI declarations.

Curiously, could solving this on the JS toolchain end handle this?

Wondering if using/emitting typescript/flow code from the purescript side would help.

Perhaps together with https://github.com/purescript/purescript/issues/4020 . It can make out for a nice js/ts interop.

1 Like

I never considered the use of a newtype in a FFI declaration to be a problem, but this thread raised a bit of doubt for me. So I asked about it in Discord and received some good answers: Discord

The TL;DR is: Don’t worry too much about it, but do be aware that the compiler won’t help you avoid a bug if you change the newtype to an ADT without making additional changes.

1 Like

newtypes have been really useful, agreed. The issue has come up when using an ADT & it hits the FFI. Sometimes it’s easy to accidentally convert a newtype to an ADT or vice-versa when doing refactoring.