Skip to content

Replace cabal project parsing with Parsec #8889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cabal-syntax/Cabal-syntax.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ library
Distribution.Parsec.FieldLineStream
Distribution.Parsec.Position
Distribution.Parsec.Warning
Distribution.Parsec.Source
Distribution.Pretty
Distribution.SPDX
Distribution.SPDX.License
Expand Down
15 changes: 15 additions & 0 deletions Cabal-syntax/src/Distribution/FieldGrammar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Distribution.FieldGrammar
, ParsecFieldGrammar
, ParsecFieldGrammar'
, parseFieldGrammar
, parseFieldGrammarCheckingStanzas
, fieldGrammarKnownFieldList
, PrettyFieldGrammar
, PrettyFieldGrammar'
Expand Down Expand Up @@ -65,6 +66,20 @@ x ^^^ f = f x
data PS ann = PS (Fields ann) [Section ann] [[Section ann]]

-- | Partition field list into field map and groups of sections.
-- Groups sections between fields. This means that the following snippet contains
-- two section groups:
--
-- @
-- -- first group
-- some-section
-- field: value
-- another-section
-- field: value
-- foo: bar
-- -- second group
-- yet-another-section
-- field: value
-- @
partitionFields :: [Field ann] -> (Fields ann, [[Section ann]])
partitionFields = finalize . foldl' f (PS mempty mempty mempty)
where
Expand Down
35 changes: 24 additions & 11 deletions Cabal-syntax/src/Distribution/FieldGrammar/Parsec.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | This module provides a 'FieldGrammarParser', one way to parse
Expand Down Expand Up @@ -54,6 +55,7 @@
module Distribution.FieldGrammar.Parsec
( ParsecFieldGrammar
, parseFieldGrammar
, parseFieldGrammarCheckingStanzas
, fieldGrammarKnownFieldList

-- * Auxiliary
Expand Down Expand Up @@ -112,24 +114,35 @@ data Section ann = MkSection !(Name ann) [SectionArg ann] [Field ann]
data ParsecFieldGrammar s a = ParsecFG
{ fieldGrammarKnownFields :: !(Set FieldName)
, fieldGrammarKnownPrefixes :: !(Set FieldName)
, fieldGrammarParser :: !(CabalSpecVersion -> Fields Position -> ParseResult a)
, fieldGrammarParser :: forall src. (CabalSpecVersion -> Fields Position -> ParseResult src a)
}
deriving (Functor)

parseFieldGrammar :: CabalSpecVersion -> Fields Position -> ParsecFieldGrammar s a -> ParseResult a
parseFieldGrammar :: CabalSpecVersion -> Fields Position -> ParsecFieldGrammar s a -> ParseResult src a
parseFieldGrammar v fields grammar = do
for_ (Map.toList (Map.filterWithKey isUnknownField fields)) $ \(name, nfields) ->
for_ (Map.toList (Map.filterWithKey (isUnknownField grammar) fields)) $ \(name, nfields) ->
for_ nfields $ \(MkNamelessField pos _) ->
parseWarning pos PWTUnknownField $ "Unknown field: " ++ show name
-- TODO: fields allowed in this section

-- parse
fieldGrammarParser grammar v fields
where
isUnknownField k _ =
not $
k `Set.member` fieldGrammarKnownFields grammar
|| any (`BS.isPrefixOf` k) (fieldGrammarKnownPrefixes grammar)

isUnknownField :: ParsecFieldGrammar s a -> FieldName -> [NamelessField Position] -> Bool
isUnknownField grammar k _ =
not $
k `Set.member` fieldGrammarKnownFields grammar
|| any (`BS.isPrefixOf` k) (fieldGrammarKnownPrefixes grammar)

-- | Parse a ParsecFieldGrammar and check for fields that should be stanzas.
parseFieldGrammarCheckingStanzas :: CabalSpecVersion -> Fields Position -> ParsecFieldGrammar s a -> Set BS.ByteString -> ParseResult src a
parseFieldGrammarCheckingStanzas v fields grammar sections = do
for_ (Map.toList (Map.filterWithKey (isUnknownField grammar) fields)) $ \(name, nfields) ->
for_ nfields $ \(MkNamelessField pos _) ->
if name `Set.member` sections
then parseFailure pos $ "'" ++ fromUTF8BS name ++ "' is a stanza, not a field. Remove the trailing ':' to parse a stanza."
else parseWarning pos PWTUnknownField $ "Unknown field: " ++ show name
fieldGrammarParser grammar v fields

fieldGrammarKnownFieldList :: ParsecFieldGrammar s a -> [FieldName]
fieldGrammarKnownFieldList = Set.toList . fieldGrammarKnownFields
Expand All @@ -145,7 +158,7 @@ instance Applicative (ParsecFieldGrammar s) where
(\v fields -> f'' v fields <*> x'' v fields)
{-# INLINE (<*>) #-}

warnMultipleSingularFields :: FieldName -> [NamelessField Position] -> ParseResult ()
warnMultipleSingularFields :: FieldName -> [NamelessField Position] -> ParseResult src ()
warnMultipleSingularFields _ [] = pure ()
warnMultipleSingularFields fn (x : xs) = do
let pos = namelessFieldAnn x
Expand Down Expand Up @@ -349,7 +362,7 @@ instance FieldGrammar Parsec ParsecFieldGrammar where
-- Parsec
-------------------------------------------------------------------------------

runFieldParser' :: [Position] -> ParsecParser a -> CabalSpecVersion -> FieldLineStream -> ParseResult a
runFieldParser' :: [Position] -> ParsecParser a -> CabalSpecVersion -> FieldLineStream -> ParseResult src a
runFieldParser' inputPoss p v str = case P.runParser p' [] "<field>" str of
Right (pok, ws) -> do
traverse_ (\(PWarning t pos w) -> parseWarning (mapPosition pos) t w) ws
Expand Down Expand Up @@ -378,7 +391,7 @@ runFieldParser' inputPoss p v str = case P.runParser p' [] "<field>" str of
go n (Position row col : _) | n <= 0 = Position row (col + pcol - 1)
go n (_ : ps) = go (n - 1) ps

runFieldParser :: Position -> ParsecParser a -> CabalSpecVersion -> [FieldLine Position] -> ParseResult a
runFieldParser :: Position -> ParsecParser a -> CabalSpecVersion -> [FieldLine Position] -> ParseResult src a
runFieldParser pp p v ls = runFieldParser' poss p v (fieldLinesToStream ls)
where
poss = map (\(FieldLine pos _) -> pos) ls ++ [pp] -- add "default" position
Expand Down
5 changes: 5 additions & 0 deletions Cabal-syntax/src/Distribution/Fields.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ module Distribution.Fields
-- ** Warnings
, PWarnType (..)
, PWarning (..)
, PWarningWithSource (..)
, PSource (..)
, showPWarning
, showPWarningWithSource

-- ** Errors
, PError (..)
, PErrorWithSource (..)
, showPError
, showPErrorWithSource

-- * Pretty printing
, CommentPosition (..)
Expand Down
22 changes: 15 additions & 7 deletions Cabal-syntax/src/Distribution/Fields/ConfVar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ module Distribution.Fields.ConfVar (parseConditionConfVar, parseConditionConfVar

import Distribution.Compat.CharParsing (char, integral)
import Distribution.Compat.Prelude
import Distribution.Fields.Field (Field (..), SectionArg (..))
import Distribution.Fields.Field (Field (..), SectionArg (..), sectionArgAnn)
import Distribution.Fields.ParseResult
import Distribution.Fields.Parser (readFields)
import Distribution.Parsec (Parsec (..), Position (..), runParsecParser)
import Distribution.Parsec (Parsec (..), runParsecParser)
import Distribution.Parsec.FieldLineStream (fieldLineStreamFromBS)
import Distribution.Parsec.Position
import Distribution.Types.Condition
import Distribution.Types.ConfVar (ConfVar (..))
import Distribution.Version
Expand Down Expand Up @@ -40,10 +41,10 @@ parseConditionConfVarFromClause x =

-- | Parse @'Condition' 'ConfVar'@ from section arguments provided by parsec
-- based outline parser.
parseConditionConfVar :: [SectionArg Position] -> ParseResult (Condition ConfVar)
parseConditionConfVar args =
parseConditionConfVar :: Position -> [SectionArg Position] -> ParseResult src (Condition ConfVar)
parseConditionConfVar start_pos args =
-- The name of the input file is irrelevant, as we reformat the error message.
case P.runParser (parser <* P.eof) () "<condition>" args of
case P.runParser (P.setPosition startPos >> parser <* P.eof) () "<condition>" args of
Right x -> pure x
Left err -> do
-- Mangle the position to the actual one
Expand All @@ -59,7 +60,10 @@ parseConditionConfVar args =
(P.errorMessages err)
parseFailure epos msg
pure $ Lit True
where
startPos = P.newPos "<condition>" (positionRow start_pos) (positionCol start_pos)

-- | Parser for 'Condition' 'ConfVar'
type Parser = P.Parsec [SectionArg Position] ()

sepByNonEmpty :: Parser a -> Parser sep -> Parser (NonEmpty a)
Expand Down Expand Up @@ -132,6 +136,7 @@ parser = condOr
| s == "false" -> Just False
_ -> Nothing

string :: B8.ByteString -> Parser ()
string s = tokenPrim $ \t -> case t of
SecArgName _ s' | s == s' -> Just ()
_ -> Nothing
Expand All @@ -142,9 +147,12 @@ parser = condOr

parens = P.between (oper "(") (oper ")")

tokenPrim :: (SectionArg Position -> Maybe a) -> Parser a
tokenPrim = P.tokenPrim prettySectionArg updatePosition
-- TODO: check where the errors are reported
updatePosition x _ _ = x
updatePosition :: P.SourcePos -> SectionArg Position -> [SectionArg Position] -> P.SourcePos
updatePosition x s _ =
let Position line col = sectionArgAnn s
in P.setSourceLine (P.setSourceColumn x col) (line)
prettySectionArg = show

fromParsec :: Parsec a => Parser a
Expand Down
Loading
Loading