Skip to content

Commit 3a42c6d

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 04ab16c commit 3a42c6d

File tree

4 files changed

+101
-20
lines changed

4 files changed

+101
-20
lines changed

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module Distribution.Solver.Types.ProjectConfigPath
1414
, docProjectConfigPath
1515
, docProjectConfigFiles
1616
, cyclicalImportMsg
17+
, duplicateImportMsg
1718
, untrimmedUriImportMsg
1819
, docProjectConfigPathFailReason
1920

@@ -173,11 +174,24 @@ docProjectConfigFiles ps = vcat
173174

174175
-- | A message for a cyclical import, a "cyclical import of".
175176
cyclicalImportMsg :: ProjectConfigPath -> Doc
176-
cyclicalImportMsg path@(ProjectConfigPath (duplicate :| _)) =
177+
cyclicalImportMsg path@(ProjectConfigPath (duplicate :| _)) = seenImportMsg "cyclical" duplicate path []
178+
179+
-- | A message for a duplicate import, a "duplicate import of". If a check for
180+
-- cyclical imports has already been made then this would report a duplicate
181+
-- import by two different paths.
182+
duplicateImportMsg :: FilePath -> ProjectConfigPath -> [(FilePath, ProjectConfigPath)] -> Doc
183+
duplicateImportMsg = seenImportMsg "duplicate"
184+
185+
seenImportMsg :: String -> FilePath -> ProjectConfigPath -> [(FilePath, ProjectConfigPath)] -> Doc
186+
seenImportMsg seen duplicate path seenImportsBy =
177187
vcat
178-
[ text "cyclical import of" <+> text duplicate <> semi
188+
[ text seen <+> text "import of" <+> text duplicate <> semi
179189
, nest 2 (docProjectConfigPath path)
180-
]
190+
, nest 2 $
191+
vcat
192+
[ docProjectConfigPath dib
193+
| (_, dib) <- filter ((duplicate ==) . fst) seenImportsBy
194+
]
181195

182196
-- | A message for an import that has leading or trailing spaces.
183197
untrimmedUriImportMsg :: Doc -> ProjectConfigPath -> Doc

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

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{-# LANGUAGE DataKinds #-}
33
{-# LANGUAGE DeriveGeneric #-}
44
{-# LANGUAGE LambdaCase #-}
5+
{-# LANGUAGE MultiWayIf #-}
56
{-# LANGUAGE NamedFieldPuns #-}
67
{-# LANGUAGE RecordWildCards #-}
78
{-# LANGUAGE ScopedTypeVariables #-}
@@ -35,6 +36,7 @@ module Distribution.Client.ProjectConfig.Legacy
3536
) where
3637

3738
import Data.Coerce (coerce)
39+
import Data.IORef
3840
import Distribution.Client.Compat.Prelude
3941

4042
import Distribution.Types.Flag (FlagName, parsecFlagAssignment)
@@ -142,7 +144,8 @@ import Distribution.Types.CondTree
142144
)
143145
import Distribution.Types.SourceRepo (RepoType)
144146
import Distribution.Utils.NubList
145-
( fromNubList
147+
( NubList
148+
, fromNubList
146149
, overNubList
147150
, toNubList
148151
)
@@ -260,45 +263,55 @@ parseProject rootPath cacheDir httpTransport verbosity configToParse =
260263
do
261264
let (dir, projectFileName) = splitFileName rootPath
262265
projectDir <- makeAbsolute dir
263-
projectPath <- canonicalizeConfigPath projectDir (ProjectConfigPath $ projectFileName :| [])
264-
parseProjectSkeleton cacheDir httpTransport verbosity projectDir projectPath configToParse
266+
projectPath@(ProjectConfigPath (canonicalRoot :| _)) <- canonicalizeConfigPath projectDir (ProjectConfigPath $ projectFileName :| [])
267+
importsBy <- newIORef $ toNubList [(canonicalRoot, projectPath)]
268+
parseProjectSkeleton cacheDir httpTransport verbosity importsBy projectDir projectPath configToParse
265269
-- NOTE: Reverse the warnings so they are in line number order.
266270
<&> \case ProjectParseOk ws x -> ProjectParseOk (reverse ws) x; x -> x
267271

268272
parseProjectSkeleton
269273
:: FilePath
270274
-> HttpTransport
271275
-> Verbosity
276+
-> IORef (NubList (FilePath, ProjectConfigPath))
277+
-- ^ The imports seen so far, used to report on cycles and duplicates and to detect duplicates that are not cycles
272278
-> FilePath
273279
-- ^ The directory of the project configuration, typically the directory of cabal.project
274280
-> ProjectConfigPath
275281
-- ^ The path of the file being parsed, either the root or an import
276282
-> ProjectConfigToParse
277283
-- ^ The contents of the file to parse
278284
-> IO (ProjectParseResult ProjectConfigSkeleton)
279-
parseProjectSkeleton cacheDir httpTransport verbosity projectDir source (ProjectConfigToParse bs) =
285+
parseProjectSkeleton cacheDir httpTransport verbosity importsBy projectDir source (ProjectConfigToParse bs) =
280286
(sanityWalkPCS False =<<) <$> liftPR source (go []) (ParseUtils.readFields bs)
281287
where
282288
go :: [ParseUtils.Field] -> [ParseUtils.Field] -> IO (ProjectParseResult ProjectConfigSkeleton)
283289
go acc (x : xs) = case x of
284290
(ParseUtils.F _ "import" importLoc) -> do
285291
let importLocPath = importLoc `consProjectConfigPath` source
286292

287-
-- Once we canonicalize the import path, we can check for cyclical imports
293+
-- Once we canonicalize the import path, we can check for cyclical and duplicate imports
288294
normSource <- canonicalizeConfigPath projectDir source
289-
normLocPath <- canonicalizeConfigPath projectDir importLocPath
295+
normLocPath@(ProjectConfigPath (uniqueImport :| _)) <- canonicalizeConfigPath projectDir importLocPath
296+
seenImportsBy@(fmap fst -> seenImports) <- fromNubList <$> atomicModifyIORef' importsBy (\ibs -> (toNubList [(uniqueImport, normLocPath)] <> ibs, ibs))
290297
debug verbosity $ "\nimport path, normalized\n=======================\n" ++ render (docProjectConfigPath normLocPath)
291-
292-
if isCyclicConfigPath normLocPath
293-
then pure . projectParseFail Nothing (Just normSource) $ ParseUtils.FromString (render $ cyclicalImportMsg normLocPath) Nothing
294-
else do
295-
when
296-
(isUntrimmedUriConfigPath importLocPath)
297-
(noticeDoc verbosity $ untrimmedUriImportMsg (Disp.text "Warning:") importLocPath)
298-
let fs = (\z -> CondNode z [normLocPath] mempty) <$> fieldsToConfig normSource (reverse acc)
299-
res <- parseProjectSkeleton cacheDir httpTransport verbosity projectDir importLocPath . ProjectConfigToParse =<< fetchImportConfig normLocPath
300-
rest <- go [] xs
301-
pure . fmap mconcat . sequence $ [projectParse Nothing normSource fs, res, rest]
298+
debug verbosity "\nseen unique paths\n================="
299+
mapM_ (debug verbosity) seenImports
300+
debug verbosity "\n"
301+
302+
if
303+
| isCyclicConfigPath normLocPath ->
304+
pure . projectParseFail Nothing (Just normSource) $ ParseUtils.FromString (render $ cyclicalImportMsg normLocPath) Nothing
305+
| uniqueImport `elem` seenImports -> do
306+
pure . parseFail $ ParseUtils.FromString (render $ duplicateImportMsg uniqueImport normLocPath seenImportsBy) Nothing
307+
| otherwise -> do
308+
when
309+
(isUntrimmedUriConfigPath importLocPath)
310+
(noticeDoc verbosity $ untrimmedUriImportMsg (Disp.text "Warning:") importLocPath)
311+
let fs = (\z -> CondNode z [normLocPath] mempty) <$> fieldsToConfig normSource (reverse acc)
312+
res <- parseProjectSkeleton cacheDir httpTransport verbosity importsBy projectDir importLocPath . ProjectConfigToParse =<< fetchImportConfig normLocPath
313+
rest <- go [] xs
314+
pure . fmap mconcat . sequence $ [projectParse Nothing normSource fs, res, rest]
302315
(ParseUtils.Section l "if" p xs') -> do
303316
normSource <- canonicalizeConfigPath projectDir source
304317
subpcs <- go [] xs'

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,37 @@ main = cabalTest . withRepo "repo" . recordMode RecordMarked $ do
165165
log "checking if we detect when the same config is imported via many different paths (we don't)"
166166
yopping <- cabal' "v2-build" [ "--project-file=yops-0.project" ]
167167

168+
-- The project is named yops as it is like hops but with y's for forks.
169+
-- +-- yops-0.project
170+
-- +-- yops/yops-1.config
171+
-- +-- yops-2.config
172+
-- +-- yops/yops-3.config
173+
-- +-- yops-4.config
174+
-- +-- yops/yops-5.config
175+
-- +-- yops-6.config
176+
-- +-- yops/yops-7.config
177+
-- +-- yops-8.config
178+
-- +-- yops/yops-9.config (no further imports)
179+
-- +-- yops/yops-3.config
180+
-- +-- yops-4.config
181+
-- +-- yops/yops-5.config
182+
-- +-- yops-6.config
183+
-- +-- yops/yops-7.config
184+
-- +-- yops-8.config
185+
-- +-- yops/yops-9.config (no further imports)
186+
-- +-- yops/yops-5.config
187+
-- +-- yops-6.config
188+
-- +-- yops/yops-7.config
189+
-- +-- yops-8.config
190+
-- +-- yops/yops-9.config (no further imports)
191+
-- +-- yops/yops-7.config
192+
-- +-- yops-8.config
193+
-- +-- yops/yops-9.config (no further imports)
194+
-- +-- yops/yops-9.config (no further imports)
195+
log "checking that we detect when the same config is imported via many different paths"
196+
yopping <- fails $ cabal' "v2-build" [ "--project-file=yops-0.project" ]
197+
assertOutputContains "duplicate import of yops/yops-3.config" yopping
198+
168199
log "checking bad conditional"
169200
badIf <- fails $ cabal' "v2-build" [ "--project-file=bad-conditional.project" ]
170201
assertOutputContains "Cannot set compiler in a conditional clause of a cabal project file" badIf

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)