Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a562b2d
Add parseProjectConfig
philderbeast Jul 21, 2026
2fd8fe5
Update test to use parseProjectConfig
philderbeast Jul 21, 2026
08270e5
Add more specific test names
philderbeast Jul 21, 2026
458cb5a
Clear provenance before comparison
philderbeast Jul 21, 2026
9f1a515
Add boolean parsing for debug info
philderbeast Jul 22, 2026
0761d97
Add haddocks to default and enabled levels
philderbeast Jul 22, 2026
fa76e90
Add parsing debug-info test
philderbeast Jul 22, 2026
c8eea97
WIP
philderbeast Jul 28, 2026
2eec7ea
Add doctests for parsing legacy packages
philderbeast Jul 28, 2026
011273d
Add packages glob parser test
philderbeast Jul 28, 2026
51904bd
Add a glob test for optional packages
philderbeast Jul 28, 2026
e136451
Add a glob parser for package location
philderbeast Jul 28, 2026
add0da6
Avoid the use of Newtype
philderbeast Jul 28, 2026
265d23c
For roundtripping, show delimited lines too
philderbeast Jul 30, 2026
4c30897
Add doctests for project parsers
philderbeast Jul 30, 2026
fd126e5
Add empty string local parser test
philderbeast Jul 30, 2026
c145314
Satisfy fix-whitespace
philderbeast Sep 23, 2026
b93bc3d
Satisfy fourmolu
philderbeast Sep 23, 2026
9ac7248
Use alternative for removed unpack'
philderbeast Sep 23, 2026
68e0267
Grammar disallows parser setting in project
philderbeast Sep 23, 2026
fd57616
Expect negative for max-backjumps
philderbeast Sep 23, 2026
c403f45
Avoid empty and whitespace tokens
philderbeast Sep 23, 2026
58f80f8
Don't generate commas for single-value fields
philderbeast Sep 23, 2026
3a8255a
Document & test that legacy sets empty fields to ""
philderbeast Sep 23, 2026
a67961b
Get rid of top level function only used by doctest
philderbeast Sep 23, 2026
ccd3300
Add a changelog entry
philderbeast Sep 23, 2026
cd9ef4c
debug-info now passes
philderbeast Sep 24, 2026
e68b31d
Use normalise when reading
philderbeast Sep 24, 2026
6b4663f
Add fileNoIndexURIPath, haddocks and doctests
philderbeast Sep 24, 2026
484fb9b
Expect native local repo paths in the parser test
philderbeast Sep 24, 2026
6579dbd
Say why local repo paths are stored native
philderbeast Sep 29, 2026
dfc7562
Spell out the uses of paths
philderbeast Sep 29, 2026
d33e8d1
Assert both parsers agree in readConfig
philderbeast Sep 29, 2026
fd2eec0
Show a tree diff when the parsers disagree
philderbeast Sep 29, 2026
335cd42
Parse the round trip text with both parsers
philderbeast Sep 29, 2026
5f74257
Allow max-backjumps=-1 in docs
philderbeast Sep 29, 2026
304a0a4
Add a test for parser selection as project field
philderbeast Sep 29, 2026
22eb63f
Separate the .out files
philderbeast Sep 29, 2026
135dc1d
Add a warning about project-file-parser as field
philderbeast Sep 29, 2026
f2d144c
Mention the warning in the docs
philderbeast Sep 29, 2026
1d2ff18
Follow hlint suggestion: redundant $
philderbeast Sep 29, 2026
11dff02
Silence the legacy warnings in compare mode
philderbeast Sep 29, 2026
36190b2
Reinstate +legacy-comparison
philderbeast Sep 29, 2026
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
4 changes: 3 additions & 1 deletion Cabal-QuickCheck/src/Test/QuickCheck/Instances/Cabal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,10 @@ shortListOf1 bound gen = sized $ \n -> do
k <- choose (1, 1 `max` ((n `div` 2) `min` bound))
vectorOf k gen

-- | The parsec parser reads a single-value field as one token, stopping at a
-- comma, whereas the legacy parser took the rest of the line.
arbitraryShortToken :: Gen String
arbitraryShortToken = arbitraryShortStringWithout "{}[]"
arbitraryShortToken = arbitraryShortStringWithout "{}[],"

arbitraryShortPath :: Gen String
arbitraryShortPath = arbitraryShortStringWithout "{}[],<>:|*?" `suchThat` (not . winDevice)
Expand Down
6 changes: 6 additions & 0 deletions Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ newtype List sep b a = List {_getList :: [a]}
--
-- >>> :t alaList' FSep Token
-- alaList' FSep Token :: [String] -> List FSep Token String
--
-- >>> _getList <$> (eitherParsec "foo bar foo" :: Either String (List FSep Token String))
-- Right ["foo","bar","foo"]
--
-- >>> _getList <$> (eitherParsec "xL{4,IE-,eK<}fE?e" :: Either String (List FSep Token String))
-- Right ["xL{4","IE-","eK<}fE?e"]
alaList :: sep -> [a] -> List sep (Identity a) a
alaList _ = List

Expand Down
23 changes: 21 additions & 2 deletions Cabal/src/Distribution/Simple/Compiler.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ViewPatterns #-}

-- |
-- Module : Distribution.Simple.Compiler
Expand Down Expand Up @@ -357,9 +358,11 @@ intToOptimisationLevel i
-- levels. For compilers that do not the level is just capped to the
-- level they do support.
data DebugInfoLevel
= NoDebugInfo
= -- | The default and disabled level. Disabled by @--disable-debug-info@ or @debug-info: False@.
NoDebugInfo
| MinimalDebugInfo
| NormalDebugInfo
| -- | The enabled level when enabled by @--enable-debug-info@ or @debug-info: True@.
NormalDebugInfo
| MaximalDebugInfo
deriving (Bounded, Enum, Eq, Generic, Read, Show)

Expand All @@ -373,8 +376,24 @@ instance Parsec DebugInfoLevel where
parsecDebugInfoLevel :: CabalParsing m => m DebugInfoLevel
parsecDebugInfoLevel = flagToDebugInfoLevel . pure <$> parsecToken

-- | Converts a string to a 'DebugInfoLevel'. The string can be either a boolean
-- or 0-based integer for that enum. Evaluates to the default of 'NoDebugInfo'
-- when there's no string or when the string is "False". When the string is
-- "True", the level is 'NormalDebugInfo'. These string comparisons are
-- case-insensitive.
--
-- >>> [flagToDebugInfoLevel (Just $ show n) | n <- [0 .. 3]]
-- [NoDebugInfo,MinimalDebugInfo,NormalDebugInfo,MaximalDebugInfo]
--
-- >>> nub $ flagToDebugInfoLevel . Just <$> ["0", "False", "false"]
-- [NoDebugInfo]
--
-- >>> nub $ flagToDebugInfoLevel <$> Nothing : (Just <$> ["2", "True", "true"])
-- [NormalDebugInfo]
flagToDebugInfoLevel :: Maybe String -> DebugInfoLevel
flagToDebugInfoLevel Nothing = NormalDebugInfo
flagToDebugInfoLevel (Just (fmap toLower -> "false")) = NoDebugInfo
flagToDebugInfoLevel (Just (fmap toLower -> "true")) = NormalDebugInfo
flagToDebugInfoLevel (Just s) = case reads s of
[(i, "")]
| i >= fromEnum (minBound :: DebugInfoLevel)
Expand Down
5 changes: 4 additions & 1 deletion cabal-install/cabal-install.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -398,17 +398,20 @@ test-suite parser-tests

type: exitcode-stdio-1.0
main-is: Tests.hs
hs-source-dirs: parser-tests
hs-source-dirs: parser-tests tests
build-depends:
, cabal-install
, Cabal-tree-diff
, containers
, directory
, filepath
, network-uri >= 2.6.2.0 && <2.7
, tasty >= 1.2.3 && <1.6
, tasty-hunit >= 0.10
, tree-diff
other-modules:
Tests.ParserTests
UnitTests.Distribution.Client.TreeDiffInstances

-- Tests to run with a limited stack and heap size
-- The test suite name must be keep short cause a longer one
Expand Down
85 changes: 73 additions & 12 deletions cabal-install/parser-tests/Tests/ParserTests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Distribution.Client.Targets (readUserConstraint)
import Distribution.Client.Types.AllowNewer (AllowNewer (..), AllowOlder (..), RelaxDepMod (..), RelaxDepScope (..), RelaxDepSubject (..), RelaxDeps (..), RelaxedDep (..))
import Distribution.Client.Types.InstallMethod (InstallMethod (..))
import Distribution.Client.Types.OverwritePolicy (OverwritePolicy (..))
import Distribution.Client.Types.Repo (LocalRepo (..), RemoteRepo (..), asPosixPath)
import Distribution.Client.Types.Repo (LocalRepo (..), RemoteRepo (..))
import Distribution.Client.Types.RepoName (RepoName (..))
import Distribution.Client.Types.SourceRepo
import Distribution.Client.Types.WriteGhcEnvironmentFilesPolicy (WriteGhcEnvironmentFilesPolicy (..))
Expand All @@ -48,7 +48,6 @@ import Distribution.Solver.Types.Settings
, ReorderGoals (..)
, StrongFlags (..)
)
import Distribution.System (OS (..), buildOS)
import Distribution.Types.CondTree (CondTree (..))
import Distribution.Types.Flag (mkFlagAssignment)
import Distribution.Types.PackageId (PackageIdentifier (..))
Expand All @@ -62,27 +61,35 @@ import Distribution.Verbosity
import GHC.Stack (HasCallStack)
import Network.URI (parseURI)
import System.Directory (canonicalizePath, doesFileExist)
import System.FilePath ((</>))
import System.FilePath (normalise, (</>))
import Prelude ()

import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, testCase)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)

import Data.TreeDiff (ToExpr, ansiWlEditExpr, ediff)
import UnitTests.Distribution.Client.TreeDiffInstances ()

parserTests :: TestTree
parserTests =
testGroup
"project files parsec tests"
[ testCase "read packages" testPackages
, testCase "read packages glob" testPackagesGlob
, testCase "read packages comma separated" testPackagesCommaSeparated
, testCase "read optional-packages" testOptionalPackages
, testCase "read optional-packages glob" testOptionalPackagesGlob
, testCase "read extra-packages" testExtraPackages
, testCase "read source-repository-package" testSourceRepoList
, testCase "read project-config-build-only" testProjectConfigBuildOnly
, testCase "read project-config-shared" testProjectConfigShared
, testCase "read install-dirs" testInstallDirs
, testCase "read remote-repos" testRemoteRepos
, testCase "read local-no-index-repos" testLocalNoIndexRepos
, testCase "read project-file-parser" testProjectFileParser
, testCase "set explicit provenance" testProjectConfigProvenance
, testCase "read project-config-local-packages" testProjectConfigLocalPackages
, testCase "read project-config-local-packages-empty-string" testProjectConfigLocalPackagesEmptyString
, testCase "read project-config-all-packages" testProjectConfigAllPackages
, testCase "read project-config-specific-packages" testProjectConfigSpecificPackages
, testCase "test projectConfigAllPackages concatenation" testAllPackagesConcat
Expand All @@ -98,16 +105,35 @@ parserTests =

testPackages :: Assertion
testPackages = do
let expected = [".", "packages/packages.cabal"]
let expected = [".", "packages/packages.cabal", "a", "b"]
(config, legacy) <- readConfigDefault "packages"
assertConfigEquals expected config legacy (projectPackages . snd . condTreeData)

testPackagesGlob :: Assertion
testPackagesGlob = do
let expected = ["*/*.cabal", "../{foo,bar}/"]
(config, legacy) <- readConfig "packages" "cabal.glob.project"
assertConfigEquals expected config legacy (projectPackages . snd . condTreeData)

testPackagesCommaSeparated :: Assertion
testPackagesCommaSeparated = do
let expected = ["xL{4,IE-,eK<}fE?e"]
-- let expected = ["xL{4","IE-","eK<}fE?e"]
(config, legacy) <- readConfig "packages" "cabal.comma-separated.project"
assertConfigEquals expected config legacy (projectPackages . snd . condTreeData)

testOptionalPackages :: Assertion
testOptionalPackages = do
let expected = [".", "packages/packages.cabal"]
(config, legacy) <- readConfigDefault "optional-packages"
assertConfigEquals expected config legacy (projectPackagesOptional . snd . condTreeData)

testOptionalPackagesGlob :: Assertion
testOptionalPackagesGlob = do
let expected = ["*/*.cabal", "../{foo,bar}/"]
(config, legacy) <- readConfig "optional-packages" "cabal.glob.project"
assertConfigEquals expected config legacy (projectPackagesOptional . snd . condTreeData)

testSourceRepoList :: Assertion
testSourceRepoList = do
(config, legacy) <- readConfigDefault "source-repository-packages"
Expand Down Expand Up @@ -298,26 +324,31 @@ testRemoteRepos = do
testLocalNoIndexRepos :: Assertion
testLocalNoIndexRepos = do
(config, legacy) <- readConfigDefault "local-no-index-repos"
let actualLocalRepos = (fromNubList . projectConfigLocalNoIndexRepos . projectConfigShared . snd . condTreeData) config
assertBool "Expected LocalNoIndexRepos do not match parsed values" $ compareLists expected actualLocalRepos compareLocalRepos
let localRepos = fromNubList . projectConfigLocalNoIndexRepos . projectConfigShared . snd . condTreeData
assertBool "Expected LocalNoIndexRepos do not match parsed values" $ compareLists expected (localRepos config) compareLocalRepos
assertBool "Expected LocalNoIndexRepos do not match legacy parsed values" $ compareLists expected (localRepos legacy) compareLocalRepos
assertConfigEquals mempty config legacy (projectConfigRemoteRepos . projectConfigShared . snd . condTreeData)
where
expected = [myRepository, mySecureRepository]
myRepository =
LocalRepo
{ localRepoName = RepoName "my-repository"
, localRepoPath = normalisePath "/absolute/path/to/directory"
, localRepoPath = normalise "/absolute/path/to/directory"
, localRepoSharedCache = False
}
mySecureRepository =
LocalRepo
{ localRepoName = RepoName "my-other-repository"
, localRepoPath = normalisePath "/another/path/to/repository"
, localRepoPath = normalise "/another/path/to/repository"
, localRepoSharedCache = False
}
normalisePath path = case buildOS of
Windows -> asPosixPath path
_ -> path

-- | The parser is chosen before the project file is read, so neither parser
-- takes this field from the file.
testProjectFileParser :: Assertion
testProjectFileParser = do
(config, legacy) <- readConfigDefault "project-file-parser"
assertConfigEquals NoFlag config legacy (projectConfigProjectFileParser . projectConfigShared . snd . condTreeData)

testProjectConfigProvenance :: Assertion
testProjectConfigProvenance = do
Expand Down Expand Up @@ -397,6 +428,18 @@ testProjectConfigLocalPackages = do
packageConfigTestTestOptions = [toPathTemplate "--some-option", toPathTemplate "42"]
packageConfigBenchmarkOptions = [toPathTemplate "--some-benchmark-option", toPathTemplate "--another-option"]

-- | The parsers differ on a field with an empty value. The legacy parser
-- passes the empty rest of the line to the option's reader and so sets the
-- field to the empty string. The parsec parser sees a field with no lines and
-- leaves it unset, which is the better behaviour.
testProjectConfigLocalPackagesEmptyString :: Assertion
testProjectConfigLocalPackagesEmptyString = do
(config, legacy) <- readConfigDiverging "project-config-local-packages" "cabal.empty-string.project"
assertEqual "Legacy parser sets the empty string" (toFlag (toPathTemplate "")) (field legacy)
assertEqual "Parsec parser leaves the field unset" NoFlag (field config)
where
field = packageConfigTestHumanLog . projectConfigLocalPackages . snd . condTreeData

testProjectConfigAllPackages :: Assertion
testProjectConfigAllPackages = do
(config, legacy) <- readConfigDefault "project-config-all-packages"
Expand Down Expand Up @@ -563,8 +606,26 @@ verbosity = mkVerbosity defaultVerbosityHandles normal
readConfigDefault :: FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton)
readConfigDefault testSubDir = readConfig testSubDir "cabal.project"

-- | Reads a project file with both parsers and, with the legacy parser as the
-- oracle, checks that the parsec parser agrees with it on the whole config
-- before the caller looks at any one field.
readConfig :: FilePath -> FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton)
readConfig testSubDir projectFileName = do
(parsec, legacy) <- readConfigDiverging testSubDir projectFileName
assertEdiffEqual "Parsec parser disagrees with the legacy parser" legacy parsec
return (parsec, legacy)

-- | Like 'assertEqual' but the failure shows a tree diff of the two values
-- instead of two 'show' dumps.
assertEdiffEqual :: (Eq a, ToExpr a, HasCallStack) => String -> a -> a -> Assertion
assertEdiffEqual msg expected actual =
unless (expected == actual) . assertFailure $
unlines [msg, show (ansiWlEditExpr (ediff expected actual))]

-- | Reads a project file with both parsers without checking that they agree,
-- for the fixtures where they are known to differ.
readConfigDiverging :: FilePath -> FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton)
readConfigDiverging testSubDir projectFileName = do
(TestDir testRootFp projectConfigFp distDirLayout) <- testDirInfo testSubDir projectFileName
exists <- liftIO $ doesFileExist projectConfigFp
assertBool ("projectConfig does not exist: " <> projectConfigFp) exists
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- The glob pattern syntax example from the users guide.
-- https://github.com/haskell/cabal/blob/d8c1b6f68df0a736c32a15ae85db9ffaf002ecb8/doc/cabal-project-description-file.rst?plain=1#L146
optional-packages: */*.cabal ../{foo,bar}/
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- This example was generated for the property test "round trip packages" and
-- resulted in this reported difference:
-- projectPackages = [-"xL{4", -"IE-", -"eK<}fE?e", +"xL{4,IE-,eK<}fE?e"]
-- projectPackages = [-"7{u", -"{h", -"{=n}}}", +"7{u,{h,{=n}}}"]
packages: xL{4,IE-,eK<}fE?e
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- The glob pattern syntax example from the users guide.
-- https://github.com/haskell/cabal/blob/d8c1b6f68df0a736c32a15ae85db9ffaf002ecb8/doc/cabal-project-description-file.rst?plain=1#L146
packages: */*.cabal ../{foo,bar}/
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
packages: . packages/packages.cabal
packages: a,b
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-log:
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
project-file-parser: legacy
9 changes: 6 additions & 3 deletions cabal-install/src/Distribution/Client/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import Distribution.Client.Types
, RemoteRepo (..)
, RepoName (..)
, emptyRemoteRepo
, fileNoIndexURIPath
, isRelaxDeps
, unRepoName
)
Expand Down Expand Up @@ -207,6 +208,7 @@ import Distribution.Simple.Utils
, writeFileAtomic
)
import Distribution.Solver.Types.ConstraintSource
import Distribution.System (buildOS)
import Distribution.Utils.Path (getSymbolicPath, unsafeMakeSymbolicPath)
import Distribution.Verbosity
( normal
Expand All @@ -228,8 +230,7 @@ import System.Directory
, renameFile
)
import System.FilePath
( normalise
, takeDirectory
( takeDirectory
, (</>)
)
import System.IO.Error
Expand Down Expand Up @@ -1689,7 +1690,9 @@ postProcessRepo lineno reponameStr repo0 = do
Left $
LocalRepo
reponame
(normalise (uriPath uri))
-- Native path, so a repository named here and in a project
-- file compares equal and shares one cache key.
(fileNoIndexURIPath buildOS uri)
(uriFragment uri == "#shared-cache")
_ -> do
let repo = repo0{remoteRepoName = reponame}
Expand Down
5 changes: 4 additions & 1 deletion cabal-install/src/Distribution/Client/ProjectConfig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,10 @@ readProjectFileSkeletonCompare verbosity httpTransport distDirLayout key = do
readProjectFileSkeletonGen verbosity httpTransport distDirLayout key $ \fp -> do
debug verbosity "Reading project file using the comparative parser"
(pres, bs) <- parseProjectFileSkeletonParsec verbosity httpTransport distDirLayout key fp
lres <- parseProjectFileSkeletonLegacy verbosity httpTransport distDirLayout key fp
-- The legacy parse is only compared against, so the warnings it would
-- print while parsing, such as those about duplicate or untrimmed
-- imports, are left to the parsec parse or they would be printed twice.
lres <- parseProjectFileSkeletonLegacy (modifyVerbosityFlags (const silent) verbosity) httpTransport distDirLayout key fp
let (_, ppres) = runParseResult pres
case (lres, ppres) of
-- 1. Both succeed, compare the results
Expand Down
Loading
Loading