Skip to content

Commit 59cf957

Browse files
committed
Add Y-forking import test
- A test for detecting when the same config is imported via many different paths - Error on duplicate imports - Do the filtering in duplicateImportMsg - Use duplicateImportMsg for cycles too - Add haddocks to IORef parameter - Add changelog entry - Use ordNub instead of nub - Use NubList - Share implement of duplicate and cyclical messages - Update expectation for non-cyclical duplicate import
1 parent c3a9dd7 commit 59cf957

File tree

4 files changed

+70
-24
lines changed

4 files changed

+70
-24
lines changed

cabal-install-solver/src/Distribution/Solver/Types/ProjectConfigPath.hs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Distribution.Solver.Types.ProjectConfigPath
1212
, docProjectConfigPath
1313
, docProjectConfigPaths
1414
, cyclicalImportMsg
15+
, duplicateImportMsg
1516
, docProjectConfigPathFailReason
1617

1718
-- * Checks and Normalization
@@ -102,13 +103,24 @@ docProjectConfigPaths :: [ProjectConfigPath] -> Doc
102103
docProjectConfigPaths ps = vcat
103104
[ text "-" <+> text p | ProjectConfigPath (p :| _) <- ps ]
104105

105-
-- | A message for a cyclical import, assuming the head of the path is the
106-
-- duplicate.
106+
-- | A message for a cyclical import, a "cyclical import of".
107107
cyclicalImportMsg :: ProjectConfigPath -> Doc
108-
cyclicalImportMsg path@(ProjectConfigPath (duplicate :| _)) =
108+
cyclicalImportMsg path@(ProjectConfigPath (duplicate :| _)) = seenImportMsg "cyclical" duplicate path []
109+
110+
-- | A message for a duplicate import, a "duplicate import of".
111+
duplicateImportMsg :: FilePath -> ProjectConfigPath -> [(FilePath, ProjectConfigPath)] -> Doc
112+
duplicateImportMsg = seenImportMsg "duplicate"
113+
114+
seenImportMsg :: String -> FilePath -> ProjectConfigPath -> [(FilePath, ProjectConfigPath)] -> Doc
115+
seenImportMsg seen duplicate path seenImportsBy =
109116
vcat
110-
[ text "cyclical import of" <+> text duplicate <> semi
117+
[ text seen <+> text "import of" <+> text duplicate <> semi
111118
, nest 2 (docProjectConfigPath path)
119+
, nest 2 $
120+
vcat
121+
[ docProjectConfigPath dib
122+
| (_, dib) <- filter ((duplicate ==) . fst) seenImportsBy
123+
]
112124
]
113125

114126
docProjectConfigPathFailReason :: VR -> ProjectConfigPath -> Doc

cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{-# LANGUAGE ConstraintKinds #-}
22
{-# LANGUAGE DataKinds #-}
33
{-# LANGUAGE DeriveGeneric #-}
4+
{-# LANGUAGE MultiWayIf #-}
45
{-# LANGUAGE NamedFieldPuns #-}
56
{-# LANGUAGE RecordWildCards #-}
67
{-# LANGUAGE ScopedTypeVariables #-}
@@ -33,6 +34,7 @@ module Distribution.Client.ProjectConfig.Legacy
3334
) where
3435

3536
import Data.Coerce (coerce)
37+
import Data.IORef
3638
import Distribution.Client.Compat.Prelude
3739

3840
import Distribution.Types.Flag (FlagName, parsecFlagAssignment)
@@ -137,7 +139,8 @@ import Distribution.Types.CondTree
137139
)
138140
import Distribution.Types.SourceRepo (RepoType)
139141
import Distribution.Utils.NubList
140-
( fromNubList
142+
( NubList
143+
, fromNubList
141144
, overNubList
142145
, toNubList
143146
)
@@ -244,41 +247,51 @@ parseProject
244247
parseProject rootPath cacheDir httpTransport verbosity configToParse = do
245248
let (dir, projectFileName) = splitFileName rootPath
246249
projectDir <- makeAbsolute dir
247-
projectPath <- canonicalizeConfigPath projectDir (ProjectConfigPath $ projectFileName :| [])
248-
parseProjectSkeleton cacheDir httpTransport verbosity projectDir projectPath configToParse
250+
projectPath@(ProjectConfigPath (canonicalRoot :| _)) <- canonicalizeConfigPath projectDir (ProjectConfigPath $ projectFileName :| [])
251+
importsBy <- newIORef $ toNubList [(canonicalRoot, projectPath)]
252+
parseProjectSkeleton cacheDir httpTransport verbosity importsBy projectDir projectPath configToParse
249253

250254
parseProjectSkeleton
251255
:: FilePath
252256
-> HttpTransport
253257
-> Verbosity
258+
-> IORef (NubList (FilePath, ProjectConfigPath))
259+
-- ^ The imports seen so far, useful for reporting on duplicates and to detect duplicates that are not cycles
254260
-> FilePath
255261
-- ^ The directory of the project configuration, typically the directory of cabal.project
256262
-> ProjectConfigPath
257263
-- ^ The path of the file being parsed, either the root or an import
258264
-> ProjectConfigToParse
259265
-- ^ The contents of the file to parse
260266
-> IO (ParseResult ProjectConfigSkeleton)
261-
parseProjectSkeleton cacheDir httpTransport verbosity projectDir source (ProjectConfigToParse bs) =
267+
parseProjectSkeleton cacheDir httpTransport verbosity importsBy projectDir source (ProjectConfigToParse bs) =
262268
(sanityWalkPCS False =<<) <$> liftPR (go []) (ParseUtils.readFields bs)
263269
where
264270
go :: [ParseUtils.Field] -> [ParseUtils.Field] -> IO (ParseResult ProjectConfigSkeleton)
265271
go acc (x : xs) = case x of
266272
(ParseUtils.F _ "import" importLoc) -> do
267273
let importLocPath = importLoc `consProjectConfigPath` source
268274

269-
-- Once we canonicalize the import path, we can check for cyclical imports
270-
normLocPath <- canonicalizeConfigPath projectDir importLocPath
275+
-- Once we canonicalize the import path, we can check for cyclical and duplicate imports
276+
normLocPath@(ProjectConfigPath (uniqueImport :| _)) <- canonicalizeConfigPath projectDir importLocPath
277+
seenImportsBy@(fmap fst -> seenImports) <- fromNubList <$> atomicModifyIORef' importsBy (\ibs -> (toNubList [(uniqueImport, normLocPath)] <> ibs, ibs))
271278

272279
debug verbosity $ "\nimport path, normalized\n=======================\n" ++ render (docProjectConfigPath normLocPath)
273-
274-
if isCyclicConfigPath normLocPath
275-
then pure . parseFail $ ParseUtils.FromString (render $ cyclicalImportMsg normLocPath) Nothing
276-
else do
277-
normSource <- canonicalizeConfigPath projectDir source
278-
let fs = (\z -> CondNode z [normLocPath] mempty) <$> fieldsToConfig normSource (reverse acc)
279-
res <- parseProjectSkeleton cacheDir httpTransport verbosity projectDir importLocPath . ProjectConfigToParse =<< fetchImportConfig normLocPath
280-
rest <- go [] xs
281-
pure . fmap mconcat . sequence $ [fs, res, rest]
280+
debug verbosity "\nseen unique paths\n================="
281+
mapM_ (debug verbosity) seenImports
282+
debug verbosity "\n"
283+
284+
if
285+
| isCyclicConfigPath normLocPath ->
286+
pure . parseFail $ ParseUtils.FromString (render $ cyclicalImportMsg normLocPath) Nothing
287+
| uniqueImport `elem` seenImports -> do
288+
pure . parseFail $ ParseUtils.FromString (render $ duplicateImportMsg uniqueImport normLocPath seenImportsBy) Nothing
289+
| otherwise -> do
290+
normSource <- canonicalizeConfigPath projectDir source
291+
let fs = (\z -> CondNode z [normLocPath] mempty) <$> fieldsToConfig normSource (reverse acc)
292+
res <- parseProjectSkeleton cacheDir httpTransport verbosity importsBy projectDir importLocPath . ProjectConfigToParse =<< fetchImportConfig normLocPath
293+
rest <- go [] xs
294+
pure . fmap mconcat . sequence $ [fs, res, rest]
282295
(ParseUtils.Section l "if" p xs') -> do
283296
subpcs <- go [] xs'
284297
let fs = singletonProjectConfigSkeleton <$> fieldsToConfig source (reverse acc)

cabal-testsuite/PackageTests/ConditionalAndImport/cabal.test.hs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,9 @@ main = cabalTest . withRepo "repo" . recordMode RecordMarked $ do
252252
-- +-- yops-8.config
253253
-- +-- yops/yops-9.config (no further imports)
254254
-- +-- yops/yops-9.config (no further imports)
255-
--
256-
-- We don't check and don't error or warn on the same config being imported
257-
-- via many different paths.
258-
log "checking if we detect when the same config is imported via many different paths (we don't)"
259-
yopping <- cabal' "v2-build" [ "--project-file=yops-0.project" ]
255+
log "checking that we detect when the same config is imported via many different paths"
256+
yopping <- fails $ cabal' "v2-build" [ "--project-file=yops-0.project" ]
257+
assertOutputContains "duplicate import of yops/yops-3.config" yopping
260258

261259
log "checking bad conditional"
262260
badIf <- fails $ cabal' "v2-build" [ "--project-file=bad-conditional.project" ]

changelog.d/pr-9933

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
synopsis: Detect non-cyclical duplicate project imports
2+
description:
3+
Detect and report on duplicate imports that are non-cyclical and expand the
4+
detail in cyclical import reporting, being more explicit and consistent with
5+
non-cyclical duplicate reporting.
6+
7+
```
8+
$ cabal build --project-file=yops-0.project
9+
...
10+
Error: [Cabal-7090]
11+
Error parsing project file yops-0.project:
12+
duplicate import of yops/yops-3.config;
13+
yops/yops-3.config
14+
imported by: yops-0.project
15+
yops/yops-3.config
16+
imported by: yops-2.config
17+
imported by: yops/yops-1.config
18+
imported by: yops-0.project
19+
```
20+
21+
packages: cabal-install-solver cabal-install
22+
prs: #9578 #9933
23+
issues: #9562

0 commit comments

Comments
 (0)