Skip to content

Commit 8aad429

Browse files
fendormergify[bot]
andauthored
Fix and improve list parser of cabal init cli (#8663)
* Fix cli list parse errors in `cabal init` Occurrences of `Flag [a]` behave in a slightly unexpected way. The monoid instance of `Flag` is right associative and discard the value on the left. Thus, make sure we merge the contents of the flags, instead of using the monoid instance of `Flag` itself. * Document fixes and improvements Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 6fb48c7 commit 8aad429

File tree

4 files changed

+195
-16
lines changed

4 files changed

+195
-16
lines changed

cabal-install/src/Distribution/Client/Init/Simple.hs

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Distribution.Client.Types.SourcePackageDb (SourcePackageDb(..))
1616
import qualified Data.List.NonEmpty as NEL
1717
import Distribution.Client.Init.Utils (currentDirPkgName, mkPackageNameDep, fixupDocFiles)
1818
import Distribution.Client.Init.Defaults
19-
import Distribution.Simple.Flag (fromFlagOrDefault, flagElim, Flag(..))
19+
import Distribution.Simple.Flag (fromFlagOrDefault, flagElim, Flag (..))
2020
import Distribution.Client.Init.FlagExtractors
2121
import qualified Data.Set as Set
2222
import Distribution.Types.Dependency
@@ -170,6 +170,6 @@ addBaseDepToFlags pkgIx initFlags = case dependencies initFlags of
170170
return $ initFlags
171171
{ dependencies = Flag $ based ++ as
172172
}
173-
_ -> do
173+
NoFlag -> do
174174
based <- dependenciesPrompt pkgIx initFlags
175175
return initFlags { dependencies = Flag based }

cabal-install/src/Distribution/Client/Setup.hs

+35-14
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ import Distribution.Types.PackageVersionConstraint
112112
import Distribution.Types.UnqualComponentName
113113
( unqualComponentNameToPackageName )
114114
import Distribution.PackageDescription
115-
( BuildType(..), RepoKind(..), LibraryName(..) )
115+
( BuildType(..), RepoKind(..), LibraryName(..), Dependency )
116116
import Distribution.System ( Platform )
117117
import Distribution.ReadE
118118
( ReadE(..), succeedReadE, parsecToReadE, parsecToReadEErr, unexpectMsgString )
@@ -127,6 +127,8 @@ import Distribution.Client.GlobalFlags
127127
)
128128
import Distribution.Client.ManpageFlags (ManpageFlags, defaultManpageFlags, manpageOptions)
129129
import Distribution.FieldGrammar.Newtypes (SpecVersion (..))
130+
import Distribution.Parsec
131+
( parsecCommaList )
130132

131133
import Data.List
132134
( deleteFirstsBy )
@@ -2166,12 +2168,14 @@ initOptions _ =
21662168

21672169
, option ['x'] ["extra-source-file"]
21682170
"Extra source file to be distributed with tarball."
2169-
IT.extraSrc (\v flags -> flags { IT.extraSrc = v })
2171+
IT.extraSrc
2172+
(\v flags -> flags { IT.extraSrc = mergeListFlag (IT.extraSrc flags) v })
21702173
(reqArg' "FILE" (Flag . (:[]))
21712174
(fromFlagOrDefault []))
21722175
, option [] ["extra-doc-file"]
21732176
"Extra doc file to be distributed with tarball."
2174-
IT.extraDoc (\v flags -> flags { IT.extraDoc = v })
2177+
IT.extraDoc
2178+
(\v flags -> flags { IT.extraDoc = mergeListFlag (IT.extraDoc flags) v })
21752179
(reqArg' "FILE" (Flag . (:[])) (fromFlagOrDefault []))
21762180

21772181
, option [] ["lib", "is-library"]
@@ -2199,7 +2203,8 @@ initOptions _ =
21992203

22002204
, option [] ["test-dir"]
22012205
"Directory containing tests."
2202-
IT.testDirs (\v flags -> flags { IT.testDirs = v })
2206+
IT.testDirs (\v flags ->
2207+
flags { IT.testDirs = mergeListFlag (IT.testDirs flags) v })
22032208
(reqArg' "DIR" (Flag . (:[]))
22042209
(fromFlagOrDefault []))
22052210

@@ -2226,41 +2231,47 @@ initOptions _ =
22262231
, option ['o'] ["expose-module"]
22272232
"Export a module from the package."
22282233
IT.exposedModules
2229-
(\v flags -> flags { IT.exposedModules = v })
2234+
(\v flags -> flags { IT.exposedModules =
2235+
mergeListFlag (IT.exposedModules flags) v})
22302236
(reqArg "MODULE" (parsecToReadE ("Cannot parse module name: "++)
22312237
(Flag . (:[]) <$> parsec))
22322238
(flagElim [] (fmap prettyShow)))
22332239

22342240
, option [] ["extension"]
22352241
"Use a LANGUAGE extension (in the other-extensions field)."
22362242
IT.otherExts
2237-
(\v flags -> flags { IT.otherExts = v })
2243+
(\v flags -> flags { IT.otherExts =
2244+
mergeListFlag (IT.otherExts flags) v })
22382245
(reqArg "EXTENSION" (parsecToReadE ("Cannot parse extension: "++)
22392246
(Flag . (:[]) <$> parsec))
22402247
(flagElim [] (fmap prettyShow)))
22412248

22422249
, option ['d'] ["dependency"]
2243-
"Package dependency."
2244-
IT.dependencies (\v flags -> flags { IT.dependencies = v })
2245-
(reqArg "PACKAGE" (parsecToReadE ("Cannot parse dependency: "++)
2246-
(Flag . (:[]) <$> parsec))
2247-
(flagElim [] (fmap prettyShow)))
2250+
"Package dependencies. Permits comma separated list of dependencies."
2251+
IT.dependencies
2252+
(\v flags -> flags { IT.dependencies =
2253+
mergeListFlag (IT.dependencies flags) v })
2254+
(reqArg "DEPENDENCIES" (fmap Flag dependenciesReadE)
2255+
(fmap prettyShow . fromFlagOrDefault []))
22482256

22492257
, option [] ["application-dir"]
22502258
"Directory containing package application executable."
2251-
IT.applicationDirs (\v flags -> flags { IT.applicationDirs = v})
2259+
IT.applicationDirs (\v flags -> flags { IT.applicationDirs =
2260+
mergeListFlag (IT.applicationDirs flags) v})
22522261
(reqArg' "DIR" (Flag . (:[]))
22532262
(fromFlagOrDefault []))
22542263

22552264
, option [] ["source-dir", "sourcedir"]
22562265
"Directory containing package library source."
2257-
IT.sourceDirs (\v flags -> flags { IT.sourceDirs = v })
2266+
IT.sourceDirs (\v flags -> flags { IT.sourceDirs =
2267+
mergeListFlag (IT.sourceDirs flags) v })
22582268
(reqArg' "DIR" (Flag. (:[]))
22592269
(fromFlagOrDefault []))
22602270

22612271
, option [] ["build-tool"]
22622272
"Required external build tool."
2263-
IT.buildTools (\v flags -> flags { IT.buildTools = v })
2273+
IT.buildTools (\v flags -> flags { IT.buildTools =
2274+
mergeListFlag (IT.buildTools flags) v })
22642275
(reqArg' "TOOL" (Flag . (:[]))
22652276
(fromFlagOrDefault []))
22662277

@@ -2272,6 +2283,16 @@ initOptions _ =
22722283

22732284
, optionVerbosity IT.initVerbosity (\v flags -> flags { IT.initVerbosity = v })
22742285
]
2286+
where
2287+
dependenciesReadE :: ReadE [Dependency]
2288+
dependenciesReadE =
2289+
parsecToReadE
2290+
("Cannot parse dependencies: " ++)
2291+
(parsecCommaList parsec)
2292+
2293+
mergeListFlag :: Flag [a] -> Flag [a] -> Flag [a]
2294+
mergeListFlag currentFlags v =
2295+
Flag $ concat (flagToList currentFlags ++ flagToList v)
22752296

22762297
-- ------------------------------------------------------------
22772298
-- * Copy and Register

cabal-install/tests/UnitTests/Distribution/Client/Init/NonInteractive.hs

+139
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE LambdaCase #-}
12
module UnitTests.Distribution.Client.Init.NonInteractive
23
( tests
34
) where
@@ -24,6 +25,8 @@ import Data.List (foldl')
2425
import qualified Data.Set as Set
2526
import Distribution.Client.Init.Utils (mkPackageNameDep, mkStringyDep)
2627
import Distribution.FieldGrammar.Newtypes
28+
import Distribution.Simple.Command
29+
import Distribution.Client.Setup (initCommand)
2730

2831
tests
2932
:: Verbosity
@@ -43,6 +46,9 @@ tests _v _initFlags comp pkgIx srcDb =
4346
, testGroup "non-interactive tests"
4447
[ nonInteractiveTests pkgIx srcDb comp
4548
]
49+
, testGroup "cli parser tests"
50+
[ cliListParserTests
51+
]
4652
]
4753

4854
driverFunctionTest
@@ -1265,3 +1271,136 @@ testGo label f g h inputs = testCase label $
12651271
case (_runPrompt $ f emptyFlags) (NEL.fromList inputs) of
12661272
Left x -> g x
12671273
Right x -> h x
1274+
1275+
cliListParserTests :: TestTree
1276+
cliListParserTests = testGroup "cli list parser"
1277+
[ testCase "Single extraSrc" $ do
1278+
flags <- runParserTest ["-x", "Generated.hs"]
1279+
flags @?= emptyFlags
1280+
{ extraSrc = Flag ["Generated.hs"]
1281+
}
1282+
, testCase "Multiple extraSrc" $ do
1283+
flags <- runParserTest ["-x", "Gen1.hs", "-x", "Gen2.hs", "-x", "Gen3.hs"]
1284+
flags @?= emptyFlags
1285+
{ extraSrc = Flag ["Gen1.hs", "Gen2.hs", "Gen3.hs"]
1286+
}
1287+
, testCase "Single extraDoc" $ do
1288+
flags <- runParserTest ["--extra-doc-file", "README"]
1289+
flags @?= emptyFlags
1290+
{ extraDoc = Flag $ ["README"]
1291+
}
1292+
, testCase "Multiple extraDoc" $ do
1293+
flags <- runParserTest ["--extra-doc-file", "README",
1294+
"--extra-doc-file", "CHANGELOG",
1295+
"--extra-doc-file", "LICENSE"]
1296+
flags @?= emptyFlags
1297+
{ extraDoc = Flag $ map fromString ["README", "CHANGELOG", "LICENSE"]
1298+
}
1299+
, testCase "Single exposedModules" $ do
1300+
flags <- runParserTest ["-o", "Test"]
1301+
flags @?= emptyFlags
1302+
{ exposedModules = Flag $ map fromString ["Test"]
1303+
}
1304+
, testCase "Multiple exposedModules" $ do
1305+
flags <- runParserTest ["-o", "Test", "-o", "Test2", "-o", "Test3"]
1306+
flags @?= emptyFlags
1307+
{ exposedModules = Flag $ map fromString ["Test", "Test2", "Test3"]
1308+
}
1309+
-- there is no otherModules cli flag
1310+
-- , testCase "Single otherModules" $ do
1311+
-- flags <- runParserTest ["-o", "Test"]
1312+
-- flags @?= dummyFlags
1313+
-- { otherModules = Flag $ map fromString ["Test"]
1314+
-- }
1315+
-- , testCase "Multiple otherModules" $ do
1316+
-- flags <- runParserTest ["-o", "Test", "-o", "Test2", "-o", "Test3"]
1317+
-- flags @?= dummyFlags
1318+
-- { otherModules = Flag $ map fromString ["Test", "Test2", "Test3"]
1319+
-- }
1320+
, testCase "Single otherExts" $ do
1321+
flags <- runParserTest ["--extension", "OverloadedStrings"]
1322+
flags @?= emptyFlags
1323+
{ otherExts = Flag [EnableExtension OverloadedStrings]
1324+
}
1325+
, testCase "Multiple otherExts" $ do
1326+
flags <- runParserTest ["--extension", "OverloadedStrings",
1327+
"--extension", "FlexibleInstances",
1328+
"--extension", "FlexibleContexts"]
1329+
flags @?= emptyFlags
1330+
{ otherExts = Flag [EnableExtension OverloadedStrings,
1331+
EnableExtension FlexibleInstances,
1332+
EnableExtension FlexibleContexts]
1333+
}
1334+
, testCase "Single dependency" $ do
1335+
flags <- runParserTest ["-d", "base"]
1336+
flags @?= emptyFlags
1337+
{ dependencies = Flag [mkStringyDep "base"]
1338+
}
1339+
, testCase "Multiple dependency flags" $ do
1340+
flags <- runParserTest ["-d", "base", "-d", "vector"]
1341+
flags @?= emptyFlags
1342+
{ dependencies = Flag $ fmap mkStringyDep ["base", "vector"]
1343+
}
1344+
, testCase "Comma separated list of dependencies" $ do
1345+
flags <- runParserTest ["-d", "base,vector"]
1346+
flags @?= emptyFlags
1347+
{ dependencies = Flag $ fmap mkStringyDep ["base", "vector"]
1348+
}
1349+
, testCase "Single applicationDirs" $ do
1350+
flags <- runParserTest ["--application-dir", "app"]
1351+
flags @?= emptyFlags
1352+
{ applicationDirs = Flag ["app"]
1353+
}
1354+
, testCase "Multiple applicationDirs" $ do
1355+
flags <- runParserTest ["--application-dir", "app",
1356+
"--application-dir", "exe",
1357+
"--application-dir", "srcapp"]
1358+
flags @?= emptyFlags
1359+
{ applicationDirs = Flag ["app", "exe", "srcapp"]
1360+
}
1361+
, testCase "Single sourceDirs" $ do
1362+
flags <- runParserTest ["--source-dir", "src"]
1363+
flags @?= emptyFlags
1364+
{ sourceDirs = Flag ["src"]
1365+
}
1366+
, testCase "Multiple sourceDirs" $ do
1367+
flags <- runParserTest ["--source-dir", "src",
1368+
"--source-dir", "lib",
1369+
"--source-dir", "sources"]
1370+
flags @?= emptyFlags
1371+
{ sourceDirs = Flag ["src", "lib", "sources"]
1372+
}
1373+
, testCase "Single buildTools" $ do
1374+
flags <- runParserTest ["--build-tool", "happy"]
1375+
flags @?= emptyFlags
1376+
{ buildTools = Flag ["happy"]
1377+
}
1378+
, testCase "Multiple buildTools" $ do
1379+
flags <- runParserTest ["--build-tool", "happy",
1380+
"--build-tool", "alex",
1381+
"--build-tool", "make"]
1382+
flags @?= emptyFlags
1383+
{ buildTools = Flag ["happy", "alex", "make"]
1384+
}
1385+
, testCase "Single testDirs" $ do
1386+
flags <- runParserTest ["--test-dir", "test"]
1387+
flags @?= emptyFlags
1388+
{ testDirs = Flag ["test"]
1389+
}
1390+
, testCase "Multiple testDirs" $ do
1391+
flags <- runParserTest ["--test-dir", "test",
1392+
"--test-dir", "tests",
1393+
"--test-dir", "testsuite"]
1394+
flags @?= emptyFlags
1395+
{ testDirs = Flag ["test", "tests", "testsuite"]
1396+
}
1397+
]
1398+
where
1399+
assumeAllParse :: CommandParse (InitFlags -> InitFlags, [String]) -> IO InitFlags
1400+
assumeAllParse = \case
1401+
CommandReadyToGo (flagsF, []) -> pure (flagsF emptyFlags)
1402+
_ -> assertFailure "Expected successful parse"
1403+
1404+
runParserTest :: [String] -> IO InitFlags
1405+
runParserTest opts = do
1406+
assumeAllParse $ commandParseArgs initCommand False opts

changelog.d/pr-8663

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
synopsis: Fix and improve list parser of cabal init cli
2+
packages: cabal-install
3+
prs: #8663
4+
issues: #8659
5+
6+
description: {
7+
Occurrences of 'Flag [a]' seem to behave in an unexpected way. The monoid
8+
instance of 'Flag' is right associative and discard the value on the
9+
left, but we want to merge the contents of 'Flag'.
10+
11+
Permits:
12+
- cabal init -d base -d vector -d containers
13+
14+
Fixes for all Flag '[a]' the cli parser in cabal init. Adds cli parser tests.
15+
16+
Adds the feature to specify a comma-separated list of dependencies:
17+
- cabal init -d base,vector,containers
18+
19+
}

0 commit comments

Comments
 (0)