Skip to content

Commit

Permalink
Introduce FIeldGrammar.
Browse files Browse the repository at this point in the history
This commit reworks how GenericPackageDescription is parsed from
`[Field Position]` and pretty-printed to `Doc`.

This also fixes few issues:
- Fix #4697: `cabal format` doesn't output custom-setup stanza (nor
  foreign-lib stanzas)
- Fix #4719: `parse . pretty . parse = parse`  for all Hackage cabal files.
    - `parser-hackage-tests roundtrip` is the test program.

The handling of `license-file` and `license-files` is changed.
Now they behave the same.
  • Loading branch information
phadej committed Sep 4, 2017
1 parent 6083225 commit 690a725
Show file tree
Hide file tree
Showing 80 changed files with 3,614 additions and 2,019 deletions.
22 changes: 15 additions & 7 deletions Cabal/Cabal.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ extra-source-files:
-- Do NOT edit this section manually; instead, run the script.
-- BEGIN gen-extra-source-files
tests/ParserTests/regressions/Octree-0.5.cabal
tests/ParserTests/regressions/elif.cabal
tests/ParserTests/regressions/encoding-0.8.cabal
tests/ParserTests/regressions/generics-sop.cabal
tests/ParserTests/regressions/issue-774.cabal
tests/ParserTests/regressions/nothing-unicode.cabal
tests/ParserTests/regressions/shake.cabal
tests/ParserTests/warnings/bom.cabal
tests/ParserTests/warnings/bool.cabal
tests/ParserTests/warnings/deprecatedfield.cabal
Expand Down Expand Up @@ -274,18 +277,22 @@ library
parsec >= 3.1.9 && <3.2
exposed-modules:
Distribution.Compat.Parsec
Distribution.FieldGrammar
Distribution.FieldGrammar.Class
Distribution.FieldGrammar.Parsec
Distribution.FieldGrammar.Pretty
Distribution.PackageDescription.FieldGrammar
Distribution.PackageDescription.Parsec
Distribution.PackageDescription.Parsec.FieldDescr
Distribution.PackageDescription.Parsec.Quirks
Distribution.PackageDescription.Quirks
Distribution.Parsec.Class
Distribution.Parsec.Common
Distribution.Parsec.ConfVar
Distribution.Parsec.Field
Distribution.Parsec.Lexer
Distribution.Parsec.LexerMonad
Distribution.Parsec.Newtypes
Distribution.Parsec.ParseResult
Distribution.Parsec.Parser
Distribution.Parsec.Types.Common
Distribution.Parsec.Types.Field
Distribution.Parsec.Types.FieldDescr
Distribution.Parsec.Types.ParseResult

-- Lens functionality
exposed-modules:
Expand Down Expand Up @@ -400,6 +407,7 @@ test-suite parser-tests
type: exitcode-stdio-1.0
hs-source-dirs: tests
main-is: ParserTests.hs
build-depends: containers
build-depends:
base,
bytestring,
Expand Down Expand Up @@ -450,7 +458,7 @@ test-suite parser-hackage-tests

if flag(parsec-struct-diff)
build-depends:
generics-sop >= 0.2.5 && <0.3,
generics-sop >= 0.3.1.0 && <0.4,
these >=0.7.1 && <0.8,
singleton-bool >=0.1.1.0 && <0.2,
keys
Expand Down
4 changes: 4 additions & 0 deletions Cabal/Distribution/Compat/Parsec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module Distribution.Compat.Parsec (
P.satisfy,
P.space,
P.spaces,
skipSpaces1,
P.string,
munch,
munch1,
Expand Down Expand Up @@ -72,3 +73,6 @@ munch
=> (Char -> Bool)
-> P.ParsecT s u m String
munch = many . P.satisfy

skipSpaces1 :: P.Stream s m Char => P.ParsecT s u m ()
skipSpaces1 = P.skipMany1 P.space
21 changes: 20 additions & 1 deletion Cabal/Distribution/Compat/ReadP.hs
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,23 @@ module Distribution.Compat.ReadP
-- * Running a parser
ReadS, -- :: *; = String -> [(a,String)]
readP_to_S, -- :: ReadP a -> ReadS a
readS_to_P -- :: ReadS a -> ReadP a
readS_to_P, -- :: ReadS a -> ReadP a

-- ** Parsec
parsecToReadP,
)
where

import Prelude ()
import Distribution.Compat.Prelude hiding (many, get)
import Control.Applicative (liftA2)

import qualified Distribution.Compat.MonadFail as Fail

import Control.Monad( replicateM, (>=>) )

import qualified Text.Parsec as P

infixr 5 +++, <++

-- ---------------------------------------------------------------------------
Expand Down Expand Up @@ -414,3 +420,16 @@ readS_to_P :: ReadS a -> ReadP r a
-- parser, and therefore a possible inefficiency.
readS_to_P r =
R (\k -> Look (\s -> final [bs'' | (a,s') <- r s, bs'' <- run (k a) s']))

-- ---------------------------------------------------------------------------
-- Converting from Parsec to ReadP
--
-- | Convert @Parsec@ parser to 'ReadP'.
parsecToReadP
:: P.Parsec [Char] u a
-> u -- ^ initial user state
-> ReadP r a
parsecToReadP p u = R $ \k -> Look $ \s ->
case P.runParser (liftA2 (,) p P.getInput) u "<parsecToReadP>" s of
Right (x, s') -> final (run (k x) s')
Left _ -> Fail
23 changes: 17 additions & 6 deletions Cabal/Distribution/Compiler.hs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ import Language.Haskell.Extension
import Distribution.Version (Version, mkVersion', nullVersion)

import qualified System.Info (compilerName, compilerVersion)
import Distribution.Parsec.Class (Parsec (..))
import Distribution.Pretty (Pretty (..))
import Distribution.Text (Text(..), display)
import qualified Distribution.Compat.ReadP as Parse
import qualified Distribution.Compat.Parsec as P
import qualified Text.PrettyPrint as Disp

data CompilerFlavor =
Expand All @@ -66,12 +69,20 @@ instance Binary CompilerFlavor
knownCompilerFlavors :: [CompilerFlavor]
knownCompilerFlavors = [GHC, GHCJS, NHC, YHC, Hugs, HBC, Helium, JHC, LHC, UHC]

instance Text CompilerFlavor where
disp (OtherCompiler name) = Disp.text name
disp (HaskellSuite name) = Disp.text name
disp NHC = Disp.text "nhc98"
disp other = Disp.text (lowercase (show other))
instance Pretty CompilerFlavor where
pretty (OtherCompiler name) = Disp.text name
pretty (HaskellSuite name) = Disp.text name
pretty NHC = Disp.text "nhc98"
pretty other = Disp.text (lowercase (show other))

instance Parsec CompilerFlavor where
parsec = classifyCompilerFlavor <$> component
where
component = do
cs <- P.munch1 isAlphaNum
if all isDigit cs then fail "all digits compiler name" else return cs

instance Text CompilerFlavor where
parse = do
comp <- Parse.munch1 isAlphaNum
when (all isDigit comp) Parse.pfail
Expand All @@ -81,7 +92,7 @@ classifyCompilerFlavor :: String -> CompilerFlavor
classifyCompilerFlavor s =
fromMaybe (OtherCompiler s) $ lookup (lowercase s) compilerMap
where
compilerMap = [ (display compiler, compiler)
compilerMap = [ (lowercase (display compiler), compiler)
| compiler <- knownCompilerFlavors ]


Expand Down
86 changes: 86 additions & 0 deletions Cabal/Distribution/FieldGrammar.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | This module provides a way to specify a grammar of @.cabal@ -like files.
module Distribution.FieldGrammar (
-- * Field grammar type
FieldGrammar (..),
uniqueField,
optionalField,
optionalFieldDef,
optionalFieldDefAla,
monoidalField,
deprecatedField',
-- * Concrete grammar implementations
ParsecFieldGrammar,
ParsecFieldGrammar',
parseFieldGrammar,
fieldGrammarKnownFieldList,
PrettyFieldGrammar,
PrettyFieldGrammar',
prettyFieldGrammar,
-- * Auxlilary
(^^^),
Section(..),
Fields,
partitionFields,
takeFields,
runFieldParser,
runFieldParser',
) where

import Distribution.Compat.Prelude
import Prelude ()

import qualified Distribution.Compat.Map.Strict as Map

import Distribution.FieldGrammar.Class
import Distribution.FieldGrammar.Parsec
import Distribution.FieldGrammar.Pretty
import Distribution.Parsec.Field
import Distribution.Utils.Generic (spanMaybe)

type ParsecFieldGrammar' a = ParsecFieldGrammar a a
type PrettyFieldGrammar' a = PrettyFieldGrammar a a

infixl 5 ^^^

-- | Reverse function application which binds tighter than '<$>' and '<*>'.
-- Useful for refining grammar specification.
--
-- @
-- \<*\> 'monoidalFieldAla' "extensions" (alaList' FSep MQuoted) oldExtensions
-- ^^^ 'deprecatedSince' [1,12] "Please use 'default-extensions' or 'other-extensions' fields."
-- @
(^^^) :: a -> (a -> b) -> b
x ^^^ f = f x

-- | Partitionin state
data PS ann = PS (Fields ann) [Section ann] [[Section ann]]

-- | Partition field list into field map and groups of sections.
partitionFields :: [Field ann] -> (Fields ann, [[Section ann]])
partitionFields = finalize . foldl' f (PS mempty mempty mempty)
where
finalize :: PS ann -> (Fields ann, [[Section ann]])
finalize (PS fs s ss)
| null s = (fs, reverse ss)
| otherwise = (fs, reverse (reverse s : ss))

f :: PS ann -> Field ann -> PS ann
f (PS fs s ss) (Field (Name ann name) fss) =
PS (Map.insertWith (flip (++)) name [MkNamelessField ann fss] fs) [] ss'
where
ss' | null s = ss
| otherwise = reverse s : ss
f (PS fs s ss) (Section name sargs sfields) =
PS fs (MkSection name sargs sfields : s) ss

-- | Take all fields from the front.
takeFields :: [Field ann] -> (Fields ann, [Field ann])
takeFields = finalize . spanMaybe match
where
finalize (fs, rest) = (Map.fromListWith (flip (++)) fs, rest)

match (Field (Name ann name) fs) = Just (name, [MkNamelessField ann fs])
match _ = Nothing
149 changes: 149 additions & 0 deletions Cabal/Distribution/FieldGrammar/Class.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
module Distribution.FieldGrammar.Class (
FieldGrammar (..),
uniqueField,
optionalField,
optionalFieldDef,
optionalFieldDefAla,
monoidalField,
deprecatedField',
) where

import Distribution.Compat.Lens
import Distribution.Compat.Prelude
import Prelude ()

import Data.Functor.Identity (Identity (..))

import Distribution.Compat.Newtype (Newtype)
import Distribution.Parsec.Class (Parsec)
import Distribution.Parsec.Field
import Distribution.Pretty (Pretty)

-- | 'FieldGrammar' is parametrised by
--
-- * @s@ which is a structure we are parsing. We need this to provide prettyprinter
-- functionality
--
-- * @a@ type of the field.
--
-- /Note:/ We'd like to have @forall s. Applicative (f s)@ context.
--
class FieldGrammar g where
-- | Unfocus, zoom out, /blur/ 'FieldGrammar'.
blurFieldGrammar :: ALens' a b -> g b c -> g a c

-- | Field which should be defined, exactly once.
uniqueFieldAla
:: (Parsec b, Pretty b, Newtype b a)
=> FieldName -- ^ field name
-> (a -> b) -- ^ 'Newtype' pack
-> ALens' s a -- ^ lens into the field
-> g s a

-- | Boolean field with a default value.
booleanFieldDef
:: FieldName -- ^ field name
-> ALens' s Bool -- ^ lens into the field
-> Bool -- ^ default
-> g s Bool

-- | Optional field.
optionalFieldAla
:: (Parsec b, Pretty b, Newtype b a)
=> FieldName -- ^ field name
-> (a -> b) -- ^ 'pack'
-> ALens' s (Maybe a) -- ^ lens into the field
-> g s (Maybe a)

-- | Monoidal field.
--
-- Values are combined with 'mappend'.
--
-- /Note:/ 'optionalFieldAla' is a @monoidalField@ with 'Last' monoid.
--
monoidalFieldAla
:: (Parsec b, Pretty b, Monoid a, Newtype b a)
=> FieldName -- ^ field name
-> (a -> b) -- ^ 'pack'
-> ALens' s a -- ^ lens into the field
-> g s a

-- | Parser matching all fields with a name starting with a prefix.
prefixedFields
:: FieldName -- ^ field name prefix
-> ALens' s [(String, String)] -- ^ lens into the field
-> g s [(String, String)]

-- | Known field, which we don't parse, neither pretty print.
knownField :: FieldName -> g s ()

-- | Field which is parsed but not pretty printed.
hiddenField :: g s a -> g s a

-- | Deprecated since
deprecatedSince
:: [Int] -- ^ version
-> String -- ^ deprecation message
-> g s a
-> g s a

-- | Annotate field with since spec-version.
availableSince
:: [Int] -- ^ spec version
-> g s a
-> g s a

-- | Field which can be defined at most once.
uniqueField
:: (FieldGrammar g, Parsec a, Pretty a)
=> FieldName -- ^ field name
-> ALens' s a -- ^ lens into the field
-> g s a
uniqueField fn = uniqueFieldAla fn Identity

-- | Field which can be defined at most once.
optionalField
:: (FieldGrammar g, Parsec a, Pretty a)
=> FieldName -- ^ field name
-> ALens' s (Maybe a) -- ^ lens into the field
-> g s (Maybe a)
optionalField fn = optionalFieldAla fn Identity

-- | Optional field with default value.
optionalFieldDef
:: (FieldGrammar g, Functor (g s), Parsec a, Pretty a, Eq a, Show a)
=> FieldName -- ^ field name
-> LensLike' (Pretext (Maybe a) (Maybe a)) s a -- ^ @'Lens'' s a@: lens into the field
-> a -- ^ default value
-> g s a
optionalFieldDef fn = optionalFieldDefAla fn Identity

-- | Optional field with default value.
optionalFieldDefAla
:: (FieldGrammar g, Functor (g s), Parsec b, Pretty b, Newtype b a, Eq a, Show a)
=> FieldName -- ^ field name
-> (a -> b) -- ^ 'Newtype' pack
-> LensLike' (Pretext (Maybe a) (Maybe a)) s a -- ^ @'Lens'' s a@: lens into the field
-> a -- ^ default value
-> g s a
optionalFieldDefAla fn pack l def =
fromMaybe def <$> optionalFieldAla fn pack (l . fromNon def)

-- | Field which can be define multiple times, and the results are @mappend@ed.
monoidalField
:: (FieldGrammar g, Parsec a, Pretty a, Monoid a)
=> FieldName -- ^ field name
-> ALens' s a -- ^ lens into the field
-> g s a
monoidalField fn = monoidalFieldAla fn Identity

-- | Deprecated field. If found, warning is issued.
--
-- /Note:/ also it's not pretty printed!
--
deprecatedField'
:: FieldGrammar g
=> String -- ^ deprecation message
-> g s a
-> g s a
deprecatedField' = deprecatedSince []
Loading

0 comments on commit 690a725

Please sign in to comment.