Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add library support for adding modules to components #8

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 2 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as B
import Data.Either (partitionEithers)
import Data.List qualified as L
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty (NonEmpty (..), toList)
import Data.Maybe (catMaybes, isJust)
import Distribution.CabalSpecVersion (CabalSpecVersion)
import Distribution.Client.Add
Expand Down Expand Up @@ -165,7 +165,7 @@ mkInputs isCmpRequired cabalFile origContents args = do
if isCmpRequired
then Left "Target component is required"
else (,) <$> mkCmp Nothing <*> mkDeps args
pure $ Input cabalFile packDescr (Config origContents fields cmp deps)
pure $ Input cabalFile packDescr (Config origContents fields cmp deps BuildDepends)

disambiguateInputs
:: Maybe FilePath
Expand Down
19 changes: 19 additions & 0 deletions cabal-add.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,22 @@ test-suite cabal-add-tests
string-qq,
tasty,
temporary

test-suite cabal-add-unit-tests
type: exitcode-stdio-1.0
main-is: UnitTests.hs
hs-source-dirs: tests/
default-language: GHC2021
ghc-options: -Wall
build-depends:
base <5,
bytestring,
Cabal,
cabal-add,
Diff >=0.4,
directory,
process,
string-qq,
tasty,
temporary

81 changes: 50 additions & 31 deletions src/Distribution/Client/Add.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module Distribution.Client.Add (
Config (..),
executeConfig,
validateChanges,
TargetField (..),
) where

import Control.Applicative ((<|>))
Expand Down Expand Up @@ -85,12 +86,25 @@ data Config = Config
, cnfComponent :: !(Either CommonStanza ComponentName)
-- ^ Which component to update?
-- Usually constructed by 'resolveComponent'.
, cnfDependencies :: !(NonEmpty ByteString)
-- ^ Which dependencies to add?
, cnfAdditions :: !(NonEmpty ByteString)
-- ^ Which content to add to the target field?
-- Usually constructed by 'validateDependency'.
, cnfTargetField :: !TargetField
-- ^ Which field to add the provided content to?
}
deriving (Eq, Show)

data TargetField
= BuildDepends
| ExposedModules
| OtherModules
deriving (Eq, Show, Ord)

getTargetName :: TargetField -> ByteString
getTargetName BuildDepends = "build-depends"
getTargetName ExposedModules = "exposed-modules"
getTargetName OtherModules = "other-modules"

extractComponentNames :: GenericPackageDescription -> Set ComponentName
extractComponentNames GenericPackageDescription {..} =
foldMap (const $ S.singleton $ CLibName LMainLibName) condLibrary
Expand Down Expand Up @@ -390,16 +404,20 @@ getFieldNameAnn = \case
Field (Name ann _) _ -> ann
Section (Name ann _) _ _ -> ann

isBuildDependsField :: Field ann -> Bool
isBuildDependsField = \case
Field (Name _ "build-depends") _ -> True
isFieldWithName :: ByteString -> Field ann -> Bool
isFieldWithName name = \case
Field (Name _ fieldName) _ -> name == fieldName
_ -> False

detectLeadingComma :: ByteString -> Maybe ByteString
detectLeadingComma xs = case B.uncons xs of
detectLeadingSeparator :: ByteString -> Maybe ByteString
detectLeadingSeparator xs = case B.uncons xs of
Just (',', ys) -> Just $ B.cons ',' $ B.takeWhile (== ' ') ys
Just (' ', ys) -> Just $ B.takeWhile (== ' ') ys
_ -> Nothing

isCommaSeparated :: ByteString -> Bool
isCommaSeparated xs = ',' `B.elem` xs

dropRepeatingSpaces :: ByteString -> ByteString
dropRepeatingSpaces xs = case B.uncons xs of
Just (' ', ys) -> B.cons ' ' (B.dropWhile (== ' ') ys)
Expand All @@ -410,10 +428,10 @@ dropRepeatingSpaces xs = case B.uncons xs of
-- to preserve formatting. This often breaks however
-- if there are comments in between build-depends.
fancyAlgorithm :: Config -> Maybe ByteString
fancyAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfDependencies} = do
fancyAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfAdditions, cnfTargetField} = do
component <- L.find (isComponent cnfComponent) cnfFields
Section _ _ subFields <- pure component
buildDependsField <- L.find isBuildDependsField subFields
buildDependsField <- L.find (isFieldWithName $ getTargetName cnfTargetField) subFields
Field _ (FieldLine firstDepPos _dep : restDeps) <- pure buildDependsField

-- This is not really the second dependency:
Expand All @@ -423,14 +441,14 @@ fancyAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfDependencies
_ -> Nothing
fillerPred c = isSpaceChar8 c || c == ','

let (B.takeWhileEnd fillerPred -> pref, B.takeWhile fillerPred -> suff) =
splitAtPosition firstDepPos cnfOrigContents
let (rawPref, rawSuff) = splitAtPosition firstDepPos cnfOrigContents
(B.takeWhileEnd fillerPred -> pref, B.takeWhile fillerPred -> suff) = (rawPref, rawSuff)
prefSuff = pref <> suff

noCommaSeparation = not (isCommaSeparated rawSuff) && (cnfTargetField /= BuildDepends)
(afterLast, inBetween, beforeFirst) = case secondDepPos of
Nothing ->
( if B.any (== ',') prefSuff then pref' else "," <> pref'
, if B.any (== ',') prefSuff then prefSuff' else "," <> prefSuff'
( if B.any (== ',') prefSuff || noCommaSeparation then pref' else "," <> pref'
, if B.any (== ',') prefSuff || noCommaSeparation then prefSuff' else "," <> prefSuff'
, suff
)
where
Expand All @@ -447,38 +465,39 @@ fancyAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfDependencies
splitAtPosition pos cnfOrigContents

let (beforeFirstDep, afterFirstDep) = splitAtPosition firstDepPos cnfOrigContents
newBuildDeps = beforeFirst <> B.intercalate inBetween (NE.toList cnfDependencies) <> afterLast
newContents = beforeFirst <> B.intercalate inBetween (NE.toList cnfAdditions) <> afterLast

let ret = beforeFirstDep <> newBuildDeps <> afterFirstDep
let ret = beforeFirstDep <> newContents <> afterFirstDep
pure ret

-- | Find build-depends section and insert new
-- dependencies at the beginning. Very limited effort
-- is put into preserving formatting.
niceAlgorithm :: Config -> Maybe ByteString
niceAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfDependencies} = do
niceAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfAdditions, cnfTargetField} = do
component <- L.find (isComponent cnfComponent) cnfFields
Section _ _ subFields <- pure component
buildDependsField <- L.find isBuildDependsField subFields
Field _ (FieldLine pos _dep : _) <- pure buildDependsField
targetField <- L.find (isFieldWithName (getTargetName cnfTargetField)) subFields
Field _ (FieldLine pos _dep : _) <- pure targetField

let (before, after) = splitAtPosition pos cnfOrigContents
(_, buildDepsHeader) = splitAtPosition (getFieldNameAnn buildDependsField) before
filler = dropRepeatingSpaces $ B.drop 1 $ B.dropWhile (/= ':') buildDepsHeader
leadingCommaStyle = detectLeadingComma after
filler' = maybe ("," <> filler) (filler <>) leadingCommaStyle
newBuildDeps =
fromMaybe "" leadingCommaStyle
<> B.intercalate filler' (NE.toList cnfDependencies)
<> (if isJust leadingCommaStyle then filler else filler')
(_, targetHeader) = splitAtPosition (getFieldNameAnn targetField) before
leadingSeparatorStyle = detectLeadingSeparator after
filler = dropRepeatingSpaces $ B.drop 1 $ B.dropWhile (/= ':') targetHeader
defaultSep = if isCommaSeparated after || cnfTargetField == BuildDepends then "," else ""
filler' = maybe (defaultSep <> filler) (filler <>) leadingSeparatorStyle
newFieldContents =
fromMaybe "" leadingSeparatorStyle
<> B.intercalate filler' (NE.toList cnfAdditions)
<> (if isJust leadingSeparatorStyle then filler else filler')
pure $
before <> newBuildDeps <> after
before <> newFieldContents <> after

-- | Introduce a new build-depends section
-- after the last common stanza import.
-- This is not fancy, but very robust.
roughAlgorithm :: Config -> Maybe ByteString
roughAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfDependencies} = do
roughAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfAdditions, cnfTargetField} = do
let componentAndRest = L.dropWhile (not . isComponent cnfComponent) cnfFields
pos@(Position _ row) <- findNonImportField componentAndRest
let (before, after) = splitAtPositionLine pos cnfOrigContents
Expand All @@ -488,8 +507,8 @@ roughAlgorithm Config {cnfFields, cnfComponent, cnfOrigContents, cnfDependencies
buildDeps =
(if needsNewlineBefore then lineEnding else "")
<> B.replicate (row - 1) ' '
<> "build-depends: "
<> B.intercalate ", " (NE.toList cnfDependencies)
<> (getTargetName cnfTargetField <> ": ")
<> B.intercalate ", " (NE.toList cnfAdditions)
<> lineEnding
pure $
before <> buildDeps <> after
Expand Down
Loading