git.iko.soy/rowtype-yoga/purescript-native
on golang help sign in
Check that FFI imports match with implementations

* Refactor foreign module parsing Extract functions for matching certain types of nodes in a JS AST, in preparation for checking that FFI declarations match with foreign imports. * Add getExportedIdentifiers Also update comments * Check for unused or missing FFI declarations

commit5fe25aabc2107584d1d514530b263c18a1058494 authorHarry Garrood <harry@garrood.me> dateWed 6 Apr 2016, 04:07 (11 years ago) parent0d78fc1
4 files changed+181−54
src/Language/PureScript/Bundle.hs+103−44 src/Language/PureScript/Errors.hs+20−5 src/Language/PureScript/Make.hs+57−4 src/Language/PureScript/Parser/JS.hs+1−1
src/Language/PureScript/Bundle.hs+103−44
@@ -27,6 +27,7 @@ module Language.PureScript.Bundle (
2727 , ModuleType(..)
2828 , ErrorMessage(..)
2929 , printErrorMessage
30+ , getExportedIdentifiers
3031) where
3132
3233import Prelude ()
@@ -229,53 +230,20 @@ toModule requirePath mids mid top
229230 err = throwError . ErrorInModule mid
230231
231232 toModuleElement :: JSStatement -> m ModuleElement
232− -- var ModuleName = require("file");
233233 toModuleElement stmt
234− | JSVariable _ jsInit _ <- stmt
235− , [JSVarInitExpression var varInit] <- commaList jsInit
236− , JSIdentifier _ importName <- var
237− , JSVarInit _ jsInitEx <- varInit
238− , JSMemberExpression req _ argsE _ <- jsInitEx
239− , JSIdentifier _ "require" <- req
240− , [ Just importPath ] <- map fromStringLiteral (commaList argsE)
241− , importPath' <- checkImportPath requirePath importPath mid mids
242− = pure (Require stmt importName importPath')
243− -- var foo = expr;
244− toModuleElement stmt
245− | JSVariable _ jsInit _ <- stmt
246− , [JSVarInitExpression var varInit] <- commaList jsInit
247− , JSIdentifier _ name <- var
248− , JSVarInit _ decl <- varInit
249− = pure (Member stmt False name decl [])
250− -- exports.foo = expr; exports["foo"] = expr;
234+ | Just (importName, importPath) <- matchRequire requirePath mids mid stmt
235+ = pure (Require stmt importName importPath)
251236 toModuleElement stmt
252− | JSAssignStatement e (JSAssign _) decl _ <- stmt
253− , Just name <- accessor e
254− = pure (Member stmt True name decl [])
255− where
256− accessor :: JSExpression -> Maybe String
257− accessor (JSMemberDot exports _ nm)
258− | JSIdentifier _ "exports" <- exports
259− , JSIdentifier _ name <- nm
260− = Just name
261− accessor (JSMemberSquare exports _ nm _)
262− | JSIdentifier _ "exports" <- exports
263− , Just name <- fromStringLiteral nm
264− = Just name
265− accessor _ = Nothing
266− -- module.exports = { ... }
237+ | Just (exported, name, decl) <- matchMember stmt
238+ = pure (Member stmt exported name decl [])
267239 toModuleElement stmt
268− | JSAssignStatement e (JSAssign _) decl _ <- stmt
269− , JSMemberDot module' _ exports <- e
270− , JSIdentifier _ "module" <- module'
271− , JSIdentifier _ "exports" <- exports
272− , JSObjectLiteral _ props _ <- decl
240+ | Just props <- matchExportsAssignment stmt
273241 = (ExportsList <$> traverse toExport (trailingCommaList props))
274242 where
275243 toExport :: JSObjectProperty -> m (ExportType, String, JSExpression, [Key])
276244 toExport (JSPropertyNameandValue name _ [val]) =
277245 (,,val,[]) <$> exportType val
278− <*> extractLabel name
246+ <*> extractLabel' name
279247 toExport _ = err UnsupportedExport
280248
281249 exportType :: JSExpression -> m ExportType
@@ -287,14 +255,105 @@ toModule requirePath mids mid top
287255 = pure ForeignReexport
288256 exportType (JSIdentifier _ s) = pure (RegularExport s)
289257 exportType _ = err UnsupportedExport
290−--
291− extractLabel :: JSPropertyName -> m String
292− extractLabel (JSPropertyString _ nm) = pure (trimStringQuotes nm)
293− extractLabel (JSPropertyIdent _ nm) = pure nm
294− extractLabel _ = err UnsupportedExport
258+
259+ extractLabel' = maybe (err UnsupportedExport) pure . extractLabel
295260
296261 toModuleElement other = pure (Other other)
297262
263+-- Get a list of all the exported identifiers from a foreign module.
264+--
265+-- TODO: what if we assign to exports.foo and then later assign to
266+-- module.exports (presumably overwriting exports.foo)?
267+getExportedIdentifiers :: (MonadError ErrorMessage m)
268+ => String
269+ -> JSAST
270+ -> m [String]
271+getExportedIdentifiers mname top
272+ | JSAstProgram stmts _ <- top = concat <$> traverse go stmts
273+ | otherwise = err InvalidTopLevel
274+ where
275+ err = throwError . ErrorInModule (ModuleIdentifier mname Foreign)
276+
277+ go stmt
278+ | Just props <- matchExportsAssignment stmt
279+ = traverse toIdent (trailingCommaList props)
280+ | Just (True, name, _) <- matchMember stmt
281+ = pure [name]
282+ | otherwise
283+ = pure []
284+
285+ toIdent (JSPropertyNameandValue name _ [_]) =
286+ extractLabel' name
287+ toIdent _ =
288+ err UnsupportedExport
289+
290+ extractLabel' = maybe (err UnsupportedExport) pure . extractLabel
291+
292+-- Matches JS statements like this:
293+-- var ModuleName = require("file");
294+matchRequire :: Maybe FilePath
295+ -> S.Set String
296+ -> ModuleIdentifier
297+ -> JSStatement
298+ -> Maybe (String, Either String ModuleIdentifier)
299+matchRequire requirePath mids mid stmt
300+ | JSVariable _ jsInit _ <- stmt
301+ , [JSVarInitExpression var varInit] <- commaList jsInit
302+ , JSIdentifier _ importName <- var
303+ , JSVarInit _ jsInitEx <- varInit
304+ , JSMemberExpression req _ argsE _ <- jsInitEx
305+ , JSIdentifier _ "require" <- req
306+ , [ Just importPath ] <- map fromStringLiteral (commaList argsE)
307+ , importPath' <- checkImportPath requirePath importPath mid mids
308+ = Just (importName, importPath')
309+ | otherwise
310+ = Nothing
311+
312+-- Matches JS member declarations.
313+matchMember :: JSStatement -> Maybe (Bool, String, JSExpression)
314+matchMember stmt
315+ -- var foo = expr;
316+ | JSVariable _ jsInit _ <- stmt
317+ , [JSVarInitExpression var varInit] <- commaList jsInit
318+ , JSIdentifier _ name <- var
319+ , JSVarInit _ decl <- varInit
320+ = Just (False, name, decl)
321+ -- exports.foo = expr; exports["foo"] = expr;
322+ | JSAssignStatement e (JSAssign _) decl _ <- stmt
323+ , Just name <- accessor e
324+ = Just (True, name, decl)
325+ | otherwise
326+ = Nothing
327+ where
328+ accessor :: JSExpression -> Maybe String
329+ accessor (JSMemberDot exports _ nm)
330+ | JSIdentifier _ "exports" <- exports
331+ , JSIdentifier _ name <- nm
332+ = Just name
333+ accessor (JSMemberSquare exports _ nm _)
334+ | JSIdentifier _ "exports" <- exports
335+ , Just name <- fromStringLiteral nm
336+ = Just name
337+ accessor _ = Nothing
338+
339+-- Matches assignments to module.exports, like this:
340+-- module.exports = { ... }
341+matchExportsAssignment :: JSStatement -> Maybe JSObjectPropertyList
342+matchExportsAssignment stmt
343+ | JSAssignStatement e (JSAssign _) decl _ <- stmt
344+ , JSMemberDot module' _ exports <- e
345+ , JSIdentifier _ "module" <- module'
346+ , JSIdentifier _ "exports" <- exports
347+ , JSObjectLiteral _ props _ <- decl
348+ = Just props
349+ | otherwise
350+ = Nothing
351+
352+extractLabel :: JSPropertyName -> Maybe String
353+extractLabel (JSPropertyString _ nm) = Just (trimStringQuotes nm)
354+extractLabel (JSPropertyIdent _ nm) = Just nm
355+extractLabel _ = Nothing
356+
298357-- | Eliminate unused code based on the specified entry point set.
299358compile :: [Module] -> [ModuleIdentifier] -> [Module]
300359compile modules [] = modules
src/Language/PureScript/Errors.hs+20−5
@@ -12,6 +12,7 @@ import Data.Char (isSpace)
1212import Data.Either (lefts, rights)
1313import Data.List (intercalate, transpose, nub, nubBy, sortBy)
1414import Data.Foldable (fold)
15+import Data.Maybe (maybeToList)
1516
1617import qualified Data.Map as M
1718
@@ -28,6 +29,7 @@ import Language.PureScript.Pretty.Common (before)
2829import Language.PureScript.Types
2930import Language.PureScript.Names
3031import Language.PureScript.Kinds
32+import qualified Language.PureScript.Bundle as Bundle
3133
3234import qualified Text.PrettyPrint.Boxes as Box
3335
@@ -37,11 +39,13 @@ import Text.Parsec.Error (Message(..))
3739
3840-- | A type of error messages
3941data SimpleErrorMessage
40− = ErrorParsingFFIModule FilePath
42+ = ErrorParsingFFIModule FilePath (Maybe Bundle.ErrorMessage)
4143 | ErrorParsingModule P.ParseError
4244 | MissingFFIModule ModuleName
4345 | MultipleFFIModules ModuleName [FilePath]
4446 | UnnecessaryFFIModule ModuleName FilePath
47+ | MissingFFIImplementations ModuleName [Ident]
48+ | UnusedFFIImplementations ModuleName [Ident]
4549 | CannotGetFileInfo FilePath
4650 | CannotReadFile FilePath
4751 | CannotWriteFile FilePath
@@ -224,6 +228,8 @@ errorCode em = case unwrapErrorMessage em of
224228 MissingFFIModule{} -> "MissingFFIModule"
225229 MultipleFFIModules{} -> "MultipleFFIModules"
226230 UnnecessaryFFIModule{} -> "UnnecessaryFFIModule"
231+ MissingFFIImplementations{} -> "MissingFFIImplementations"
232+ UnusedFFIImplementations{} -> "UnusedFFIImplementations"
227233 CannotGetFileInfo{} -> "CannotGetFileInfo"
228234 CannotReadFile{} -> "CannotReadFile"
229235 CannotWriteFile{} -> "CannotWriteFile"
@@ -525,10 +531,11 @@ prettyPrintSingleError full level showWiki e = flip evalState defaultUnknownMap
525531 paras [ line "Unable to write file: "
526532 , indent . line $ path
527533 ]
528− renderSimpleErrorMessage (ErrorParsingFFIModule path) =
529− paras [ line "Unable to parse foreign module:"
530− , indent . line $ path
531− ]
534+ renderSimpleErrorMessage (ErrorParsingFFIModule path extra) =
535+ paras $ [ line "Unable to parse foreign module:"
536+ , indent . line $ path
537+ ] ++
538+ (map (indent . line) (concatMap Bundle.printErrorMessage (maybeToList extra)))
532539 renderSimpleErrorMessage (ErrorParsingModule err) =
533540 paras [ line "Unable to parse module: "
534541 , prettyPrintParseError err
@@ -540,6 +547,14 @@ prettyPrintSingleError full level showWiki e = flip evalState defaultUnknownMap
540547 , indent . line $ path
541548 , line $ "Module " ++ runModuleName mn ++ " does not contain any foreign import declarations, so a foreign module is not necessary."
542549 ]
550+ renderSimpleErrorMessage (MissingFFIImplementations mn idents) =
551+ paras [ line $ "The following values are not defined in the foreign module for module " ++ runModuleName mn ++ ": "
552+ , indent . paras $ map (line . runIdent) idents
553+ ]
554+ renderSimpleErrorMessage (UnusedFFIImplementations mn idents) =
555+ paras [ line $ "The following definitions in the foreign module for module " ++ runModuleName mn ++ " are unused: "
556+ , indent . paras $ map (line . runIdent) idents
557+ ]
543558 renderSimpleErrorMessage (MultipleFFIModules mn paths) =
544559 paras [ line $ "Multiple foreign module implementations have been provided for module " ++ runModuleName mn ++ ": "
545560 , indent . paras $ map line paths
src/Language/PureScript/Make.hs+57−4
@@ -25,6 +25,7 @@ module Language.PureScript.Make
2525import Prelude ()
2626import Prelude.Compat
2727
28+import Control.Applicative ((<|>))
2829import Control.Monad hiding (sequence)
2930import Control.Monad.Error.Class (MonadError(..))
3031import Control.Monad.Writer.Class (MonadWriter(..))
@@ -52,6 +53,8 @@ import qualified Data.ByteString.UTF8 as BU8
5253import qualified Data.Set as S
5354import qualified Data.Map as M
5455
56+import qualified Text.Parsec as Parsec
57+
5558import SourceMap.Types
5659import SourceMap
5760
@@ -61,6 +64,8 @@ import System.FilePath ((</>), takeDirectory, makeRelative, splitPath, normalise
6164import System.IO.Error (tryIOError)
6265import System.IO.UTF8 (readUTF8File, writeUTF8File)
6366
67+import qualified Language.JavaScript.Parser as JS
68+
6469import Language.PureScript.Crash
6570import Language.PureScript.AST
6671import Language.PureScript.Externs
@@ -76,6 +81,8 @@ import Language.PureScript.Renamer
7681import Language.PureScript.Sugar
7782import Language.PureScript.TypeChecker
7883import qualified Language.PureScript.Constants as C
84+import qualified Language.PureScript.Bundle as Bundle
85+import qualified Language.PureScript.Parser as PSParser
7986
8087import qualified Language.PureScript.CodeGen.JS as J
8188import qualified Language.PureScript.CoreFn as CF
@@ -331,7 +338,9 @@ buildMakeActions outputDir filePathMap foreigns usePrefix =
331338 | not $ requiresForeign m -> do
332339 tell $ errorMessage $ UnnecessaryFFIModule mn path
333340 return Nothing
334− | otherwise -> return $ Just $ J.JSApp Nothing (J.JSVar Nothing "require") [J.JSStringLiteral Nothing "./foreign"]
341+ | otherwise -> do
342+ checkForeignDecls m path
343+ return $ Just $ J.JSApp Nothing (J.JSVar Nothing "require") [J.JSStringLiteral Nothing "./foreign"]
335344 Nothing | requiresForeign m -> throwError . errorMessage $ MissingFFIModule mn
336345 | otherwise -> return Nothing
337346 rawJs <- J.moduleToJs m foreignInclude
@@ -384,9 +393,6 @@ buildMakeActions outputDir filePathMap foreigns usePrefix =
384393 exists <- doesFileExist path
385394 traverse (const $ getModificationTime path) $ guard exists
386395
387− readTextFile :: FilePath -> Make String
388− readTextFile path = makeIO (const (ErrorMessage [] $ CannotReadFile path)) $ readUTF8File path
389−
390396 writeTextFile :: FilePath -> String -> Make ()
391397 writeTextFile path text = makeIO (const (ErrorMessage [] $ CannotWriteFile path)) $ do
392398 mkdirp path
@@ -397,3 +403,50 @@ buildMakeActions outputDir filePathMap foreigns usePrefix =
397403
398404 progress :: ProgressMessage -> Make ()
399405 progress = liftIO . putStrLn . renderProgressMessage
406+
407+readTextFile :: FilePath -> Make String
408+readTextFile path = makeIO (const (ErrorMessage [] $ CannotReadFile path)) $ readUTF8File path
409+
410+-- |
411+-- Check that the declarations in a given PureScript module match with those
412+-- in its corresponding foreign module.
413+--
414+checkForeignDecls :: CF.Module ann -> FilePath -> SupplyT Make ()
415+checkForeignDecls m path = do
416+ jsStr <- lift $ readTextFile path
417+ js <- either (errorParsingModule . Bundle.UnableToParseModule) pure $ JS.parse jsStr path
418+
419+ foreignIdentsStrs <- either errorParsingModule pure $ getExps js
420+ let foreignIdents =
421+ either
422+ (internalError . ("checkForeignDecls: unexpected idents: " ++) . show)
423+ S.fromList
424+ (traverse parseIdent foreignIdentsStrs)
425+ let importedIdents = S.fromList $ map fst (CF.moduleForeign m)
426+
427+ let unusedFFI = foreignIdents S.\\ importedIdents
428+ unless (null unusedFFI) $
429+ tell . errorMessage . UnusedFFIImplementations mname $
430+ S.toList unusedFFI
431+
432+ let missingFFI = importedIdents S.\\ foreignIdents
433+ unless (null missingFFI) $
434+ throwError . errorMessage . MissingFFIImplementations mname $
435+ S.toList missingFFI
436+
437+ where
438+ mname = CF.moduleName m
439+
440+ errorParsingModule :: Bundle.ErrorMessage -> SupplyT Make a
441+ errorParsingModule = throwError . errorMessage . ErrorParsingFFIModule path . Just
442+
443+ getExps :: JS.JSAST -> Either Bundle.ErrorMessage [String]
444+ getExps = Bundle.getExportedIdentifiers (runModuleName mname)
445+
446+ -- TODO: Handling for parenthesised operators should be removed after 0.9.
447+ parseIdent :: String -> Either String Ident
448+ parseIdent str = try str <|> try ("(" ++ str ++ ")")
449+ where
450+ try s = either (Left . show) Right $ do
451+ ts <- PSParser.lex "" s
452+ PSParser.runTokenParser "" (PSParser.parseIdent <* Parsec.eof) ts
src/Language/PureScript/Parser/JS.hs+1−1
@@ -43,7 +43,7 @@ parseForeignModulesFromFiles files = do
4343 foreigns <- parU files $ \(path, file) ->
4444 case findModuleName (lines file) of
4545 Just name -> return (name, path)
46− Nothing -> throwError (errorMessage $ ErrorParsingFFIModule path)
46+ Nothing -> throwError (errorMessage $ ErrorParsingFFIModule path Nothing)
4747 let grouped = groupBy ((==) `on` fst) $ sortBy (compare `on` fst) foreigns
4848 forM_ grouped $ \grp ->
4949 when (length grp > 1) $ do