packages feed

aihc-cabal-syntax-1.0.0.1: test/Compliance/Adapter.hs

{-# LANGUAGE OverloadedStrings #-}
module Compliance.Adapter (toCabal, runResult) where

import Control.Monad (foldM)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Aihc.Cabal as A
import qualified Distribution.CabalSpecVersion as C
import qualified Distribution.Compat.NonEmptySet as NES
import qualified Distribution.Compiler as C
import qualified Distribution.FieldGrammar.Parsec as C
import qualified Distribution.Fields.Field as C
import qualified Distribution.Fields.ParseResult as C
import qualified Distribution.PackageDescription as C
import qualified Distribution.PackageDescription.FieldGrammar as C
import qualified Distribution.Parsec as C
import qualified Distribution.Types.Version as C
import qualified Distribution.Types.VersionRange as C
import qualified Distribution.Utils.Path as C

-- Only fields retained as text use Cabal field grammars. Typed fields use
-- the project AST. This module cannot read the original file or reference AST.
toCabal :: A.Package -> Either String C.GenericPackageDescription
toCabal pkg = do
  spec <- maybe (Left "Unsupported Cabal format version") Right
    (C.cabalSpecFromVersionDigits (map fromInteger (NE.toList (A.versionNumbers (A.cabalVersion pkg)))))
  -- The typed name, version, and format version replace the retained text.
  let retained = foldr Map.delete (A.packageFields pkg) ["name", "version", "cabal-version"]
  pd <- fields spec C.packageDescriptionFieldGrammar
    (Map.insert "name" [text (A.packageName pkg)] (Map.insert "version" [text "0"] retained))
  repositories <- traverse (sourceRepository spec) (A.packageSourceRepositories pkg)
  flags <- traverse (flag spec) (A.packageFlags pkg)
  version <- convertVersion (A.packageVersion pkg)
  -- The grammar tells if the file sets a build type. The value is typed.
  buildType <- traverse (const (atom (A.buildType pkg))) (C.buildTypeRaw pd)
  setup <- traverse (fmap (\deps -> C.SetupBuildInfo deps False) . traverse convertDependency)
    (A.packageSetupDependencies pkg)
  let description = pd
        { C.package = C.PackageIdentifier (C.mkPackageName (T.unpack (A.packageName pkg))) version
        , C.specVersion = spec
        , C.buildTypeRaw = buildType
        , C.setupBuildInfo = setup
        , C.sourceRepos = repositories
        }
      initial = C.emptyGenericPackageDescription
        { C.packageDescription = description, C.genPackageFlags = flags }
  foldM (component spec) initial (A.packageComponents pkg)

flag :: C.CabalSpecVersion -> A.Flag -> Either String C.PackageFlag
flag spec f = do
  raw <- fields spec (C.flagFieldGrammar (C.mkFlagName (T.unpack (A.flagName f))))
    (Map.singleton "description" [text (A.flagDescription f)])
  pure raw { C.flagDefault = A.flagDefault f, C.flagManual = A.flagManual f }

sourceRepository :: C.CabalSpecVersion -> A.SourceRepository -> Either String C.SourceRepo
sourceRepository spec repository = do
  kind <- atom (A.sourceRepositoryKind repository)
  fields spec (C.sourceRepoFieldGrammar kind) (A.sourceRepositoryFields repository)

fields :: C.CabalSpecVersion -> C.ParsecFieldGrammar s a -> Map.Map Text [A.FieldValue] -> Either String a
fields spec grammar retained = either (Left . show) Right
  (snd (runResult (C.parseFieldGrammar spec (fieldMap retained) grammar)))
  where
    -- Keep each occurrence, each line, and each source position.
    fieldMap = Map.fromList . map (\(k, vs) -> (TE.encodeUtf8 k, map value vs)) . Map.toList
    value (A.FieldValue p ls) = C.MkNamelessField (position p)
      [C.FieldLine (position q) (TE.encodeUtf8 line) | A.FieldLine q line <- ls]
    position (A.Position row column) = C.Position row column

-- | Run a Cabal-syntax parser without a source name. The results keep the
-- error and warning format of Cabal-syntax 3.12.
runResult :: C.ParseResult () a
  -> ([C.PWarning], Either (Maybe C.Version, NonEmpty C.PError) a)
runResult result = case C.runParseResult result of
  (warnings, outcome) -> (map C.pwarning warnings, either (Left . fmap (fmap C.perror)) Right outcome)

-- | A field value for text that has no source position. Each line gets a
-- new row, so that Cabal free text rules give the same text.
text :: Text -> A.FieldValue
text value = A.FieldValue (A.Position 0 0)
  [A.FieldLine (A.Position row 1) line | not (T.null value), (row, line) <- zip [1 ..] (T.splitOn "\n" value)]

-- | Cabal-syntax makes these paths without checks or normalization.
path :: FilePath -> C.SymbolicPathX allowAbsolute from to
path = C.unsafeMakeSymbolicPath

atom :: C.Parsec a => Text -> Either String a
atom input = maybe (Left ("Cannot convert field value: " ++ T.unpack input)) Right (C.simpleParsec (T.unpack input))

convertVersion :: A.Version -> Either String C.Version
convertVersion v = do
  let digits = NE.toList (A.versionNumbers v)
  if any (> toInteger (maxBound :: Int)) digits
    then Left "Version component exceeds the Cabal integer limit"
    else Right (C.mkVersion (map fromInteger digits))

convertRange :: A.VersionRange -> Either String C.VersionRange
convertRange range = case range of
  A.AnyVersion -> Right C.anyVersion
  A.Equal v -> C.thisVersion <$> convertVersion v
  A.Later v -> C.laterVersion <$> convertVersion v
  A.Earlier v -> C.earlierVersion <$> convertVersion v
  A.AtLeast v -> C.orLaterVersion <$> convertVersion v
  A.AtMost v -> C.orEarlierVersion <$> convertVersion v
  A.MajorBound v -> C.majorBoundVersion <$> convertVersion v
  A.Both a b -> C.intersectVersionRanges <$> convertRange a <*> convertRange b
  A.EitherRange a b -> C.unionVersionRanges <$> convertRange a <*> convertRange b

convertDependency :: A.Dependency -> Either String C.Dependency
convertDependency dep = do
  range <- convertRange (A.dependencyRange dep)
  pure (C.Dependency (C.mkPackageName (T.unpack (A.dependencyPackage dep))) range
    (NES.fromNonEmpty (fmap target (A.dependencyLibraries dep))))
  where
    target A.MainLibrary = C.LMainLibName
    target (A.NamedLibrary name) = C.LSubLibName (C.mkUnqualComponentName (T.unpack name))

convertMixin :: A.Mixin -> Either String C.Mixin
convertMixin (A.Mixin pkg lib provides requires) = do
  provides' <- renaming provides
  requires' <- renaming requires
  pure (C.Mixin (C.mkPackageName (T.unpack pkg)) (library lib) (C.IncludeRenaming provides' requires'))
  where
    library A.MainLibrary = C.LMainLibName
    library (A.NamedLibrary name) = C.LSubLibName (C.mkUnqualComponentName (T.unpack name))
    renaming A.DefaultRenaming = Right C.DefaultRenaming
    renaming (A.ModuleRenaming pairs) = C.ModuleRenaming <$> traverse (\(a, b) -> (,) <$> atom a <*> atom b) pairs
    renaming (A.HidingRenaming names) = C.HidingRenaming <$> traverse atom names

convertBuildInfo :: C.BuildInfo -> A.BuildInfo -> Either String C.BuildInfo
convertBuildInfo raw bi = do
  other <- traverse atom (A.otherModules bi)
  autogen <- traverse atom (A.autogenModules bi)
  virtual <- traverse atom (A.virtualModules bi)
  language <- traverse atom (A.defaultLanguage bi)
  otherLanguages <- traverse atom (A.otherLanguages bi)
  extensions <- traverse atom (A.extensions bi)
  otherExtensions <- traverse atom (A.otherExtensions bi)
  legacyExtensions <- traverse atom (A.legacyExtensions bi)
  dependencies <- traverse convertDependency (A.dependencies bi)
  mixins <- traverse convertMixin (A.mixins bi)
  modern <- traverse modernTool [t | t <- A.buildTools bi, Just _ <- [A.toolPackage t]]
  legacy <- traverse legacyTool [t | t <- A.buildTools bi, Nothing <- [A.toolPackage t]]
  let C.PerCompilerFlavor _ ghcjs = C.options raw
  pure raw
    { C.buildable = fromMaybe True (A.buildable bi)
    , C.hsSourceDirs = map path (A.sourceDirs bi)
    , C.otherModules = other
    , C.autogenModules = autogen
    , C.virtualModules = virtual
    , C.defaultLanguage = language
    , C.otherLanguages = otherLanguages
    , C.defaultExtensions = extensions
    , C.otherExtensions = otherExtensions
    , C.oldExtensions = legacyExtensions
    , C.targetBuildDepends = dependencies
    , C.mixins = mixins
    , C.buildTools = legacy
    , C.buildToolDepends = modern
    , C.cSources = map path (A.cSources bi)
    , C.cxxSources = map path (A.cxxSources bi)
    , C.asmSources = map path (A.asmSources bi)
    , C.cmmSources = map path (A.cmmSources bi)
    , C.jsSources = map path (A.jsSources bi)
    , C.includeDirs = map path (A.includeDirs bi)
    , C.includes = map path (A.includes bi)
    , C.installIncludes = map path (A.installIncludes bi)
    , C.autogenIncludes = map path (A.autogenIncludes bi)
    , C.extraLibDirs = map path (A.extraLibDirs bi)
    , C.extraLibDirsStatic = map path (A.extraLibDirsStatic bi)
    , C.frameworks = map (path . T.unpack) (A.frameworks bi)
    , C.extraFrameworkDirs = map path (A.extraFrameworkDirs bi)
    , C.cppOptions = map T.unpack (A.cppOptions bi)
    , C.ccOptions = map T.unpack (A.ccOptions bi)
    , C.cxxOptions = map T.unpack (A.cxxOptions bi)
    , C.options = C.PerCompilerFlavor (map T.unpack (A.ghcOptions bi)) ghcjs
    }
  where
    modernTool tool = case A.toolPackage tool of
      Nothing -> Left "Missing build tool package"
      Just name -> C.ExeDependency (C.mkPackageName (T.unpack name))
        (C.mkUnqualComponentName (T.unpack (A.toolName tool))) <$> convertRange (A.toolRange tool)
    legacyTool tool = C.LegacyExeDependency (T.unpack (A.toolName tool)) <$> convertRange (A.toolRange tool)

convertCondition :: A.Condition -> Either String (C.Condition C.ConfVar)
convertCondition cond = case cond of
  A.Literal b -> Right (C.Lit b)
  A.OS name -> C.Var . C.OS <$> atom name
  A.Arch name -> C.Var . C.Arch <$> atom name
  A.Impl name range -> C.Var <$> (C.Impl <$> atom name <*> convertRange range)
  A.FlagValue name -> Right (C.Var (C.PackageFlag (C.mkFlagName (T.unpack name))))
  A.Not a -> C.CNot <$> convertCondition a
  A.And a b -> C.CAnd <$> convertCondition a <*> convertCondition b
  A.Or a b -> C.COr <$> convertCondition a <*> convertCondition b

convertTree :: (A.BuildInfo -> Either String a)
  -> A.Conditional A.BuildInfo -> Either String (C.CondTree C.ConfVar a)
convertTree convert (A.Conditional bi branches) = do
  value <- convert bi
  children <- traverse branch branches
  pure (C.CondNode value children)
  where
    branch (A.Branch cond yes no) = C.CondBranch <$> convertCondition cond
      <*> convertTree convert yes <*> traverse (convertTree convert) no

component :: C.CabalSpecVersion -> C.GenericPackageDescription
  -> A.Component (A.Conditional A.BuildInfo) -> Either String C.GenericPackageDescription
component spec gpd (A.Component kind tree) = case kind of
  A.Library target -> do
    let libName = case target of
          A.MainLibrary -> C.LMainLibName
          A.NamedLibrary n -> C.LSubLibName (componentName n)
    converted <- convertTree (library libName) tree
    pure $ case target of
      A.MainLibrary -> gpd { C.condLibrary = Just converted }
      A.NamedLibrary n -> gpd { C.condSubLibraries = C.condSubLibraries gpd ++ [(componentName n, converted)] }
  A.Executable name -> do
    converted <- convertTree (executable (componentName name)) tree
    pure gpd { C.condExecutables = C.condExecutables gpd ++ [(componentName name, converted)] }
  A.ForeignLibrary name -> do
    converted <- convertTree (foreignLibrary (componentName name)) tree
    pure gpd { C.condForeignLibs = C.condForeignLibs gpd ++ [(componentName name, converted)] }
  A.TestSuite name -> do
    converted <- convertTree testSuite tree
    pure gpd { C.condTestSuites = C.condTestSuites gpd ++ [(componentName name, converted)] }
  A.Benchmark name -> do
    converted <- convertTree benchmark tree
    pure gpd { C.condBenchmarks = C.condBenchmarks gpd ++ [(componentName name, converted)] }
  where
    componentName = C.mkUnqualComponentName . T.unpack
    library name bi = do
      raw <- fields spec (C.libraryFieldGrammar name) (A.extraFields bi)
      info <- convertBuildInfo (C.libBuildInfo raw) bi
      exposed <- traverse atom (A.exposedModules bi)
      pure raw { C.libBuildInfo = info, C.exposedModules = exposed }
    executable name bi = do
      raw <- fields spec (C.executableFieldGrammar name) (A.extraFields bi)
      info <- convertBuildInfo (C.buildInfo raw) bi
      pure raw { C.buildInfo = info, C.modulePath = path (fromMaybe "" (A.mainIs bi)) }
    foreignLibrary name bi = do
      raw <- fields spec (C.foreignLibFieldGrammar name) (A.extraFields bi)
      info <- convertBuildInfo (C.foreignLibBuildInfo raw) bi
      pure raw { C.foreignLibBuildInfo = info }
    testSuite bi = do
      raw <- fields spec C.testSuiteFieldGrammar (A.extraFields bi)
      info <- convertBuildInfo (C._testStanzaBuildInfo raw) bi
      result (C.validateTestSuite spec C.zeroPos raw
        { C._testStanzaBuildInfo = info, C._testStanzaMainIs = path <$> A.mainIs bi })
    benchmark bi = do
      raw <- fields spec C.benchmarkFieldGrammar (A.extraFields bi)
      info <- convertBuildInfo (C._benchmarkStanzaBuildInfo raw) bi
      result (C.validateBenchmark spec C.zeroPos raw
        { C._benchmarkStanzaBuildInfo = info, C._benchmarkStanzaMainIs = path <$> A.mainIs bi })
    result = either (Left . show) Right . snd . runResult