Skip to content

Commit

Permalink
Add ProjectParseResult
Browse files Browse the repository at this point in the history
- Track which file has errors and which has warnings
- Add test for import parse warnings
- Remove added type sigs, use e for error type
- Move ProjectParseResult into its own module
- Import qualified from Deprecated.ParseUtils
- Reverse warnings so they are in line number order
- Report parse result error in imported config
- Split project test into warning and error tests
- Add type synonyms for project parse
- Extract function reportProjectParseWarnings
- Show the snippet that doesn't parse
- Add if, elif and else test projects
- Fix else for elif typo
- Show provenance if not root
- Rerun expected output with provenance
- Redo ParseWarningProvenence with ordered output
- Add ProjectParseError record
- Reword badly formed comment lines
- Satisfy fix-whitespace
- Add changelog entry
- Updated - indented expectation
- No snippet when modifying compiler under condition
- Only show custom message with snippet
- Rerun expected output with source
- Use a Doc for the ReportParseResult message
- Update expected .out files
- Use normalized path when recursing
- Consistent projectParse ... source
- Consistent projectParse ... normSource
- Use normalizeWindowsOutput
- Use .md extension on changelog entry
- Satisfy HLint
- Revert elif to else, see that this is wrong and undo
  • Loading branch information
philderbeast committed Jan 21, 2025
1 parent 54d364d commit 8ba44d3
Show file tree
Hide file tree
Showing 26 changed files with 727 additions and 109 deletions.
1 change: 1 addition & 0 deletions cabal-install/cabal-install.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ library
-- this modules are moved from Cabal
-- they are needed for as long until cabal-install moves to parsec parser
Distribution.Deprecated.ParseUtils
Distribution.Deprecated.ProjectParseUtils
Distribution.Deprecated.ReadP
Distribution.Deprecated.ViewAsFieldDescr

Expand Down
17 changes: 9 additions & 8 deletions cabal-install/src/Distribution/Client/Errors.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import Distribution.Pretty
import Distribution.Simple (VersionRange)
import Distribution.Simple.Utils
import Network.URI
import Text.PrettyPrint hiding (render, (<>))
import qualified Text.PrettyPrint as PP
import Text.Regex.Posix.ByteString (WrapError)

data CabalInstallException
Expand Down Expand Up @@ -112,7 +114,7 @@ data CabalInstallException
| ParseExtraLinesFailedErr String String
| ParseExtraLinesOkError [PWarning]
| FetchPackageErr
| ReportParseResult String FilePath String String
| ReportParseResult String FilePath String Doc
| ReportSourceRepoProblems String
| BenchActionException
| RenderBenchTargetProblem [String]
Expand Down Expand Up @@ -495,13 +497,12 @@ exceptionMessageCabalInstall e = case e of
ParseExtraLinesOkError ws -> unlines (map (showPWarning "Error parsing additional config lines") ws)
FetchPackageErr -> "fetchPackage: source repos not supported"
ReportParseResult filetype filename line msg ->
"Error parsing "
++ filetype
++ " "
++ filename
++ line
++ ":\n"
++ msg
PP.render $
vcat
-- NOTE: As given to us, the line number string is prefixed by a colon.
[ text "Error parsing" <+> text filetype <+> text filename PP.<> text line PP.<> colon
, nest 1 $ text "-" <+> msg
]
ReportSourceRepoProblems errorStr -> errorStr
BenchActionException ->
"The bench command does not support '--only-dependencies'. "
Expand Down
72 changes: 53 additions & 19 deletions cabal-install/src/Distribution/Client/ProjectConfig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,18 @@ module Distribution.Client.ProjectConfig
, maxNumFetchJobs
) where

import Distribution.Client.Compat.Prelude
import Text.PrettyPrint (nest, render, text, vcat)
import Distribution.Client.Compat.Prelude hiding (empty)
import Distribution.Simple.Utils
( createDirectoryIfMissingVerbose
, dieWithException
, maybeExit
, notice
, noticeDoc
, ordNub
, rawSystemIOWithEnv
, warn
)
import Text.PrettyPrint (cat, colon, comma, empty, hsep, nest, quotes, render, text, vcat)
import Prelude ()

import Distribution.Client.Glob
Expand Down Expand Up @@ -136,10 +146,12 @@ import Distribution.Client.Utils
( determineNumJobs
)
import qualified Distribution.Deprecated.ParseUtils as OldParser
( ParseResult (..)
, locatedErrorMsg
( locatedErrorMsg
, showPWarning
)
import qualified Distribution.Deprecated.ProjectParseUtils as OldParser
( ProjectParseResult (..)
)
import Distribution.Fields
( PError
, PWarning
Expand Down Expand Up @@ -172,14 +184,6 @@ import Distribution.Simple.Setup
, fromFlagOrDefault
, toFlag
)
import Distribution.Simple.Utils
( createDirectoryIfMissingVerbose
, dieWithException
, maybeExit
, notice
, rawSystemIOWithEnv
, warn
)
import Distribution.System
( Platform
)
Expand Down Expand Up @@ -240,6 +244,7 @@ import System.IO
, withBinaryFile
)

import Distribution.Deprecated.ProjectParseUtils (ProjectParseError (..), ProjectParseWarning)
import Distribution.Solver.Types.ProjectConfigPath

----------------------------------------
Expand Down Expand Up @@ -874,16 +879,45 @@ readGlobalConfig verbosity configFileFlag = do
monitorFiles [monitorFileHashed configFile]
return (convertLegacyGlobalConfig config)

reportParseResult :: Verbosity -> String -> FilePath -> OldParser.ParseResult ProjectConfigSkeleton -> IO ProjectConfigSkeleton
reportParseResult verbosity _filetype filename (OldParser.ParseOk warnings x) = do
reportProjectParseWarnings :: Verbosity -> FilePath -> [ProjectParseWarning] -> IO ()
reportProjectParseWarnings verbosity projectFile warnings =
unless (null warnings) $
let msg = unlines (map (OldParser.showPWarning (intercalate ", " $ filename : (projectConfigPathRoot <$> projectSkeletonImports x))) warnings)
in warn verbosity msg
let msgs =
[ OldParser.showPWarning pFilename w
| (p, w) <- warnings
, let pFilename = fst $ unconsProjectConfigPath p
]
in noticeDoc verbosity $
vcat
[ (text "Warnings found while parsing the project file" <> comma) <+> (text (takeFileName projectFile) <> colon)
, cat [nest 1 $ text "-" <+> text m | m <- ordNub msgs]
]

reportParseResult :: Verbosity -> String -> FilePath -> OldParser.ProjectParseResult ProjectConfigSkeleton -> IO ProjectConfigSkeleton
reportParseResult verbosity _filetype projectFile (OldParser.ProjectParseOk warnings x) = do
reportProjectParseWarnings verbosity projectFile warnings
return x
reportParseResult verbosity filetype filename (OldParser.ParseFailed err) =
reportParseResult verbosity filetype projectFile (OldParser.ProjectParseFailed (ProjectParseError snippet rootOrImportee err)) = do
let (line, msg) = OldParser.locatedErrorMsg err
errLineNo = maybe "" (\n -> ':' : show n) line
in dieWithException verbosity $ ReportParseResult filetype filename errLineNo msg
let errLineNo = maybe "" (\n -> ':' : show n) line
let (sourceFile, provenance) =
maybe
(projectFile, empty)
( \p ->
( fst $ unconsProjectConfigPath p
, if isTopLevelConfigPath p then empty else docProjectConfigPath p
)
)
rootOrImportee
let doc = case snippet of
Nothing -> vcat (text <$> lines msg)
Just s ->
vcat
[ provenance
, text "Failed to parse" <+> quotes (text s) <+> (text "with error" <> colon)
, nest 2 $ hsep $ text <$> lines msg
]
dieWithException verbosity $ ReportParseResult filetype sourceFile errLineNo doc

---------------------------------------------
-- Finding packages in the project
Expand Down
73 changes: 46 additions & 27 deletions cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}

-- | Project configuration, implementation in terms of legacy types.
Expand Down Expand Up @@ -161,6 +163,11 @@ import Distribution.Deprecated.ParseUtils
, syntaxError
)
import qualified Distribution.Deprecated.ParseUtils as ParseUtils
import Distribution.Deprecated.ProjectParseUtils
( ProjectParseResult (..)
, projectParse
, projectParseFail
)
import Distribution.Deprecated.ReadP
( ReadP
, (+++)
Expand All @@ -185,6 +192,7 @@ import Distribution.Utils.Path hiding
)

import qualified Data.ByteString.Char8 as BS
import Data.Functor ((<&>))
import qualified Data.Map as Map
import qualified Data.Set as Set
import Network.URI (URI (..), nullURIAuth, parseURI)
Expand Down Expand Up @@ -242,12 +250,15 @@ parseProject
-> Verbosity
-> ProjectConfigToParse
-- ^ The contents of the file to parse
-> IO (ParseResult ProjectConfigSkeleton)
parseProject rootPath cacheDir httpTransport verbosity configToParse = do
let (dir, projectFileName) = splitFileName rootPath
projectDir <- makeAbsolute dir
projectPath <- canonicalizeConfigPath projectDir (ProjectConfigPath $ projectFileName :| [])
parseProjectSkeleton cacheDir httpTransport verbosity projectDir projectPath configToParse
-> IO (ProjectParseResult ProjectConfigSkeleton)
parseProject rootPath cacheDir httpTransport verbosity configToParse =
do
let (dir, projectFileName) = splitFileName rootPath
projectDir <- makeAbsolute dir
projectPath <- canonicalizeConfigPath projectDir (ProjectConfigPath $ projectFileName :| [])
parseProjectSkeleton cacheDir httpTransport verbosity projectDir projectPath configToParse
-- NOTE: Reverse the warnings so they are in line number order.
<&> \case ProjectParseOk ws x -> ProjectParseOk (reverse ws) x; x -> x

parseProjectSkeleton
:: FilePath
Expand All @@ -259,60 +270,65 @@ parseProjectSkeleton
-- ^ The path of the file being parsed, either the root or an import
-> ProjectConfigToParse
-- ^ The contents of the file to parse
-> IO (ParseResult ProjectConfigSkeleton)
-> IO (ProjectParseResult ProjectConfigSkeleton)
parseProjectSkeleton cacheDir httpTransport verbosity projectDir source (ProjectConfigToParse bs) =
(sanityWalkPCS False =<<) <$> liftPR (go []) (ParseUtils.readFields bs)
(sanityWalkPCS False =<<) <$> liftPR source (go []) (ParseUtils.readFields bs)
where
go :: [ParseUtils.Field] -> [ParseUtils.Field] -> IO (ParseResult ProjectConfigSkeleton)
go :: [ParseUtils.Field] -> [ParseUtils.Field] -> IO (ProjectParseResult ProjectConfigSkeleton)
go acc (x : xs) = case x of
(ParseUtils.F _ "import" importLoc) -> do
let importLocPath = importLoc `consProjectConfigPath` source

-- Once we canonicalize the import path, we can check for cyclical imports
normSource <- canonicalizeConfigPath projectDir source
normLocPath <- canonicalizeConfigPath projectDir importLocPath

debug verbosity $ "\nimport path, normalized\n=======================\n" ++ render (docProjectConfigPath normLocPath)

if isCyclicConfigPath normLocPath
then pure . parseFail $ ParseUtils.FromString (render $ cyclicalImportMsg normLocPath) Nothing
then pure . projectParseFail Nothing (Just normSource) $ ParseUtils.FromString (render $ cyclicalImportMsg normLocPath) Nothing
else do
when
(isUntrimmedUriConfigPath importLocPath)
(noticeDoc verbosity $ untrimmedUriImportMsg (Disp.text "Warning:") importLocPath)
normSource <- canonicalizeConfigPath projectDir source
let fs = (\z -> CondNode z [normLocPath] mempty) <$> fieldsToConfig normSource (reverse acc)
res <- parseProjectSkeleton cacheDir httpTransport verbosity projectDir importLocPath . ProjectConfigToParse =<< fetchImportConfig normLocPath
rest <- go [] xs
pure . fmap mconcat . sequence $ [fs, res, rest]
pure . fmap mconcat . sequence $ [projectParse Nothing normSource fs, res, rest]
(ParseUtils.Section l "if" p xs') -> do
normSource <- canonicalizeConfigPath projectDir source
subpcs <- go [] xs'
let fs = singletonProjectConfigSkeleton <$> fieldsToConfig source (reverse acc)
(elseClauses, rest) <- parseElseClauses xs
let condNode =
(\c pcs e -> CondNode mempty mempty [CondBranch c pcs e])
<$>
-- we rewrap as as a section so the readFields lexer of the conditional parser doesn't get confused
adaptParseError l (parseConditionConfVarFromClause . BS.pack $ "if(" <> p <> ")")
( let s = "if(" <> p <> ")"
in projectParse (Just s) normSource (adaptParseError l (parseConditionConfVarFromClause $ BS.pack s))
)
<*> subpcs
<*> elseClauses
pure . fmap mconcat . sequence $ [fs, condNode, rest]
pure . fmap mconcat . sequence $ [projectParse Nothing normSource fs, condNode, rest]
_ -> go (x : acc) xs
go acc [] = do
normSource <- canonicalizeConfigPath projectDir source
pure . fmap singletonProjectConfigSkeleton . fieldsToConfig normSource $ reverse acc
pure . fmap singletonProjectConfigSkeleton . projectParse Nothing normSource . fieldsToConfig normSource $ reverse acc

parseElseClauses :: [ParseUtils.Field] -> IO (ParseResult (Maybe ProjectConfigSkeleton), ParseResult ProjectConfigSkeleton)
parseElseClauses :: [ParseUtils.Field] -> IO (ProjectParseResult (Maybe ProjectConfigSkeleton), ProjectParseResult ProjectConfigSkeleton)
parseElseClauses x = case x of
(ParseUtils.Section _l "else" _p xs' : xs) -> do
subpcs <- go [] xs'
rest <- go [] xs
pure (Just <$> subpcs, rest)
(ParseUtils.Section l "elif" p xs' : xs) -> do
normSource <- canonicalizeConfigPath projectDir source
subpcs <- go [] xs'
(elseClauses, rest) <- parseElseClauses xs
let condNode =
(\c pcs e -> CondNode mempty mempty [CondBranch c pcs e])
<$> adaptParseError l (parseConditionConfVarFromClause . BS.pack $ "else(" <> p <> ")")
<$> ( let s = "elif(" <> p <> ")"
in projectParse (Just s) normSource (adaptParseError l (parseConditionConfVarFromClause $ BS.pack s))
)
<*> subpcs
<*> elseClauses
pure (Just <$> condNode, rest)
Expand All @@ -331,15 +347,16 @@ parseProjectSkeleton cacheDir httpTransport verbosity projectDir source (Project
addProvenance :: ProjectConfigPath -> ProjectConfig -> ProjectConfig
addProvenance sourcePath x = x{projectConfigProvenance = Set.singleton $ Explicit sourcePath}

adaptParseError :: Show e => ParseUtils.LineNo -> Either e a -> ParseResult a
adaptParseError _ (Right x) = pure x
adaptParseError l (Left e) = parseFail $ ParseUtils.FromString (show e) (Just l)

liftPR :: (a -> IO (ParseResult b)) -> ParseResult a -> IO (ParseResult b)
liftPR f (ParseOk ws x) = addWarnings <$> f x
liftPR :: ProjectConfigPath -> (a -> IO (ProjectParseResult b)) -> ParseResult a -> IO (ProjectParseResult b)
liftPR p f (ParseOk ws x) = addWarnings <$> f x
where
addWarnings (ParseOk ws' x') = ParseOk (ws' ++ ws) x'
addWarnings (ProjectParseOk ws' x') = ProjectParseOk (ws' ++ ((p,) <$> ws)) x'
addWarnings x' = x'
liftPR _ (ParseFailed e) = pure $ ParseFailed e
liftPR p _ (ParseFailed e) = pure $ projectParseFail Nothing (Just p) e

fetchImportConfig :: ProjectConfigPath -> IO BS.ByteString
fetchImportConfig (ProjectConfigPath (pci :| _)) = do
Expand All @@ -362,12 +379,14 @@ parseProjectSkeleton cacheDir httpTransport verbosity projectDir source (Project
where
isSet f = f (projectConfigShared pc) /= NoFlag

sanityWalkPCS :: Bool -> ProjectConfigSkeleton -> ParseResult ProjectConfigSkeleton
sanityWalkPCS underConditional t@(CondNode d _c comps)
| underConditional && modifiesCompiler d = parseFail $ ParseUtils.FromString "Cannot set compiler in a conditional clause of a cabal project file" Nothing
| otherwise = mapM_ sanityWalkBranch comps >> pure t
sanityWalkPCS :: Bool -> ProjectConfigSkeleton -> ProjectParseResult ProjectConfigSkeleton
sanityWalkPCS underConditional t@(CondNode d (listToMaybe -> c) comps)
| underConditional && modifiesCompiler d =
projectParseFail Nothing c $ ParseUtils.FromString "Cannot set compiler in a conditional clause of a cabal project file" Nothing
| otherwise =
mapM_ sanityWalkBranch comps >> pure t

sanityWalkBranch :: CondBranch ConfVar [ProjectConfigPath] ProjectConfig -> ParseResult ()
sanityWalkBranch :: CondBranch ConfVar [ProjectConfigPath] ProjectConfig -> ProjectParseResult ()
sanityWalkBranch (CondBranch _c t f) = traverse_ (sanityWalkPCS True) f >> sanityWalkPCS True t >> pure ()

------------------------------------------------------------------
Expand Down
51 changes: 51 additions & 0 deletions cabal-install/src/Distribution/Deprecated/ProjectParseUtils.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{-# OPTIONS_HADDOCK hide #-}

module Distribution.Deprecated.ProjectParseUtils
( ProjectParseError (..)
, ProjectParseWarning
, ProjectParseResult (..)
, projectParseFail
, projectParse
) where

import Distribution.Client.Compat.Prelude hiding (get)
import Prelude ()

import qualified Distribution.Deprecated.ParseUtils as Pkg (PError, PWarning, ParseResult (..))
import Distribution.Solver.Types.ProjectConfigPath (ProjectConfigPath)

type ProjectParseWarning = (ProjectConfigPath, Pkg.PWarning)

data ProjectParseError = ProjectParseError
{ projectParseSnippet :: Maybe String
, projectParseSource :: Maybe ProjectConfigPath
, projectParseError :: Pkg.PError
}
deriving (Show)

data ProjectParseResult a
= ProjectParseFailed ProjectParseError
| ProjectParseOk [ProjectParseWarning] a
deriving (Show)

projectParse :: Maybe String -> ProjectConfigPath -> Pkg.ParseResult a -> ProjectParseResult a
projectParse s path (Pkg.ParseFailed err) = ProjectParseFailed $ ProjectParseError s (Just path) err
projectParse _ path (Pkg.ParseOk ws x) = ProjectParseOk [(path, w) | w <- ws] x

instance Functor ProjectParseResult where
fmap _ (ProjectParseFailed err) = ProjectParseFailed err
fmap f (ProjectParseOk ws x) = ProjectParseOk ws $ f x

instance Applicative ProjectParseResult where
pure = ProjectParseOk []
(<*>) = ap

instance Monad ProjectParseResult where
return = pure
ProjectParseFailed err >>= _ = ProjectParseFailed err
ProjectParseOk ws x >>= f = case f x of
ProjectParseFailed err -> ProjectParseFailed err
ProjectParseOk ws' x' -> ProjectParseOk (ws' ++ ws) x'

projectParseFail :: Maybe String -> Maybe ProjectConfigPath -> Pkg.PError -> ProjectParseResult a
projectParseFail s p e = ProjectParseFailed $ ProjectParseError s p e
Loading

0 comments on commit 8ba44d3

Please sign in to comment.