diff --git a/src/Ccap/Codegen/Imports.purs b/src/Ccap/Codegen/Imports.purs index 3b4a3bd..c85f4cc 100644 --- a/src/Ccap/Codegen/Imports.purs +++ b/src/Ccap/Codegen/Imports.purs @@ -3,9 +3,12 @@ module Ccap.Codegen.Imports , Imported , Includes , importInScope + , importPath , importsInScope , importsInScopes , parseImports + , possibleImportPaths + , resolveImport , validateImports ) where @@ -32,7 +35,7 @@ type FoundImport = { imprt :: Import, filePath :: FilePath } type Imported - = { imprt :: Import, mod :: Module } + = Source { imprt :: Import, mod :: Module } data ImportError = NotFound Module Import @@ -42,18 +45,26 @@ data ImportError instance importValidationError :: ValidationError ImportError where printError = case _ of NotFound mod imprt -> - mod.name <> " tried to import module: " <> imprt + mod.name + <> " tried to import module: " + <> imprt <> " but it does not exist, or was not included." - ParseError imprt message -> "Parsing imported module, " <> imprt <> ", failed with error: " <> message + ParseError imprt message -> + "Parsing imported module, " + <> imprt + <> ", failed with error: " + <> message + +importPath :: Import -> FilePath +importPath = String.replaceAll (Pattern ".") (Replacement Path.sep) + +resolveImport :: FilePath -> Import -> FilePath +resolveImport filePath = FS.joinPaths filePath <<< flip append ".tmpl" <<< importPath possibleImportPaths :: Includes -> FilePath -> Import -> Array FilePath possibleImportPaths includes source imprt = - let - sourceDirs = Path.dirname source : includes - - importPath = String.replaceAll (Pattern ".") (Replacement Path.sep) imprt - in - sourceDirs <#> flip FS.joinPaths importPath <#> flip append ".tmpl" + (Path.dirname source : includes) + <#> (flip resolveImport $ importPath imprt) importInScope :: Includes -> Source Module -> Import -> Effect (Either ImportError FoundImport) importInScope included { source, contents } imprt = @@ -90,7 +101,10 @@ importsInScopes includes sources = parseImports :: Array FoundImport -> Effect (Either (Array ImportError) (Array Imported)) parseImports imports = traverse parse imports <#> toValidation where - parse { imprt, filePath } = bimap (ParseError imprt) (\source -> { imprt, mod: source.contents }) <$> FS.sourceFile filePath + parse { imprt, filePath } = + bimap (ParseError imprt) + (\source -> { source: filePath, contents: { imprt, mod: source.contents } }) + <$> FS.sourceFile filePath -- | Validate that the imports of the given modules exist and parse the imported modules -- | Note: Does not validate the contents of the imported files. diff --git a/src/Ccap/Codegen/Module.purs b/src/Ccap/Codegen/Module.purs index 88e21e7..d8af825 100644 --- a/src/Ccap/Codegen/Module.purs +++ b/src/Ccap/Codegen/Module.purs @@ -4,13 +4,15 @@ module Ccap.Codegen.Module ) where import Prelude -import Ccap.Codegen.Imports (Imported, Includes, validateImports) +import Ccap.Codegen.Imports (Imported, Includes, possibleImportPaths, validateImports) import Ccap.Codegen.TypeRef (validateAllTypeRefs) import Ccap.Codegen.Types (Module, Source, ValidatedModule) -import Ccap.Codegen.ValidationError (class ValidationError, printError) +import Ccap.Codegen.ValidationError (printError) import Control.Monad.Except (ExceptT(..), except, runExceptT, withExceptT) -import Control.MonadPlus (guard) +import Data.Array as Array +import Data.Bifunctor (lmap) import Data.Either (Either) +import Data.Foldable (any) import Data.Traversable (for) import Effect (Effect) @@ -21,27 +23,36 @@ validateModules :: Effect (Either (Array String) (Array (Source ValidatedModule))) validateModules includes sources = runExceptT do - allImports <- withErrors $ ExceptT $ validateImports includes sources - withErrors $ except - $ for sources \source -> do - let - mod = source.contents - - imports = importsForModule mod allImports - _ <- validateAllTypeRefs mod imports - pure $ source { contents = mod { imports = imports } } - -withErrors :: - forall f e a. - Functor f => - ValidationError e => - ExceptT (Array e) f a -> - ExceptT (Array String) f a -withErrors = withExceptT $ map printError - -importsForModule :: Module -> Array Imported -> Array Module -importsForModule mod imports = do - imported <- imports - imprt <- mod.imports - guard $ imprt == imported.imprt - pure $ imported.mod + allImports <- withExceptT (map printError) $ ExceptT $ validateImports includes sources + except $ for sources $ validateModule includes allImports + +validateModule :: + Includes -> + Array Imported -> + Source Module -> + Either (Array String) (Source ValidatedModule) +validateModule includes allImports source = + let + imports = importsForModule includes source allImports + + validatedSource = source { contents = source.contents { imports = imports } } + + validations = lmap (map printError) $ validateAllTypeRefs source.contents imports + in + validations *> pure validatedSource + +importsForModule :: Includes -> Source Module -> Array Imported -> Array Module +importsForModule includes source = + map _.contents.mod + <<< Array.filter (isImportedBy includes source) + +-- This was already done by validateImports, we should adjust the return type of that so we don't +-- have to do this again. +isImportedBy :: Includes -> Source Module -> Imported -> Boolean +isImportedBy includes source imported = + let + { source: modulePath, contents: { imports } } = source + + { source: importPath, contents: { imprt, mod: { name } } } = imported + in -- TODO: this is almost identical to importInScope + any (eq importPath) $ possibleImportPaths includes modulePath =<< imports diff --git a/test/Ccap/Codegen/Imports.purs b/test/Ccap/Codegen/Imports.purs index 2e0ab15..cf8bf2c 100644 --- a/test/Ccap/Codegen/Imports.purs +++ b/test/Ccap/Codegen/Imports.purs @@ -5,11 +5,13 @@ module Test.Ccap.Codegen.Imports import Prelude import Ccap.Codegen.FileSystem as FS import Ccap.Codegen.Imports (importsInScope, validateImports) +import Ccap.Codegen.Module (validateModules) import Ccap.Codegen.TypeRef (validateAllTypeRefs) import Ccap.Codegen.ValidationError (class ValidationError, printError) import Control.Monad.Except (ExceptT(..), except, runExceptT, withExceptT) import Data.Either (either) import Data.Foldable (fold) +import Data.Traversable (intercalate, sequence, traverse) import Effect.Class (liftEffect) import Node.Path (FilePath) import Node.Path as Path @@ -17,14 +19,20 @@ import Test.Ccap.Codegen.Util (eqElems, shouldBeLeft, shouldBeRight) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldSatisfy) -root :: FilePath -root = "./test/resources/includes/" +resources :: FilePath +resources = "./test/resources/" + +imports_ :: FilePath +imports_ = Path.concat [ resources, "imports" ] + +includes_ :: FilePath +includes_ = Path.concat [ resources, "includes" ] internal_ :: FilePath -internal_ = Path.concat [ root, "internal" ] +internal_ = Path.concat [ includes_, "internal" ] external_ :: FilePath -external_ = Path.concat [ root, "external" ] +external_ = Path.concat [ includes_, "external" ] internal :: FilePath -> FilePath internal fileName = Path.concat [ internal_, fileName ] @@ -44,6 +52,12 @@ externalSource = internal "SourceExternal.tmpl" externalSubmoduleSource :: FilePath externalSubmoduleSource = internal "SourceExternalSubmodule.tmpl" +app1 :: FilePath +app1 = Path.concat [ imports_, "app1", "Main.tmpl" ] + +app2 :: FilePath +app2 = Path.concat [ imports_, "app2", "Main.tmpl" ] + withPrintErrors ∷ ∀ e m a. ValidationError e ⇒ Monad m ⇒ ExceptT (Array e) m a → ExceptT String m a withPrintErrors = withExceptT $ fold <<< map printError @@ -77,6 +91,15 @@ specs = withPrintErrors $ ExceptT $ validateImports includes [ source ] shouldBeRight imports + itCanValidateAllModules filePaths includes = + it "Can validate all modules" do + modules <- + liftEffect + $ runExceptT do + sources <- ExceptT $ map sequence $ traverse FS.sourceFile filePaths + withExceptT (intercalate "|") $ ExceptT $ validateModules includes sources + shouldBeRight modules + itFailsWithoutIncludes filePath = it "Fails validation without including the external folder" do let @@ -95,7 +118,7 @@ specs = $ runExceptT do source <- ExceptT $ FS.sourceFile filePath imports <- withPrintErrors $ ExceptT $ validateImports includes [ source ] - withPrintErrors $ except $ validateAllTypeRefs source.contents (imports <#> _.mod) + withPrintErrors $ except $ validateAllTypeRefs source.contents (imports <#> _.contents.mod) shouldBeRight typeDecls in describe "template include syntax" do @@ -115,6 +138,7 @@ specs = itCanFindImports submoduleSource [] itCanValidateImports submoduleSource [] itHasValidTypeReferences submoduleSource [] + itCanValidateAllModules [ submoduleSource ] [] describe "a file with an external reference" do itCanBeParsed externalSource itHasImports externalSource [ "External" ] @@ -122,6 +146,7 @@ specs = itCanValidateImports externalSource [ external_ ] itFailsWithoutIncludes externalSource itHasValidTypeReferences externalSource [ external_ ] + itCanValidateAllModules [ externalSource ] [ external_ ] describe "a file with an external reference to a submodule" do itCanBeParsed externalSubmoduleSource itHasImports externalSubmoduleSource [ "submodule.ExternalSubmodule" ] @@ -129,3 +154,6 @@ specs = itCanValidateImports externalSubmoduleSource [ external_ ] itFailsWithoutIncludes externalSubmoduleSource itHasValidTypeReferences externalSubmoduleSource [ external_ ] + itCanValidateAllModules [ externalSubmoduleSource ] [ external_ ] + describe "two files with identical relative imports" + $ itCanValidateAllModules [ app1, app2 ] [] diff --git a/test/resources/imports/app1/Main.tmpl b/test/resources/imports/app1/Main.tmpl new file mode 100644 index 0000000..e0099c0 --- /dev/null +++ b/test/resources/imports/app1/Main.tmpl @@ -0,0 +1,6 @@ +scala: test.App1 +purs: Test.App1 + +import Types + +type App: Types.Foo diff --git a/test/resources/imports/app1/Types.tmpl b/test/resources/imports/app1/Types.tmpl new file mode 100644 index 0000000..5b7e702 --- /dev/null +++ b/test/resources/imports/app1/Types.tmpl @@ -0,0 +1,4 @@ +scala: test.Lib1 +purs: Test.Lib1 + +type Foo: Int diff --git a/test/resources/imports/app2/Main.tmpl b/test/resources/imports/app2/Main.tmpl new file mode 100644 index 0000000..8d153ac --- /dev/null +++ b/test/resources/imports/app2/Main.tmpl @@ -0,0 +1,6 @@ +scala: test.App2 +purs: Test.App2 + +import Types + +type App: Types.Bar diff --git a/test/resources/imports/app2/Types.tmpl b/test/resources/imports/app2/Types.tmpl new file mode 100644 index 0000000..bef2130 --- /dev/null +++ b/test/resources/imports/app2/Types.tmpl @@ -0,0 +1,4 @@ +scala: test.Lib2 +purs: Test.Lib2 + +type Bar: Int