Created
August 31, 2017 08:59
-
-
Save dcoutts/a6823815b251b2baaff21ae3a9490904 to your computer and use it in GitHub Desktop.
TargetSelector with TargetPackageNamed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# LANGUAGE CPP, DeriveGeneric, DeriveFunctor, RecordWildCards #-} | |
----------------------------------------------------------------------------- | |
-- | | |
-- Module : Distribution.Client.TargetSelector | |
-- Copyright : (c) Duncan Coutts 2012, 2015, 2016 | |
-- License : BSD-like | |
-- | |
-- Maintainer : duncan@community.haskell.org | |
-- | |
-- Handling for user-specified target selectors. | |
-- | |
----------------------------------------------------------------------------- | |
module Distribution.Client.TargetSelector ( | |
-- * Target selectors | |
TargetSelector(..), | |
TargetImplicitCwd(..), | |
ComponentKind(..), | |
SubComponentTarget(..), | |
QualLevel(..), | |
componentKind, | |
-- * Reading target selectors | |
readTargetSelectors, | |
TargetSelectorProblem(..), | |
reportTargetSelectorProblems, | |
showTargetSelector, | |
TargetString, | |
showTargetString, | |
parseTargetString, | |
-- ** non-IO | |
readTargetSelectorsWith, | |
DirActions(..), | |
defaultDirActions, | |
) where | |
import Distribution.Package | |
( Package(..), PackageId, packageName ) | |
import Distribution.Types.UnqualComponentName ( unUnqualComponentName ) | |
import Distribution.Client.Types | |
( PackageLocation(..) ) | |
import Distribution.Verbosity | |
import Distribution.PackageDescription | |
( PackageDescription | |
, Executable(..) | |
, TestSuite(..), TestSuiteInterface(..), testModules | |
, Benchmark(..), BenchmarkInterface(..), benchmarkModules | |
, BuildInfo(..), explicitLibModules, exeModules ) | |
import Distribution.PackageDescription.Configuration | |
( flattenPackageDescription ) | |
import Distribution.Solver.Types.SourcePackage | |
( SourcePackage(..) ) | |
import Distribution.ModuleName | |
( ModuleName, toFilePath ) | |
import Distribution.Simple.LocalBuildInfo | |
( Component(..), ComponentName(..) | |
, pkgComponents, componentName, componentBuildInfo ) | |
import Distribution.Types.ForeignLib | |
import Distribution.Text | |
( display, simpleParse ) | |
import Distribution.Simple.Utils | |
( die', lowercase, ordNub ) | |
import Distribution.Client.Utils | |
( makeRelativeCanonical ) | |
import Data.Either | |
( partitionEithers ) | |
import Data.Function | |
( on ) | |
import Data.List | |
( nubBy, stripPrefix, partition, intercalate, sortBy, groupBy ) | |
import Data.Maybe | |
( maybeToList ) | |
import Data.Ord | |
( comparing ) | |
import Distribution.Compat.Binary (Binary) | |
import GHC.Generics (Generic) | |
#if MIN_VERSION_containers(0,5,0) | |
import qualified Data.Map.Lazy as Map.Lazy | |
import qualified Data.Map.Strict as Map | |
import Data.Map.Strict (Map) | |
#else | |
import qualified Data.Map as Map.Lazy | |
import qualified Data.Map as Map | |
import Data.Map (Map) | |
#endif | |
import qualified Data.Set as Set | |
import Control.Arrow ((&&&)) | |
import Control.Monad | |
#if __GLASGOW_HASKELL__ < 710 | |
import Control.Applicative (Applicative(..), (<$>)) | |
#endif | |
import Control.Applicative (Alternative(..)) | |
import qualified Distribution.Compat.ReadP as Parse | |
import Distribution.Compat.ReadP | |
( (+++), (<++) ) | |
import Distribution.ParseUtils | |
( readPToMaybe ) | |
import Data.Char | |
( isSpace, isAlphaNum ) | |
import System.FilePath as FilePath | |
( takeExtension, dropExtension | |
, splitDirectories, joinPath, splitPath ) | |
import qualified System.Directory as IO | |
( doesFileExist, doesDirectoryExist, canonicalizePath | |
, getCurrentDirectory ) | |
import System.FilePath | |
( (</>), (<.>), normalise, dropTrailingPathSeparator ) | |
import Text.EditDistance | |
( defaultEditCosts, restrictedDamerauLevenshteinDistance ) | |
-- ------------------------------------------------------------ | |
-- * Target selector terms | |
-- ------------------------------------------------------------ | |
-- | A target selector is expression selecting a set of components (as targets | |
-- for a actions like @build@, @run@, @test@ etc). A target selector | |
-- corresponds to the user syntax for referring to targets on the command line. | |
-- | |
-- From the users point of view a target can be many things: packages, dirs, | |
-- component names, files etc. Internally we consider a target to be a specific | |
-- component (or module\/file within a component), and all the users' notions | |
-- of targets are just different ways of referring to these component targets. | |
-- | |
-- So target selectors are expressions in the sense that they are interpreted | |
-- to refer to one or more components. For example a 'TargetPackage' gets | |
-- interpreted differently by different commands to refer to all or a subset | |
-- of components within the package. | |
-- | |
-- The syntax has lots of optional parts: | |
-- | |
-- > [ package name | package dir | package .cabal file ] | |
-- > [ [lib:|exe:] component name ] | |
-- > [ module name | source file ] | |
-- | |
data TargetSelector pkg = | |
-- | A package as a whole: the default components for the package or all | |
-- components of a particular kind. | |
-- | |
TargetPackage TargetImplicitCwd pkg (Maybe ComponentKindFilter) | |
-- | All packages, or all components of a particular kind in all packages. | |
-- | |
| TargetAllPackages (Maybe ComponentKindFilter) | |
-- | A specific component in a package. | |
-- | |
| TargetComponent pkg ComponentName SubComponentTarget | |
-- | A named package, but not a known local package. It could for example | |
-- resolve to a dependency of a local package or to a package from | |
-- hackage. Either way, it requires further processing to resolve. | |
-- | |
| TargetPackageName PackageName | |
deriving (Eq, Ord, Functor, Show, Generic) | |
-- | | |
data TargetImplicitCwd = TargetImplicitCwd | TargetExplicitNamed | |
deriving (Eq, Ord, Show, Generic) | |
data ComponentKind = LibKind | FLibKind | ExeKind | TestKind | BenchKind | |
deriving (Eq, Ord, Enum, Show) | |
type ComponentKindFilter = ComponentKind | |
-- | Either the component as a whole or detail about a file or module target | |
-- within a component. | |
-- | |
data SubComponentTarget = | |
-- | The component as a whole | |
WholeComponent | |
-- | A specific module within a component. | |
| ModuleTarget ModuleName | |
-- | A specific file within a component. | |
| FileTarget FilePath | |
deriving (Eq, Ord, Show, Generic) | |
instance Binary SubComponentTarget | |
-- ------------------------------------------------------------ | |
-- * Top level, do everything | |
-- ------------------------------------------------------------ | |
-- | Parse a bunch of command line args as 'TargetSelector's, failing with an | |
-- error if any are unrecognised. The possible target selectors are based on | |
-- the available packages (and their locations). | |
-- | |
readTargetSelectors :: [SourcePackage (PackageLocation a)] | |
-> [String] | |
-> IO (Either [TargetSelectorProblem] | |
[TargetSelector PackageId]) | |
readTargetSelectors = readTargetSelectorsWith defaultDirActions | |
readTargetSelectorsWith :: (Applicative m, Monad m) => DirActions m | |
-> [SourcePackage (PackageLocation a)] | |
-> [String] | |
-> m (Either [TargetSelectorProblem] | |
[TargetSelector PackageId]) | |
readTargetSelectorsWith dirActions@DirActions{..} pkgs targetStrs = | |
case parseTargetStrings targetStrs of | |
([], utargets) -> do | |
utargets' <- mapM (getTargetStringFileStatus dirActions) utargets | |
pkgs' <- mapM (selectPackageInfo dirActions) pkgs | |
cwd <- getCurrentDirectory | |
let (cwdPkg, otherPkgs) = selectCwdPackage cwd pkgs' | |
case resolveTargetSelectors cwdPkg otherPkgs utargets' of | |
([], btargets) -> return (Right (map (fmap packageId) btargets)) | |
(problems, _) -> return (Left problems) | |
(strs, _) -> return (Left (map TargetSelectorUnrecognised strs)) | |
where | |
selectCwdPackage :: FilePath | |
-> [PackageInfo] | |
-> ([PackageInfo], [PackageInfo]) | |
selectCwdPackage cwd pkgs' = | |
let (cwdpkg, others) = partition isPkgDirCwd pkgs' | |
in (cwdpkg, others) | |
where | |
isPkgDirCwd PackageInfo { pinfoDirectory = Just (dir,_) } | |
| dir == cwd = True | |
isPkgDirCwd _ = False | |
data DirActions m = DirActions { | |
doesFileExist :: FilePath -> m Bool, | |
doesDirectoryExist :: FilePath -> m Bool, | |
canonicalizePath :: FilePath -> m FilePath, | |
getCurrentDirectory :: m FilePath | |
} | |
defaultDirActions :: DirActions IO | |
defaultDirActions = | |
DirActions { | |
doesFileExist = IO.doesFileExist, | |
doesDirectoryExist = IO.doesDirectoryExist, | |
-- Workaround for <https://github.com/haskell/directory/issues/63> | |
canonicalizePath = IO.canonicalizePath . dropTrailingPathSeparator, | |
getCurrentDirectory = IO.getCurrentDirectory | |
} | |
makeRelativeToCwd :: Applicative m => DirActions m -> FilePath -> m FilePath | |
makeRelativeToCwd DirActions{..} path = | |
makeRelativeCanonical <$> canonicalizePath path <*> getCurrentDirectory | |
-- ------------------------------------------------------------ | |
-- * Parsing target strings | |
-- ------------------------------------------------------------ | |
-- | The outline parse of a target selector. It takes one of the forms: | |
-- | |
-- > str1 | |
-- > str1:str2 | |
-- > str1:str2:str3 | |
-- > str1:str2:str3:str4 | |
-- | |
data TargetString = | |
TargetString1 String | |
| TargetString2 String String | |
| TargetString3 String String String | |
| TargetString4 String String String String | |
| TargetString5 String String String String String | |
| TargetString7 String String String String String String String | |
deriving (Show, Eq) | |
-- | Parse a bunch of 'TargetString's (purely without throwing exceptions). | |
-- | |
parseTargetStrings :: [String] -> ([String], [TargetString]) | |
parseTargetStrings = | |
partitionEithers | |
. map (\str -> maybe (Left str) Right (parseTargetString str)) | |
parseTargetString :: String -> Maybe TargetString | |
parseTargetString = | |
readPToMaybe parseTargetApprox | |
where | |
parseTargetApprox :: Parse.ReadP r TargetString | |
parseTargetApprox = | |
(do a <- tokenQ | |
return (TargetString1 a)) | |
+++ (do a <- tokenQ0 | |
_ <- Parse.char ':' | |
b <- tokenQ | |
return (TargetString2 a b)) | |
+++ (do a <- tokenQ0 | |
_ <- Parse.char ':' | |
b <- tokenQ | |
_ <- Parse.char ':' | |
c <- tokenQ | |
return (TargetString3 a b c)) | |
+++ (do a <- tokenQ0 | |
_ <- Parse.char ':' | |
b <- token | |
_ <- Parse.char ':' | |
c <- tokenQ | |
_ <- Parse.char ':' | |
d <- tokenQ | |
return (TargetString4 a b c d)) | |
+++ (do a <- tokenQ0 | |
_ <- Parse.char ':' | |
b <- token | |
_ <- Parse.char ':' | |
c <- tokenQ | |
_ <- Parse.char ':' | |
d <- tokenQ | |
_ <- Parse.char ':' | |
e <- tokenQ | |
return (TargetString5 a b c d e)) | |
+++ (do a <- tokenQ0 | |
_ <- Parse.char ':' | |
b <- token | |
_ <- Parse.char ':' | |
c <- tokenQ | |
_ <- Parse.char ':' | |
d <- tokenQ | |
_ <- Parse.char ':' | |
e <- tokenQ | |
_ <- Parse.char ':' | |
f <- tokenQ | |
_ <- Parse.char ':' | |
g <- tokenQ | |
return (TargetString7 a b c d e f g)) | |
token = Parse.munch1 (\x -> not (isSpace x) && x /= ':') | |
tokenQ = parseHaskellString <++ token | |
token0 = Parse.munch (\x -> not (isSpace x) && x /= ':') | |
tokenQ0= parseHaskellString <++ token0 | |
parseHaskellString :: Parse.ReadP r String | |
parseHaskellString = Parse.readS_to_P reads | |
-- | Render a 'TargetString' back as the external syntax. This is mainly for | |
-- error messages. | |
-- | |
showTargetString :: TargetString -> String | |
showTargetString = intercalate ":" . components | |
where | |
components (TargetString1 s1) = [s1] | |
components (TargetString2 s1 s2) = [s1,s2] | |
components (TargetString3 s1 s2 s3) = [s1,s2,s3] | |
components (TargetString4 s1 s2 s3 s4) = [s1,s2,s3,s4] | |
components (TargetString5 s1 s2 s3 s4 s5) = [s1,s2,s3,s4,s5] | |
components (TargetString7 s1 s2 s3 s4 s5 s6 s7) = [s1,s2,s3,s4,s5,s6,s7] | |
showTargetSelector :: Package p => TargetSelector p -> String | |
showTargetSelector ts = | |
let (t':_) = [ t | ql <- [QL1 .. QLFull] | |
, t <- renderTargetSelector ql ts ] | |
in showTargetString (forgetFileStatus t') | |
showTargetSelectorKind :: TargetSelector a -> String | |
showTargetSelectorKind bt = case bt of | |
TargetPackage TargetExplicitNamed _ Nothing -> "package" | |
TargetPackage TargetExplicitNamed _ (Just _) -> "package:filter" | |
TargetPackage TargetImplicitCwd _ Nothing -> "cwd-package" | |
TargetPackage TargetImplicitCwd _ (Just _) -> "cwd-package:filter" | |
TargetAllPackages Nothing -> "all-packages" | |
TargetAllPackages (Just _) -> "all-packages:filter" | |
TargetComponent _ _ WholeComponent -> "component" | |
TargetComponent _ _ ModuleTarget{} -> "module" | |
TargetComponent _ _ FileTarget{} -> "file" | |
TargetPackageName{} -> "package name" | |
-- ------------------------------------------------------------ | |
-- * Checking if targets exist as files | |
-- ------------------------------------------------------------ | |
data TargetStringFileStatus = | |
TargetStringFileStatus1 String FileStatus | |
| TargetStringFileStatus2 String FileStatus String | |
| TargetStringFileStatus3 String FileStatus String String | |
| TargetStringFileStatus4 String String String String | |
| TargetStringFileStatus5 String String String String String | |
| TargetStringFileStatus7 String String String String String String String | |
deriving (Eq, Ord, Show) | |
data FileStatus = FileStatusExistsFile FilePath -- the canonicalised filepath | |
| FileStatusExistsDir FilePath -- the canonicalised filepath | |
| FileStatusNotExists Bool -- does the parent dir exist even? | |
deriving (Eq, Ord, Show) | |
noFileStatus :: FileStatus | |
noFileStatus = FileStatusNotExists False | |
getTargetStringFileStatus :: (Applicative m, Monad m) => DirActions m | |
-> TargetString -> m TargetStringFileStatus | |
getTargetStringFileStatus DirActions{..} t = | |
case t of | |
TargetString1 s1 -> | |
(\f1 -> TargetStringFileStatus1 s1 f1) <$> fileStatus s1 | |
TargetString2 s1 s2 -> | |
(\f1 -> TargetStringFileStatus2 s1 f1 s2) <$> fileStatus s1 | |
TargetString3 s1 s2 s3 -> | |
(\f1 -> TargetStringFileStatus3 s1 f1 s2 s3) <$> fileStatus s1 | |
TargetString4 s1 s2 s3 s4 -> | |
return (TargetStringFileStatus4 s1 s2 s3 s4) | |
TargetString5 s1 s2 s3 s4 s5 -> | |
return (TargetStringFileStatus5 s1 s2 s3 s4 s5) | |
TargetString7 s1 s2 s3 s4 s5 s6 s7 -> | |
return (TargetStringFileStatus7 s1 s2 s3 s4 s5 s6 s7) | |
where | |
fileStatus f = do | |
fexists <- doesFileExist f | |
dexists <- doesDirectoryExist f | |
case splitPath f of | |
_ | fexists -> FileStatusExistsFile <$> canonicalizePath f | |
| dexists -> FileStatusExistsDir <$> canonicalizePath f | |
(d:_) -> FileStatusNotExists <$> doesDirectoryExist d | |
_ -> pure (FileStatusNotExists False) | |
forgetFileStatus :: TargetStringFileStatus -> TargetString | |
forgetFileStatus t = case t of | |
TargetStringFileStatus1 s1 _ -> TargetString1 s1 | |
TargetStringFileStatus2 s1 _ s2 -> TargetString2 s1 s2 | |
TargetStringFileStatus3 s1 _ s2 s3 -> TargetString3 s1 s2 s3 | |
TargetStringFileStatus4 s1 s2 s3 s4 -> TargetString4 s1 s2 s3 s4 | |
TargetStringFileStatus5 s1 s2 s3 s4 | |
s5 -> TargetString5 s1 s2 s3 s4 s5 | |
TargetStringFileStatus7 s1 s2 s3 s4 | |
s5 s6 s7 -> TargetString7 s1 s2 s3 s4 s5 s6 s7 | |
-- ------------------------------------------------------------ | |
-- * Resolving target strings to target selectors | |
-- ------------------------------------------------------------ | |
-- | Given a bunch of user-specified targets, try to resolve what it is they | |
-- refer to. | |
-- | |
resolveTargetSelectors :: [PackageInfo] -- any pkg in the cur dir | |
-> [PackageInfo] -- all the other local packages | |
-> [TargetStringFileStatus] | |
-> ([TargetSelectorProblem], | |
[TargetSelector PackageInfo]) | |
-- default local dir target if there's no given target: | |
resolveTargetSelectors [] [] [] = | |
([TargetSelectorNoTargetsInProject], []) | |
resolveTargetSelectors [] _opinfo [] = | |
([TargetSelectorNoTargetsInCwd], []) | |
resolveTargetSelectors ppinfo _opinfo [] = | |
([], [TargetPackage TargetImplicitCwd (head ppinfo) Nothing]) | |
--TODO: in future allow multiple packages in the same dir | |
resolveTargetSelectors ppinfo opinfo targetStrs = | |
partitionEithers | |
. map (resolveTargetSelector ppinfo opinfo) | |
$ targetStrs | |
resolveTargetSelector :: [PackageInfo] -> [PackageInfo] | |
-> TargetStringFileStatus | |
-> Either TargetSelectorProblem | |
(TargetSelector PackageInfo) | |
resolveTargetSelector ppinfo opinfo targetStrStatus = | |
case findMatch (matcher targetStrStatus) of | |
Unambiguous _ | |
| projectIsEmpty -> Left TargetSelectorNoTargetsInProject | |
Unambiguous (TargetPackage TargetImplicitCwd _ mkfilter) | |
| null ppinfo -> Left (TargetSelectorNoCurrentPackage targetStr) | |
| otherwise -> Right (TargetPackage TargetImplicitCwd | |
(head ppinfo) mkfilter) | |
--TODO: in future allow multiple packages in the same dir | |
Unambiguous target -> Right target | |
None errs | |
| TargetStringFileStatus1 str _ <- targetStrStatus | |
, validPackageName str -> Right (TargetPackageName (mkPackageName str)) | |
| projectIsEmpty -> Left TargetSelectorNoTargetsInProject | |
| otherwise -> Left (classifyMatchErrors errs) | |
Ambiguous exactMatch targets -> | |
case disambiguateTargetSelectors | |
matcher targetStrStatus exactMatch | |
targets of | |
Right targets' -> Left (TargetSelectorAmbiguous targetStr | |
(map (fmap (fmap packageId)) targets')) | |
Left ((m, ms):_) -> Left (MatchingInternalError targetStr | |
(fmap packageId m) | |
(map (fmap (map (fmap packageId))) ms)) | |
Left [] -> internalError "resolveTargetSelector" | |
where | |
matcher = matchTargetSelector ppinfo opinfo | |
targetStr = forgetFileStatus targetStrStatus | |
projectIsEmpty = null ppinfo && null opinfo | |
classifyMatchErrors errs | |
| not (null expected) | |
= let (things, got:_) = unzip expected in | |
TargetSelectorExpected targetStr things got | |
| not (null nosuch) | |
= TargetSelectorNoSuch targetStr nosuch | |
| otherwise | |
= internalError $ "classifyMatchErrors: " ++ show errs | |
where | |
expected = [ (thing, got) | |
| (_, MatchErrorExpected thing got) | |
<- map (innerErr Nothing) errs ] | |
-- Trim the list of alternatives by dropping duplicates and | |
-- retaining only at most three most similar (by edit distance) ones. | |
nosuch = Map.foldrWithKey genResults [] $ Map.fromListWith Set.union $ | |
[ ((inside, thing, got), Set.fromList alts) | |
| (inside, MatchErrorNoSuch thing got alts) | |
<- map (innerErr Nothing) errs | |
] | |
genResults (inside, thing, got) alts acc = ( | |
inside | |
, thing | |
, got | |
, take maxResults | |
$ map fst | |
$ takeWhile distanceLow | |
$ sortBy (comparing snd) | |
$ map addLevDist | |
$ Set.toList alts | |
) : acc | |
where | |
addLevDist = id &&& restrictedDamerauLevenshteinDistance | |
defaultEditCosts got | |
distanceLow (_, dist) = dist < length got `div` 2 | |
maxResults = 3 | |
innerErr _ (MatchErrorIn kind thing m) | |
= innerErr (Just (kind,thing)) m | |
innerErr c m = (c,m) | |
-- | The various ways that trying to resolve a 'TargetString' to a | |
-- 'TargetSelector' can fail. | |
-- | |
data TargetSelectorProblem | |
= TargetSelectorExpected TargetString [String] String | |
-- ^ [expected thing] (actually got) | |
| TargetSelectorNoSuch TargetString | |
[(Maybe (String, String), String, String, [String])] | |
-- ^ [([in thing], no such thing, actually got, alternatives)] | |
| TargetSelectorAmbiguous TargetString | |
[(TargetString, TargetSelector PackageId)] | |
| MatchingInternalError TargetString (TargetSelector PackageId) | |
[(TargetString, [TargetSelector PackageId])] | |
| TargetSelectorUnrecognised String | |
-- ^ Syntax error when trying to parse a target string. | |
| TargetSelectorNoCurrentPackage TargetString | |
| TargetSelectorNoTargetsInCwd | |
| TargetSelectorNoTargetsInProject | |
deriving (Show, Eq) | |
data QualLevel = QL1 | QL2 | QL3 | QLFull | |
deriving (Eq, Enum, Show) | |
disambiguateTargetSelectors | |
:: (TargetStringFileStatus -> Match (TargetSelector PackageInfo)) | |
-> TargetStringFileStatus -> Bool | |
-> [TargetSelector PackageInfo] | |
-> Either [(TargetSelector PackageInfo, | |
[(TargetString, [TargetSelector PackageInfo])])] | |
[(TargetString, TargetSelector PackageInfo)] | |
disambiguateTargetSelectors matcher matchInput exactMatch matchResults = | |
case partitionEithers results of | |
(errs@(_:_), _) -> Left errs | |
([], ok) -> Right ok | |
where | |
-- So, here's the strategy. We take the original match results, and make a | |
-- table of all their renderings at all qualification levels. | |
-- Note there can be multiple renderings at each qualification level. | |
matchResultsRenderings :: [(TargetSelector PackageInfo, | |
[TargetStringFileStatus])] | |
matchResultsRenderings = | |
[ (matchResult, matchRenderings) | |
| matchResult <- matchResults | |
, let matchRenderings = | |
[ rendering | |
| ql <- [QL1 .. QLFull] | |
, rendering <- renderTargetSelector ql matchResult ] | |
] | |
-- Of course the point is that we're looking for renderings that are | |
-- unambiguous matches. So we build another memo table of all the matches | |
-- for all of those renderings. So by looking up in this table we can see | |
-- if we've got an unambiguous match. | |
memoisedMatches :: Map TargetStringFileStatus | |
(Match (TargetSelector PackageInfo)) | |
memoisedMatches = | |
-- avoid recomputing the main one if it was an exact match | |
(if exactMatch then Map.insert matchInput (ExactMatch 0 matchResults) | |
else id) | |
$ Map.Lazy.fromList | |
[ (rendering, matcher rendering) | |
| rendering <- concatMap snd matchResultsRenderings ] | |
-- Finally, for each of the match results, we go through all their | |
-- possible renderings (in order of qualification level, though remember | |
-- there can be multiple renderings per level), and find the first one | |
-- that has an unambiguous match. | |
results :: [Either (TargetSelector PackageInfo, | |
[(TargetString, [TargetSelector PackageInfo])]) | |
(TargetString, TargetSelector PackageInfo)] | |
results = | |
[ case findUnambiguous originalMatch matchRenderings of | |
Just unambiguousRendering -> | |
Right ( forgetFileStatus unambiguousRendering | |
, originalMatch) | |
-- This case is an internal error, but we bubble it up and report it | |
Nothing -> | |
Left ( originalMatch | |
, [ (forgetFileStatus rendering, matches) | |
| rendering <- matchRenderings | |
, let (ExactMatch _ matches) = | |
memoisedMatches Map.! rendering | |
] ) | |
| (originalMatch, matchRenderings) <- matchResultsRenderings ] | |
findUnambiguous :: TargetSelector PackageInfo | |
-> [TargetStringFileStatus] | |
-> Maybe TargetStringFileStatus | |
findUnambiguous _ [] = Nothing | |
findUnambiguous t (r:rs) = | |
case memoisedMatches Map.! r of | |
ExactMatch _ [t'] | fmap packageName t == fmap packageName t' | |
-> Just r | |
ExactMatch _ _ -> findUnambiguous t rs | |
InexactMatch a b -> internalError $ "InexactMatch " ++ show a ++ " " ++ show (map (fmap (display . packageId)) b) | |
NoMatch _ _ -> internalError "NoMatch" | |
internalError :: String -> a | |
internalError msg = | |
error $ "TargetSelector: internal error: " ++ msg | |
-- | Throw an exception with a formatted message if there are any problems. | |
-- | |
reportTargetSelectorProblems :: Verbosity -> [TargetSelectorProblem] -> IO a | |
reportTargetSelectorProblems verbosity problems = do | |
case [ str | TargetSelectorUnrecognised str <- problems ] of | |
[] -> return () | |
targets -> | |
die' verbosity $ unlines | |
[ "Unrecognised target syntax for '" ++ name ++ "'." | |
| name <- targets ] | |
case [ (t, m, ms) | MatchingInternalError t m ms <- problems ] of | |
[] -> return () | |
((target, originalMatch, renderingsAndMatches):_) -> | |
die' verbosity $ "Internal error in target matching. It should always " | |
++ "be possible to find a syntax that's sufficiently qualified to " | |
++ "give an unambiguous match. However when matching '" | |
++ showTargetString target ++ "' we found " | |
++ showTargetSelector originalMatch | |
++ " (" ++ showTargetSelectorKind originalMatch ++ ") which does " | |
++ "not have an unambiguous syntax. The possible syntax and the " | |
++ "targets they match are as follows:\n" | |
++ unlines | |
[ "'" ++ showTargetString rendering ++ "' which matches " | |
++ intercalate ", " | |
[ showTargetSelector match ++ | |
" (" ++ showTargetSelectorKind match ++ ")" | |
| match <- matches ] | |
| (rendering, matches) <- renderingsAndMatches ] | |
case [ (t, e, g) | TargetSelectorExpected t e g <- problems ] of | |
[] -> return () | |
targets -> | |
die' verbosity $ unlines | |
[ "Unrecognised target '" ++ showTargetString target | |
++ "'.\n" | |
++ "Expected a " ++ intercalate " or " expected | |
++ ", rather than '" ++ got ++ "'." | |
| (target, expected, got) <- targets ] | |
case [ (t, e) | TargetSelectorNoSuch t e <- problems ] of | |
[] -> return () | |
targets -> | |
die' verbosity $ unlines | |
[ "Unknown target '" ++ showTargetString target ++ | |
"'.\n" ++ unlines | |
[ (case inside of | |
Just (kind, "") | |
-> "The " ++ kind ++ " has no " | |
Just (kind, thing) | |
-> "The " ++ kind ++ " " ++ thing ++ " has no " | |
Nothing -> "There is no ") | |
++ intercalate " or " [ mungeThing thing ++ " '" ++ got ++ "'" | |
| (thing, got, _alts) <- nosuch' ] ++ "." | |
++ if null alternatives then "" else | |
"\nPerhaps you meant " ++ intercalate ";\nor " | |
[ "the " ++ thing ++ " '" ++ intercalate "' or '" alts ++ "'?" | |
| (thing, alts) <- alternatives ] | |
| (inside, nosuch') <- groupByContainer nosuch | |
, let alternatives = | |
[ (thing, alts) | |
| (thing,_got,alts@(_:_)) <- nosuch' ] | |
] | |
| (target, nosuch) <- targets | |
, let groupByContainer = | |
map (\g@((inside,_,_,_):_) -> | |
(inside, [ (thing,got,alts) | |
| (_,thing,got,alts) <- g ])) | |
. groupBy ((==) `on` (\(x,_,_,_) -> x)) | |
. sortBy (compare `on` (\(x,_,_,_) -> x)) | |
] | |
where | |
mungeThing "file" = "file target" | |
mungeThing thing = thing | |
case [ (t, ts) | TargetSelectorAmbiguous t ts <- problems ] of | |
[] -> return () | |
targets -> | |
die' verbosity $ unlines | |
[ "Ambiguous target '" ++ showTargetString target | |
++ "'. It could be:\n " | |
++ unlines [ " "++ showTargetString ut ++ | |
" (" ++ showTargetSelectorKind bt ++ ")" | |
| (ut, bt) <- amb ] | |
| (target, amb) <- targets ] | |
case [ t | TargetSelectorNoCurrentPackage t <- problems ] of | |
[] -> return () | |
target:_ -> | |
die' verbosity $ | |
"The target '" ++ showTargetString target ++ "' refers to the " | |
++ "components in the package in the current directory, but there " | |
++ "is no package in the current directory (or at least not listed " | |
++ "as part of the project)." | |
--TODO: report a different error if there is a .cabal file but it's | |
-- not a member of the project | |
case [ () | TargetSelectorNoTargetsInCwd <- problems ] of | |
[] -> return () | |
_:_ -> | |
die' verbosity $ | |
"No targets given and there is no package in the current " | |
++ "directory. Use the target 'all' for all packages in the " | |
++ "project or specify packages or components by name or location. " | |
++ "See 'cabal build --help' for more details on target options." | |
case [ () | TargetSelectorNoTargetsInProject <- problems ] of | |
[] -> return () | |
_:_ -> | |
die' verbosity $ | |
"There is no <pkgname>.cabal package file or cabal.project file. " | |
++ "To build packages locally you need at minimum a <pkgname>.cabal " | |
++ "file. You can use 'cabal init' to create one.\n" | |
++ "\n" | |
++ "For non-trivial projects you will also want a cabal.project " | |
++ "file in the root directory of your project. This file lists the " | |
++ "packages in your project and all other build configuration. " | |
++ "See the Cabal user guide for full details." | |
fail "reportTargetSelectorProblems: internal error" | |
---------------------------------- | |
-- Syntax type | |
-- | |
-- | Syntax for the 'TargetSelector': the matcher and renderer | |
-- | |
data Syntax = Syntax QualLevel Matcher Renderer | |
| AmbiguousAlternatives Syntax Syntax | |
| ShadowingAlternatives Syntax Syntax | |
type Matcher = TargetStringFileStatus -> Match (TargetSelector PackageInfo) | |
type Renderer = TargetSelector PackageId -> [TargetStringFileStatus] | |
foldSyntax :: (a -> a -> a) -> (a -> a -> a) | |
-> (QualLevel -> Matcher -> Renderer -> a) | |
-> (Syntax -> a) | |
foldSyntax ambiguous unambiguous syntax = go | |
where | |
go (Syntax ql match render) = syntax ql match render | |
go (AmbiguousAlternatives a b) = ambiguous (go a) (go b) | |
go (ShadowingAlternatives a b) = unambiguous (go a) (go b) | |
---------------------------------- | |
-- Top level renderer and matcher | |
-- | |
renderTargetSelector :: Package p => QualLevel -> TargetSelector p | |
-> [TargetStringFileStatus] | |
renderTargetSelector ql ts = | |
foldSyntax | |
(++) (++) | |
(\ql' _ render -> guard (ql == ql') >> render (fmap packageId ts)) | |
syntax | |
where | |
syntax = syntaxForms [] [] -- don't need pinfo for rendering | |
matchTargetSelector :: [PackageInfo] -> [PackageInfo] | |
-> TargetStringFileStatus | |
-> Match (TargetSelector PackageInfo) | |
matchTargetSelector ppinfo opinfo = \utarget -> | |
nubMatchesBy ((==) `on` (fmap packageName)) $ | |
let ql = targetQualLevel utarget in | |
foldSyntax | |
(<|>) (<//>) | |
(\ql' match _ -> guard (ql == ql') >> match utarget) | |
syntax | |
where | |
syntax = syntaxForms ppinfo opinfo | |
targetQualLevel TargetStringFileStatus1{} = QL1 | |
targetQualLevel TargetStringFileStatus2{} = QL2 | |
targetQualLevel TargetStringFileStatus3{} = QL3 | |
targetQualLevel TargetStringFileStatus4{} = QLFull | |
targetQualLevel TargetStringFileStatus5{} = QLFull | |
targetQualLevel TargetStringFileStatus7{} = QLFull | |
---------------------------------- | |
-- Syntax forms | |
-- | |
-- | All the forms of syntax for 'TargetSelector'. | |
-- | |
syntaxForms :: [PackageInfo] -> [PackageInfo] -> Syntax | |
syntaxForms ppinfo opinfo = | |
-- The various forms of syntax here are ambiguous in many cases. | |
-- Our policy is by default we expose that ambiguity and report | |
-- ambiguous matches. In certain cases we override the ambiguity | |
-- by having some forms shadow others. | |
-- | |
-- We make modules shadow files because module name "Q" clashes | |
-- with file "Q" with no extension but these refer to the same | |
-- thing anyway so it's not a useful ambiguity. Other cases are | |
-- not ambiguous like "Q" vs "Q.hs" or "Data.Q" vs "Data/Q". | |
ambiguousAlternatives | |
-- convenient single-component forms | |
[ shadowingAlternatives | |
[ ambiguousAlternatives | |
[ syntaxForm1All | |
, syntaxForm1Filter | |
, shadowingAlternatives | |
[ syntaxForm1Component pcinfo | |
, syntaxForm1Package pinfo | |
] | |
] | |
, syntaxForm1Component ocinfo | |
, syntaxForm1Module cinfo | |
, syntaxForm1File pinfo | |
-- , syntaxForm1Name | |
] | |
-- two-component partially qualified forms | |
-- fully qualified form for 'all' | |
, syntaxForm2MetaAll | |
, syntaxForm2AllFilter | |
, syntaxForm2NamespacePackage pinfo | |
, syntaxForm2PackageComponent pinfo | |
, syntaxForm2PackageFilter pinfo | |
, syntaxForm2KindComponent cinfo | |
, shadowingAlternatives | |
[ syntaxForm2PackageModule pinfo | |
, syntaxForm2PackageFile pinfo | |
] | |
, shadowingAlternatives | |
[ syntaxForm2ComponentModule cinfo | |
, syntaxForm2ComponentFile cinfo | |
] | |
-- rarely used partially qualified forms | |
, syntaxForm3PackageKindComponent pinfo | |
, shadowingAlternatives | |
[ syntaxForm3PackageComponentModule pinfo | |
, syntaxForm3PackageComponentFile pinfo | |
] | |
, shadowingAlternatives | |
[ syntaxForm3KindComponentModule cinfo | |
, syntaxForm3KindComponentFile cinfo | |
] | |
, syntaxForm3NamespacePackageFilter pinfo | |
-- fully-qualified forms for all and cwd with filter | |
, syntaxForm3MetaAllFilter | |
, syntaxForm3MetaCwdFilter | |
-- fully-qualified form for package and package with filter | |
, syntaxForm3MetaNamespacePackage pinfo | |
, syntaxForm4MetaNamespacePackageFilter pinfo | |
-- fully-qualified forms for component, module and file | |
, syntaxForm5MetaNamespacePackageKindComponent pinfo | |
, syntaxForm7MetaNamespacePackageKindComponentNamespaceModule pinfo | |
, syntaxForm7MetaNamespacePackageKindComponentNamespaceFile pinfo | |
] | |
where | |
ambiguousAlternatives = foldr1 AmbiguousAlternatives | |
shadowingAlternatives = foldr1 ShadowingAlternatives | |
pinfo = ppinfo ++ opinfo | |
cinfo = concatMap pinfoComponents pinfo | |
pcinfo = concatMap pinfoComponents ppinfo | |
ocinfo = concatMap pinfoComponents opinfo | |
-- | Syntax: "all" to select all packages in the project | |
-- | |
-- > cabal build all | |
-- | |
syntaxForm1All :: Syntax | |
syntaxForm1All = | |
syntaxForm1 render $ \str1 _fstatus1 -> do | |
guardMetaAll str1 | |
return (TargetAllPackages Nothing) | |
where | |
render (TargetAllPackages Nothing) = | |
[TargetStringFileStatus1 "all" noFileStatus] | |
render _ = [] | |
-- | Syntax: filter | |
-- | |
-- > cabal build tests | |
-- | |
syntaxForm1Filter :: Syntax | |
syntaxForm1Filter = | |
syntaxForm1 render $ \str1 _fstatus1 -> do | |
kfilter <- matchComponentKindFilter str1 | |
return (TargetPackage TargetImplicitCwd dummyPackageInfo (Just kfilter)) | |
where | |
render (TargetPackage TargetImplicitCwd _ (Just kfilter)) = | |
[TargetStringFileStatus1 (dispF kfilter) noFileStatus] | |
render _ = [] | |
-- Only used for TargetPackage TargetImplicitCwd | |
dummyPackageInfo :: PackageInfo | |
dummyPackageInfo = | |
PackageInfo { | |
pinfoId = PackageIdentifier | |
(mkPackageName "dummyPackageInfo") | |
(mkVersion []), | |
pinfoLocation = unused, | |
pinfoDirectory = unused, | |
pinfoPackageFile = unused, | |
pinfoComponents = unused | |
} | |
where | |
unused = error "dummyPackageInfo" | |
-- | Syntax: package (name, dir or file) | |
-- | |
-- > cabal build foo | |
-- > cabal build ../bar ../bar/bar.cabal | |
-- | |
syntaxForm1Package :: [PackageInfo] -> Syntax | |
syntaxForm1Package pinfo = | |
syntaxForm1 render $ \str1 fstatus1 -> do | |
guardPackage str1 fstatus1 | |
p <- matchPackage pinfo str1 fstatus1 | |
return (TargetPackage p Nothing ExplicitNamedPackage) | |
where | |
render (TargetPackage p Nothing ExplicitNamedPackage) = | |
[TargetStringFileStatus1 (dispP p) noFileStatus] | |
render _ = [] | |
-- | Syntax: component | |
-- | |
-- > cabal build foo | |
-- | |
syntaxForm1Component :: [ComponentInfo] -> Syntax | |
syntaxForm1Component cs = | |
syntaxForm1 render $ \str1 _fstatus1 -> do | |
guardComponentName str1 | |
c <- matchComponentName cs str1 | |
return (TargetComponent (cinfoPackage c) (cinfoName c) WholeComponent) | |
where | |
render (TargetComponent p c WholeComponent) = | |
[TargetStringFileStatus1 (dispC p c) noFileStatus] | |
render _ = [] | |
-- | Syntax: module | |
-- | |
-- > cabal build Data.Foo | |
-- | |
syntaxForm1Module :: [ComponentInfo] -> Syntax | |
syntaxForm1Module cs = | |
syntaxForm1 render $ \str1 _fstatus1 -> do | |
guardModuleName str1 | |
let ms = [ (m,c) | c <- cs, m <- cinfoModules c ] | |
(m,c) <- matchModuleNameAnd ms str1 | |
return (TargetComponent (cinfoPackage c) (cinfoName c) (ModuleTarget m)) | |
where | |
render (TargetComponent _p _c (ModuleTarget m)) = | |
[TargetStringFileStatus1 (dispM m) noFileStatus] | |
render _ = [] | |
-- | Syntax: file name | |
-- | |
-- > cabal build Data/Foo.hs bar/Main.hsc | |
-- | |
syntaxForm1File :: [PackageInfo] -> Syntax | |
syntaxForm1File ps = | |
-- Note there's a bit of an inconsistency here vs the other syntax forms | |
-- for files. For the single-part syntax the target has to point to a file | |
-- that exists (due to our use of matchPackageDirectoryPrefix), whereas for | |
-- all the other forms we don't require that. | |
syntaxForm1 render $ \str1 fstatus1 -> | |
expecting "file" str1 $ do | |
(pkgfile, p) <- matchPackageDirectoryPrefix ps fstatus1 | |
orNoThingIn "package" (display (packageName p)) $ do | |
(filepath, c) <- matchComponentFile (pinfoComponents p) pkgfile | |
return (TargetComponent p (cinfoName c) (FileTarget filepath)) | |
where | |
render (TargetComponent _p _c (FileTarget f)) = | |
[TargetStringFileStatus1 f noFileStatus] | |
render _ = [] | |
syntaxForm1Name :: Syntax | |
syntaxForm1Name = | |
syntaxForm1 render $ \str1 _fstatus1 -> do | |
pn <- matchSomePackageName str1 | |
exactMatches [TargetPackageName pn] | |
where | |
render (TargetPackageName pn) = | |
[TargetStringFileStatus1 (display pn) noFileStatus] | |
render _ = [] | |
--- | |
-- | Syntax: :all | |
-- | |
-- > cabal build :all | |
-- | |
syntaxForm2MetaAll :: Syntax | |
syntaxForm2MetaAll = | |
syntaxForm2 render $ \str1 _fstatus1 str2 -> do | |
guardNamespaceMeta str1 | |
guardMetaAll str2 | |
return (TargetAllPackages Nothing) | |
where | |
render (TargetAllPackages Nothing) = | |
[TargetStringFileStatus2 "" noFileStatus "all"] | |
render _ = [] | |
-- | Syntax: all : filer | |
-- | |
-- > cabal build all:tests | |
-- | |
syntaxForm2AllFilter :: Syntax | |
syntaxForm2AllFilter = | |
syntaxForm2 render $ \str1 _fstatus1 str2 -> do | |
guardMetaAll str1 | |
kfilter <- matchComponentKindFilter str2 | |
return (TargetAllPackages (Just kfilter)) | |
where | |
render (TargetAllPackages (Just kfilter)) = | |
[TargetStringFileStatus2 "all" noFileStatus (dispF kfilter)] | |
render _ = [] | |
-- | Syntax: package : filer | |
-- | |
-- > cabal build foo:tests | |
-- | |
syntaxForm2PackageFilter :: [PackageInfo] -> Syntax | |
syntaxForm2PackageFilter ps = | |
syntaxForm2 render $ \str1 fstatus1 str2 -> do | |
guardPackage str1 fstatus1 | |
p <- matchPackage ps str1 fstatus1 | |
kfilter <- matchComponentKindFilter str2 | |
return (TargetPackage p (Just kfilter) ExplicitNamedPackage) | |
where | |
render (TargetPackage p (Just kfilter) ExplicitNamedPackage) = | |
[TargetStringFileStatus2 (dispP p) noFileStatus (dispF kfilter)] | |
render _ = [] | |
-- | Syntax: pkg : package name | |
-- | |
-- > cabal build pkg:foo | |
-- | |
syntaxForm2NamespacePackage :: [PackageInfo] -> Syntax | |
syntaxForm2NamespacePackage pinfo = | |
syntaxForm2 render $ \str1 _fstatus1 str2 -> do | |
guardNamespacePackage str1 | |
guardPackageName str2 | |
p <- matchPackage pinfo str2 noFileStatus | |
return (TargetPackage p Nothing ExplicitNamedPackage) | |
where | |
render (TargetPackage p Nothing ExplicitNamedPackage) = | |
[TargetStringFileStatus2 "pkg" noFileStatus (dispP p)] | |
render _ = [] | |
-- | Syntax: package : component | |
-- | |
-- > cabal build foo:foo | |
-- > cabal build ./foo:foo | |
-- > cabal build ./foo.cabal:foo | |
-- | |
syntaxForm2PackageComponent :: [PackageInfo] -> Syntax | |
syntaxForm2PackageComponent ps = | |
syntaxForm2 render $ \str1 fstatus1 str2 -> do | |
guardPackage str1 fstatus1 | |
guardComponentName str2 | |
p <- matchPackage ps str1 fstatus1 | |
orNoThingIn "package" (display (packageName p)) $ do | |
c <- matchComponentName (pinfoComponents p) str2 | |
return (TargetComponent p (cinfoName c) WholeComponent) | |
--TODO: the error here ought to say there's no component by that name in | |
-- this package, and name the package | |
where | |
render (TargetComponent p c WholeComponent) = | |
[TargetStringFileStatus2 (dispP p) noFileStatus (dispC p c)] | |
render _ = [] | |
-- | Syntax: namespace : component | |
-- | |
-- > cabal build lib:foo exe:foo | |
-- | |
syntaxForm2KindComponent :: [ComponentInfo] -> Syntax | |
syntaxForm2KindComponent cs = | |
syntaxForm2 render $ \str1 _fstatus1 str2 -> do | |
ckind <- matchComponentKind str1 | |
guardComponentName str2 | |
c <- matchComponentKindAndName cs ckind str2 | |
return (TargetComponent (cinfoPackage c) (cinfoName c) WholeComponent) | |
where | |
render (TargetComponent p c WholeComponent) = | |
[TargetStringFileStatus2 (dispK c) noFileStatus (dispC p c)] | |
render _ = [] | |
-- | Syntax: package : module | |
-- | |
-- > cabal build foo:Data.Foo | |
-- > cabal build ./foo:Data.Foo | |
-- > cabal build ./foo.cabal:Data.Foo | |
-- | |
syntaxForm2PackageModule :: [PackageInfo] -> Syntax | |
syntaxForm2PackageModule ps = | |
syntaxForm2 render $ \str1 fstatus1 str2 -> do | |
guardPackage str1 fstatus1 | |
guardModuleName str2 | |
p <- matchPackage ps str1 fstatus1 | |
orNoThingIn "package" (display (packageName p)) $ do | |
let ms = [ (m,c) | c <- pinfoComponents p, m <- cinfoModules c ] | |
(m,c) <- matchModuleNameAnd ms str2 | |
return (TargetComponent p (cinfoName c) (ModuleTarget m)) | |
where | |
render (TargetComponent p _c (ModuleTarget m)) = | |
[TargetStringFileStatus2 (dispP p) noFileStatus (dispM m)] | |
render _ = [] | |
-- | Syntax: component : module | |
-- | |
-- > cabal build foo:Data.Foo | |
-- | |
syntaxForm2ComponentModule :: [ComponentInfo] -> Syntax | |
syntaxForm2ComponentModule cs = | |
syntaxForm2 render $ \str1 _fstatus1 str2 -> do | |
guardComponentName str1 | |
guardModuleName str2 | |
c <- matchComponentName cs str1 | |
orNoThingIn "component" (cinfoStrName c) $ do | |
let ms = cinfoModules c | |
m <- matchModuleName ms str2 | |
return (TargetComponent (cinfoPackage c) (cinfoName c) | |
(ModuleTarget m)) | |
where | |
render (TargetComponent p c (ModuleTarget m)) = | |
[TargetStringFileStatus2 (dispC p c) noFileStatus (dispM m)] | |
render _ = [] | |
-- | Syntax: package : filename | |
-- | |
-- > cabal build foo:Data/Foo.hs | |
-- > cabal build ./foo:Data/Foo.hs | |
-- > cabal build ./foo.cabal:Data/Foo.hs | |
-- | |
syntaxForm2PackageFile :: [PackageInfo] -> Syntax | |
syntaxForm2PackageFile ps = | |
syntaxForm2 render $ \str1 fstatus1 str2 -> do | |
guardPackage str1 fstatus1 | |
p <- matchPackage ps str1 fstatus1 | |
orNoThingIn "package" (display (packageName p)) $ do | |
(filepath, c) <- matchComponentFile (pinfoComponents p) str2 | |
return (TargetComponent p (cinfoName c) (FileTarget filepath)) | |
where | |
render (TargetComponent p _c (FileTarget f)) = | |
[TargetStringFileStatus2 (dispP p) noFileStatus f] | |
render _ = [] | |
-- | Syntax: component : filename | |
-- | |
-- > cabal build foo:Data/Foo.hs | |
-- | |
syntaxForm2ComponentFile :: [ComponentInfo] -> Syntax | |
syntaxForm2ComponentFile cs = | |
syntaxForm2 render $ \str1 _fstatus1 str2 -> do | |
guardComponentName str1 | |
c <- matchComponentName cs str1 | |
orNoThingIn "component" (cinfoStrName c) $ do | |
(filepath, _) <- matchComponentFile [c] str2 | |
return (TargetComponent (cinfoPackage c) (cinfoName c) | |
(FileTarget filepath)) | |
where | |
render (TargetComponent p c (FileTarget f)) = | |
[TargetStringFileStatus2 (dispC p c) noFileStatus f] | |
render _ = [] | |
--- | |
-- | Syntax: :all : filter | |
-- | |
-- > cabal build :all:tests | |
-- | |
syntaxForm3MetaAllFilter :: Syntax | |
syntaxForm3MetaAllFilter = | |
syntaxForm3 render $ \str1 _fstatus1 str2 str3 -> do | |
guardNamespaceMeta str1 | |
guardMetaAll str2 | |
kfilter <- matchComponentKindFilter str3 | |
return (TargetAllPackages (Just kfilter)) | |
where | |
render (TargetAllPackages (Just kfilter)) = | |
[TargetStringFileStatus3 "" noFileStatus "all" (dispF kfilter)] | |
render _ = [] | |
syntaxForm3MetaCwdFilter :: Syntax | |
syntaxForm3MetaCwdFilter = | |
syntaxForm3 render $ \str1 _fstatus1 str2 str3 -> do | |
guardNamespaceMeta str1 | |
guardNamespaceCwd str2 | |
kfilter <- matchComponentKindFilter str3 | |
return (TargetPackage TargetImplicitCwd dummyPackageInfo (Just kfilter)) | |
where | |
render (TargetPackage TargetImplicitCwd _ (Just kfilter)) = | |
[TargetStringFileStatus3 "" noFileStatus "cwd" (dispF kfilter)] | |
render _ = [] | |
-- | Syntax: :pkg : package name | |
-- | |
-- > cabal build :pkg:foo | |
-- | |
syntaxForm3MetaNamespacePackage :: [PackageInfo] -> Syntax | |
syntaxForm3MetaNamespacePackage pinfo = | |
syntaxForm3 render $ \str1 _fstatus1 str2 str3 -> do | |
guardNamespaceMeta str1 | |
guardNamespacePackage str2 | |
guardPackageName str3 | |
p <- matchPackage pinfo str3 noFileStatus | |
return (TargetPackage p Nothing ExplicitNamedPackage) | |
where | |
render (TargetPackage p Nothing ExplicitNamedPackage) = | |
[TargetStringFileStatus3 "" noFileStatus "pkg" (dispP p)] | |
render _ = [] | |
-- | Syntax: package : namespace : component | |
-- | |
-- > cabal build foo:lib:foo | |
-- > cabal build foo/:lib:foo | |
-- > cabal build foo.cabal:lib:foo | |
-- | |
syntaxForm3PackageKindComponent :: [PackageInfo] -> Syntax | |
syntaxForm3PackageKindComponent ps = | |
syntaxForm3 render $ \str1 fstatus1 str2 str3 -> do | |
guardPackage str1 fstatus1 | |
ckind <- matchComponentKind str2 | |
guardComponentName str3 | |
p <- matchPackage ps str1 fstatus1 | |
orNoThingIn "package" (display (packageName p)) $ do | |
c <- matchComponentKindAndName (pinfoComponents p) ckind str3 | |
return (TargetComponent p (cinfoName c) WholeComponent) | |
where | |
render (TargetComponent p c WholeComponent) = | |
[TargetStringFileStatus3 (dispP p) noFileStatus (dispK c) (dispC p c)] | |
render _ = [] | |
-- | Syntax: package : component : module | |
-- | |
-- > cabal build foo:foo:Data.Foo | |
-- > cabal build foo/:foo:Data.Foo | |
-- > cabal build foo.cabal:foo:Data.Foo | |
-- | |
syntaxForm3PackageComponentModule :: [PackageInfo] -> Syntax | |
syntaxForm3PackageComponentModule ps = | |
syntaxForm3 render $ \str1 fstatus1 str2 str3 -> do | |
guardPackage str1 fstatus1 | |
guardComponentName str2 | |
guardModuleName str3 | |
p <- matchPackage ps str1 fstatus1 | |
orNoThingIn "package" (display (packageName p)) $ do | |
c <- matchComponentName (pinfoComponents p) str2 | |
orNoThingIn "component" (cinfoStrName c) $ do | |
let ms = cinfoModules c | |
m <- matchModuleName ms str3 | |
return (TargetComponent p (cinfoName c) (ModuleTarget m)) | |
where | |
render (TargetComponent p c (ModuleTarget m)) = | |
[TargetStringFileStatus3 (dispP p) noFileStatus (dispC p c) (dispM m)] | |
render _ = [] | |
-- | Syntax: namespace : component : module | |
-- | |
-- > cabal build lib:foo:Data.Foo | |
-- | |
syntaxForm3KindComponentModule :: [ComponentInfo] -> Syntax | |
syntaxForm3KindComponentModule cs = | |
syntaxForm3 render $ \str1 _fstatus1 str2 str3 -> do | |
ckind <- matchComponentKind str1 | |
guardComponentName str2 | |
guardModuleName str3 | |
c <- matchComponentKindAndName cs ckind str2 | |
orNoThingIn "component" (cinfoStrName c) $ do | |
let ms = cinfoModules c | |
m <- matchModuleName ms str3 | |
return (TargetComponent (cinfoPackage c) (cinfoName c) | |
(ModuleTarget m)) | |
where | |
render (TargetComponent p c (ModuleTarget m)) = | |
[TargetStringFileStatus3 (dispK c) noFileStatus (dispC p c) (dispM m)] | |
render _ = [] | |
-- | Syntax: package : component : filename | |
-- | |
-- > cabal build foo:foo:Data/Foo.hs | |
-- > cabal build foo/:foo:Data/Foo.hs | |
-- > cabal build foo.cabal:foo:Data/Foo.hs | |
-- | |
syntaxForm3PackageComponentFile :: [PackageInfo] -> Syntax | |
syntaxForm3PackageComponentFile ps = | |
syntaxForm3 render $ \str1 fstatus1 str2 str3 -> do | |
guardPackage str1 fstatus1 | |
guardComponentName str2 | |
p <- matchPackage ps str1 fstatus1 | |
orNoThingIn "package" (display (packageName p)) $ do | |
c <- matchComponentName (pinfoComponents p) str2 | |
orNoThingIn "component" (cinfoStrName c) $ do | |
(filepath, _) <- matchComponentFile [c] str3 | |
return (TargetComponent p (cinfoName c) (FileTarget filepath)) | |
where | |
render (TargetComponent p c (FileTarget f)) = | |
[TargetStringFileStatus3 (dispP p) noFileStatus (dispC p c) f] | |
render _ = [] | |
-- | Syntax: namespace : component : filename | |
-- | |
-- > cabal build lib:foo:Data/Foo.hs | |
-- | |
syntaxForm3KindComponentFile :: [ComponentInfo] -> Syntax | |
syntaxForm3KindComponentFile cs = | |
syntaxForm3 render $ \str1 _fstatus1 str2 str3 -> do | |
ckind <- matchComponentKind str1 | |
guardComponentName str2 | |
c <- matchComponentKindAndName cs ckind str2 | |
orNoThingIn "component" (cinfoStrName c) $ do | |
(filepath, _) <- matchComponentFile [c] str3 | |
return (TargetComponent (cinfoPackage c) (cinfoName c) | |
(FileTarget filepath)) | |
where | |
render (TargetComponent p c (FileTarget f)) = | |
[TargetStringFileStatus3 (dispK c) noFileStatus (dispC p c) f] | |
render _ = [] | |
syntaxForm3NamespacePackageFilter :: [PackageInfo] -> Syntax | |
syntaxForm3NamespacePackageFilter ps = | |
syntaxForm3 render $ \str1 _fstatus1 str2 str3 -> do | |
guardNamespacePackage str1 | |
guardPackageName str2 | |
p <- matchPackage ps str2 noFileStatus | |
kfilter <- matchComponentKindFilter str3 | |
return (TargetPackage p (Just kfilter) ExplicitNamedPackage) | |
where | |
render (TargetPackage p (Just kfilter) ExplicitNamedPackage) = | |
[TargetStringFileStatus3 "pkg" noFileStatus (dispP p) (dispF kfilter)] | |
render _ = [] | |
-- | |
syntaxForm4MetaNamespacePackageFilter :: [PackageInfo] -> Syntax | |
syntaxForm4MetaNamespacePackageFilter ps = | |
syntaxForm4 render $ \str1 str2 str3 str4 -> do | |
guardNamespaceMeta str1 | |
guardNamespacePackage str2 | |
guardPackageName str3 | |
p <- matchPackage ps str3 noFileStatus | |
kfilter <- matchComponentKindFilter str4 | |
return (TargetPackage p (Just kfilter) ExplicitNamedPackage) | |
where | |
render (TargetPackage p (Just kfilter) ExplicitNamedPackage) = | |
[TargetStringFileStatus4 "" "pkg" (dispP p) (dispF kfilter)] | |
render _ = [] | |
-- | Syntax: :pkg : package : namespace : component | |
-- | |
-- > cabal build :pkg:foo:lib:foo | |
-- | |
syntaxForm5MetaNamespacePackageKindComponent :: [PackageInfo] -> Syntax | |
syntaxForm5MetaNamespacePackageKindComponent ps = | |
syntaxForm5 render $ \str1 str2 str3 str4 str5 -> do | |
guardNamespaceMeta str1 | |
guardNamespacePackage str2 | |
guardPackageName str3 | |
ckind <- matchComponentKind str4 | |
guardComponentName str5 | |
p <- matchPackage ps str3 noFileStatus | |
orNoThingIn "package" (display (packageName p)) $ do | |
c <- matchComponentKindAndName (pinfoComponents p) ckind str5 | |
return (TargetComponent p (cinfoName c) WholeComponent) | |
where | |
render (TargetComponent p c WholeComponent) = | |
[TargetStringFileStatus5 "" "pkg" (dispP p) (dispK c) (dispC p c)] | |
render _ = [] | |
-- | Syntax: :pkg : package : namespace : component : module : module | |
-- | |
-- > cabal build :pkg:foo:lib:foo:module:Data.Foo | |
-- | |
syntaxForm7MetaNamespacePackageKindComponentNamespaceModule | |
:: [PackageInfo] -> Syntax | |
syntaxForm7MetaNamespacePackageKindComponentNamespaceModule ps = | |
syntaxForm7 render $ \str1 str2 str3 str4 str5 str6 str7 -> do | |
guardNamespaceMeta str1 | |
guardNamespacePackage str2 | |
guardPackageName str3 | |
ckind <- matchComponentKind str4 | |
guardComponentName str5 | |
guardNamespaceModule str6 | |
p <- matchPackage ps str3 noFileStatus | |
orNoThingIn "package" (display (packageName p)) $ do | |
c <- matchComponentKindAndName (pinfoComponents p) ckind str5 | |
orNoThingIn "component" (cinfoStrName c) $ do | |
let ms = cinfoModules c | |
m <- matchModuleName ms str7 | |
return (TargetComponent p (cinfoName c) (ModuleTarget m)) | |
where | |
render (TargetComponent p c (ModuleTarget m)) = | |
[TargetStringFileStatus7 "" "pkg" (dispP p) | |
(dispK c) (dispC p c) | |
"module" (dispM m)] | |
render _ = [] | |
-- | Syntax: :pkg : package : namespace : component : file : filename | |
-- | |
-- > cabal build :pkg:foo:lib:foo:file:Data/Foo.hs | |
-- | |
syntaxForm7MetaNamespacePackageKindComponentNamespaceFile | |
:: [PackageInfo] -> Syntax | |
syntaxForm7MetaNamespacePackageKindComponentNamespaceFile ps = | |
syntaxForm7 render $ \str1 str2 str3 str4 str5 str6 str7 -> do | |
guardNamespaceMeta str1 | |
guardNamespacePackage str2 | |
guardPackageName str3 | |
ckind <- matchComponentKind str4 | |
guardComponentName str5 | |
guardNamespaceFile str6 | |
p <- matchPackage ps str3 noFileStatus | |
orNoThingIn "package" (display (packageName p)) $ do | |
c <- matchComponentKindAndName (pinfoComponents p) ckind str5 | |
orNoThingIn "component" (cinfoStrName c) $ do | |
(filepath,_) <- matchComponentFile [c] str7 | |
return (TargetComponent p (cinfoName c) (FileTarget filepath)) | |
where | |
render (TargetComponent p c (FileTarget f)) = | |
[TargetStringFileStatus7 "" "pkg" (dispP p) | |
(dispK c) (dispC p c) | |
"file" f] | |
render _ = [] | |
--------------------------------------- | |
-- Syntax utils | |
-- | |
type Match1 = String -> FileStatus -> Match (TargetSelector PackageInfo) | |
type Match2 = String -> FileStatus -> String | |
-> Match (TargetSelector PackageInfo) | |
type Match3 = String -> FileStatus -> String -> String | |
-> Match (TargetSelector PackageInfo) | |
type Match4 = String -> String -> String -> String | |
-> Match (TargetSelector PackageInfo) | |
type Match5 = String -> String -> String -> String -> String | |
-> Match (TargetSelector PackageInfo) | |
type Match7 = String -> String -> String -> String -> String -> String -> String | |
-> Match (TargetSelector PackageInfo) | |
syntaxForm1 :: Renderer -> Match1 -> Syntax | |
syntaxForm2 :: Renderer -> Match2 -> Syntax | |
syntaxForm3 :: Renderer -> Match3 -> Syntax | |
syntaxForm4 :: Renderer -> Match4 -> Syntax | |
syntaxForm5 :: Renderer -> Match5 -> Syntax | |
syntaxForm7 :: Renderer -> Match7 -> Syntax | |
syntaxForm1 render f = | |
Syntax QL1 match render | |
where | |
match = \(TargetStringFileStatus1 str1 fstatus1) -> | |
f str1 fstatus1 | |
syntaxForm2 render f = | |
Syntax QL2 match render | |
where | |
match = \(TargetStringFileStatus2 str1 fstatus1 str2) -> | |
f str1 fstatus1 str2 | |
syntaxForm3 render f = | |
Syntax QL3 match render | |
where | |
match = \(TargetStringFileStatus3 str1 fstatus1 str2 str3) -> | |
f str1 fstatus1 str2 str3 | |
syntaxForm4 render f = | |
Syntax QLFull match render | |
where | |
match (TargetStringFileStatus4 str1 str2 str3 str4) | |
= f str1 str2 str3 str4 | |
match _ = mzero | |
syntaxForm5 render f = | |
Syntax QLFull match render | |
where | |
match (TargetStringFileStatus5 str1 str2 str3 str4 str5) | |
= f str1 str2 str3 str4 str5 | |
match _ = mzero | |
syntaxForm7 render f = | |
Syntax QLFull match render | |
where | |
match (TargetStringFileStatus7 str1 str2 str3 str4 str5 str6 str7) | |
= f str1 str2 str3 str4 str5 str6 str7 | |
match _ = mzero | |
dispP :: Package p => p -> String | |
dispP = display . packageName | |
dispC :: Package p => p -> ComponentName -> String | |
dispC = componentStringName | |
dispK :: ComponentName -> String | |
dispK = showComponentKindShort . componentKind | |
dispF :: ComponentKind -> String | |
dispF = showComponentKindFilterShort | |
dispM :: ModuleName -> String | |
dispM = display | |
------------------------------- | |
-- Package and component info | |
-- | |
data PackageInfo = PackageInfo { | |
pinfoId :: PackageId, | |
pinfoLocation :: PackageLocation (), | |
pinfoDirectory :: Maybe (FilePath, FilePath), | |
pinfoPackageFile :: Maybe (FilePath, FilePath), | |
pinfoComponents :: [ComponentInfo] | |
} | |
-- not instance of Show due to recursive construction | |
data ComponentInfo = ComponentInfo { | |
cinfoName :: ComponentName, | |
cinfoStrName :: ComponentStringName, | |
cinfoPackage :: PackageInfo, | |
cinfoSrcDirs :: [FilePath], | |
cinfoModules :: [ModuleName], | |
cinfoHsFiles :: [FilePath], -- other hs files (like main.hs) | |
cinfoCFiles :: [FilePath], | |
cinfoJsFiles :: [FilePath] | |
} | |
-- not instance of Show due to recursive construction | |
type ComponentStringName = String | |
instance Package PackageInfo where | |
packageId = pinfoId | |
selectPackageInfo :: (Applicative m, Monad m) => DirActions m | |
-> SourcePackage (PackageLocation a) -> m PackageInfo | |
selectPackageInfo dirActions@DirActions{..} | |
SourcePackage { | |
packageDescription = pkg, | |
packageSource = loc | |
} = do | |
(pkgdir, pkgfile) <- | |
case loc of | |
--TODO: local tarballs, remote tarballs etc | |
LocalUnpackedPackage dir -> do | |
dirabs <- canonicalizePath dir | |
dirrel <- makeRelativeToCwd dirActions dirabs | |
--TODO: ought to get this earlier in project reading | |
let fileabs = dirabs </> display (packageName pkg) <.> "cabal" | |
filerel = dirrel </> display (packageName pkg) <.> "cabal" | |
exists <- doesFileExist fileabs | |
return ( Just (dirabs, dirrel) | |
, if exists then Just (fileabs, filerel) else Nothing | |
) | |
_ -> return (Nothing, Nothing) | |
let pinfo = | |
PackageInfo { | |
pinfoId = packageId pkg, | |
pinfoLocation = fmap (const ()) loc, | |
pinfoDirectory = pkgdir, | |
pinfoPackageFile = pkgfile, | |
pinfoComponents = selectComponentInfo pinfo | |
(flattenPackageDescription pkg) | |
} | |
return pinfo | |
selectComponentInfo :: PackageInfo -> PackageDescription -> [ComponentInfo] | |
selectComponentInfo pinfo pkg = | |
[ ComponentInfo { | |
cinfoName = componentName c, | |
cinfoStrName = componentStringName pkg (componentName c), | |
cinfoPackage = pinfo, | |
cinfoSrcDirs = ordNub (hsSourceDirs bi), | |
-- [ pkgroot </> srcdir | |
-- | (pkgroot,_) <- maybeToList (pinfoDirectory pinfo) | |
-- , srcdir <- hsSourceDirs bi ], | |
cinfoModules = ordNub (componentModules c), | |
cinfoHsFiles = ordNub (componentHsFiles c), | |
cinfoCFiles = ordNub (cSources bi), | |
cinfoJsFiles = ordNub (jsSources bi) | |
} | |
| c <- pkgComponents pkg | |
, let bi = componentBuildInfo c ] | |
componentStringName :: Package pkg => pkg -> ComponentName -> ComponentStringName | |
componentStringName pkg CLibName = display (packageName pkg) | |
componentStringName _ (CSubLibName name) = unUnqualComponentName name | |
componentStringName _ (CFLibName name) = unUnqualComponentName name | |
componentStringName _ (CExeName name) = unUnqualComponentName name | |
componentStringName _ (CTestName name) = unUnqualComponentName name | |
componentStringName _ (CBenchName name) = unUnqualComponentName name | |
componentModules :: Component -> [ModuleName] | |
-- I think it's unlikely users will ask to build a requirement | |
-- which is not mentioned locally. | |
componentModules (CLib lib) = explicitLibModules lib | |
componentModules (CFLib flib) = foreignLibModules flib | |
componentModules (CExe exe) = exeModules exe | |
componentModules (CTest test) = testModules test | |
componentModules (CBench bench) = benchmarkModules bench | |
componentHsFiles :: Component -> [FilePath] | |
componentHsFiles (CExe exe) = [modulePath exe] | |
componentHsFiles (CTest TestSuite { | |
testInterface = TestSuiteExeV10 _ mainfile | |
}) = [mainfile] | |
componentHsFiles (CBench Benchmark { | |
benchmarkInterface = BenchmarkExeV10 _ mainfile | |
}) = [mainfile] | |
componentHsFiles _ = [] | |
------------------------------ | |
-- Matching meta targets | |
-- | |
guardNamespaceMeta :: String -> Match () | |
guardNamespaceMeta = guardToken [""] "meta namespace" | |
guardMetaAll :: String -> Match () | |
guardMetaAll = guardToken ["all"] "meta-target 'all'" | |
guardNamespacePackage :: String -> Match () | |
guardNamespacePackage = guardToken ["pkg", "package"] "'pkg' namespace" | |
guardNamespaceCwd :: String -> Match () | |
guardNamespaceCwd = guardToken ["cwd"] "'cwd' namespace" | |
guardNamespaceModule :: String -> Match () | |
guardNamespaceModule = guardToken ["mod", "module"] "'module' namespace" | |
guardNamespaceFile :: String -> Match () | |
guardNamespaceFile = guardToken ["file"] "'file' namespace" | |
guardToken :: [String] -> String -> String -> Match () | |
guardToken tokens msg s | |
| caseFold s `elem` tokens = increaseConfidence | |
| otherwise = matchErrorExpected msg s | |
------------------------------ | |
-- Matching component kinds | |
-- | |
componentKind :: ComponentName -> ComponentKind | |
componentKind CLibName = LibKind | |
componentKind (CSubLibName _) = LibKind | |
componentKind (CFLibName _) = FLibKind | |
componentKind (CExeName _) = ExeKind | |
componentKind (CTestName _) = TestKind | |
componentKind (CBenchName _) = BenchKind | |
cinfoKind :: ComponentInfo -> ComponentKind | |
cinfoKind = componentKind . cinfoName | |
matchComponentKind :: String -> Match ComponentKind | |
matchComponentKind s | |
| s' `elem` liblabels = increaseConfidence >> return LibKind | |
| s' `elem` fliblabels = increaseConfidence >> return FLibKind | |
| s' `elem` exelabels = increaseConfidence >> return ExeKind | |
| s' `elem` testlabels = increaseConfidence >> return TestKind | |
| s' `elem` benchlabels = increaseConfidence >> return BenchKind | |
| otherwise = matchErrorExpected "component kind" s | |
where | |
s' = caseFold s | |
liblabels = ["lib", "library"] | |
fliblabels = ["flib", "foreign-library"] | |
exelabels = ["exe", "executable"] | |
testlabels = ["tst", "test", "test-suite"] | |
benchlabels = ["bench", "benchmark"] | |
matchComponentKindFilter :: String -> Match ComponentKind | |
matchComponentKindFilter s | |
| s' `elem` liblabels = increaseConfidence >> return LibKind | |
| s' `elem` fliblabels = increaseConfidence >> return FLibKind | |
| s' `elem` exelabels = increaseConfidence >> return ExeKind | |
| s' `elem` testlabels = increaseConfidence >> return TestKind | |
| s' `elem` benchlabels = increaseConfidence >> return BenchKind | |
| otherwise = matchErrorExpected "component kind filter" s | |
where | |
s' = caseFold s | |
liblabels = ["libs", "libraries"] | |
fliblabels = ["flibs", "foreign-libraries"] | |
exelabels = ["exes", "executables"] | |
testlabels = ["tests", "test-suites"] | |
benchlabels = ["benches", "benchmarks"] | |
showComponentKind :: ComponentKind -> String | |
showComponentKind LibKind = "library" | |
showComponentKind FLibKind = "foreign library" | |
showComponentKind ExeKind = "executable" | |
showComponentKind TestKind = "test-suite" | |
showComponentKind BenchKind = "benchmark" | |
showComponentKindShort :: ComponentKind -> String | |
showComponentKindShort LibKind = "lib" | |
showComponentKindShort FLibKind = "flib" | |
showComponentKindShort ExeKind = "exe" | |
showComponentKindShort TestKind = "test" | |
showComponentKindShort BenchKind = "bench" | |
showComponentKindFilterShort :: ComponentKind -> String | |
showComponentKindFilterShort LibKind = "libs" | |
showComponentKindFilterShort FLibKind = "flibs" | |
showComponentKindFilterShort ExeKind = "exes" | |
showComponentKindFilterShort TestKind = "tests" | |
showComponentKindFilterShort BenchKind = "benchmarks" | |
------------------------------ | |
-- Matching package targets | |
-- | |
guardPackage :: String -> FileStatus -> Match () | |
guardPackage str fstatus = | |
guardPackageName str | |
<|> guardPackageDir str fstatus | |
<|> guardPackageFile str fstatus | |
guardPackageName :: String -> Match () | |
guardPackageName s | |
| validPackageName s = increaseConfidence | |
| otherwise = matchErrorExpected "package name" s | |
validPackageName :: String -> Bool | |
validPackageName s = | |
all validPackageNameChar s | |
&& not (null s) | |
where | |
validPackageNameChar c = isAlphaNum c || c == '-' | |
guardPackageDir :: String -> FileStatus -> Match () | |
guardPackageDir _ (FileStatusExistsDir _) = increaseConfidence | |
guardPackageDir str _ = matchErrorExpected "package directory" str | |
guardPackageFile :: String -> FileStatus -> Match () | |
guardPackageFile _ (FileStatusExistsFile file) | |
| takeExtension file == ".cabal" | |
= increaseConfidence | |
guardPackageFile str _ = matchErrorExpected "package .cabal file" str | |
matchPackage :: [PackageInfo] -> String -> FileStatus -> Match PackageInfo | |
matchPackage pinfo = \str fstatus -> | |
orNoThingIn "project" "" $ | |
matchPackageName pinfo str | |
<//> (matchPackageDir pinfo str fstatus | |
<|> matchPackageFile pinfo str fstatus) | |
matchPackageName :: [PackageInfo] -> String -> Match PackageInfo | |
matchPackageName ps = \str -> do | |
guard (validPackageName str) | |
orNoSuchThing "package" str | |
(map (display . packageName) ps) $ | |
increaseConfidenceFor $ | |
matchInexactly caseFold (display . packageName) ps str | |
matchPackageDir :: [PackageInfo] | |
-> String -> FileStatus -> Match PackageInfo | |
matchPackageDir ps = \str fstatus -> | |
case fstatus of | |
FileStatusExistsDir canondir -> | |
orNoSuchThing "package directory" str (map (snd . fst) dirs) $ | |
increaseConfidenceFor $ | |
fmap snd $ matchExactly (fst . fst) dirs canondir | |
_ -> mzero | |
where | |
dirs = [ ((dabs,drel),p) | |
| p@PackageInfo{ pinfoDirectory = Just (dabs,drel) } <- ps ] | |
matchPackageFile :: [PackageInfo] -> String -> FileStatus -> Match PackageInfo | |
matchPackageFile ps = \str fstatus -> do | |
case fstatus of | |
FileStatusExistsFile canonfile -> | |
orNoSuchThing "package .cabal file" str (map (snd . fst) files) $ | |
increaseConfidenceFor $ | |
fmap snd $ matchExactly (fst . fst) files canonfile | |
_ -> mzero | |
where | |
files = [ ((fabs,frel),p) | |
| p@PackageInfo{ pinfoPackageFile = Just (fabs,frel) } <- ps ] | |
--TODO: test outcome when dir exists but doesn't match any known one | |
--TODO: perhaps need another distinction, vs no such thing, point is the | |
-- thing is not known, within the project, but could be outside project | |
matchSomePackageName :: String -> Match PackageName | |
matchSomePackageName str = do | |
guard (validPackageName str) | |
return (mkPackageName str) | |
------------------------------ | |
-- Matching component targets | |
-- | |
guardComponentName :: String -> Match () | |
guardComponentName s | |
| all validComponentChar s | |
&& not (null s) = increaseConfidence | |
| otherwise = matchErrorExpected "component name" s | |
where | |
validComponentChar c = isAlphaNum c || c == '.' | |
|| c == '_' || c == '-' || c == '\'' | |
matchComponentName :: [ComponentInfo] -> String -> Match ComponentInfo | |
matchComponentName cs str = | |
orNoSuchThing "component" str (map cinfoStrName cs) | |
$ increaseConfidenceFor | |
$ matchInexactly caseFold cinfoStrName cs str | |
matchComponentKindAndName :: [ComponentInfo] -> ComponentKind -> String | |
-> Match ComponentInfo | |
matchComponentKindAndName cs ckind str = | |
orNoSuchThing (showComponentKind ckind ++ " component") str | |
(map render cs) | |
$ increaseConfidenceFor | |
$ matchInexactly (\(ck, cn) -> (ck, caseFold cn)) | |
(\c -> (cinfoKind c, cinfoStrName c)) | |
cs | |
(ckind, str) | |
where | |
render c = showComponentKindShort (cinfoKind c) ++ ":" ++ cinfoStrName c | |
------------------------------ | |
-- Matching module targets | |
-- | |
guardModuleName :: String -> Match () | |
guardModuleName s = | |
case simpleParse s :: Maybe ModuleName of | |
Just _ -> increaseConfidence | |
_ | all validModuleChar s | |
&& not (null s) -> return () | |
| otherwise -> matchErrorExpected "module name" s | |
where | |
validModuleChar c = isAlphaNum c || c == '.' || c == '_' || c == '\'' | |
matchModuleName :: [ModuleName] -> String -> Match ModuleName | |
matchModuleName ms str = | |
orNoSuchThing "module" str (map display ms) | |
$ increaseConfidenceFor | |
$ matchInexactly caseFold display ms str | |
matchModuleNameAnd :: [(ModuleName, a)] -> String -> Match (ModuleName, a) | |
matchModuleNameAnd ms str = | |
orNoSuchThing "module" str (map (display . fst) ms) | |
$ increaseConfidenceFor | |
$ matchInexactly caseFold (display . fst) ms str | |
------------------------------ | |
-- Matching file targets | |
-- | |
matchPackageDirectoryPrefix :: [PackageInfo] -> FileStatus | |
-> Match (FilePath, PackageInfo) | |
matchPackageDirectoryPrefix ps (FileStatusExistsFile filepath) = | |
increaseConfidenceFor $ | |
matchDirectoryPrefix pkgdirs filepath | |
where | |
pkgdirs = [ (dir, p) | |
| p@PackageInfo { pinfoDirectory = Just (dir,_) } <- ps ] | |
matchPackageDirectoryPrefix _ _ = mzero | |
matchComponentFile :: [ComponentInfo] -> String | |
-> Match (FilePath, ComponentInfo) | |
matchComponentFile cs str = | |
orNoSuchThing "file" str [] $ | |
matchComponentModuleFile cs str | |
<|> matchComponentOtherFile cs str | |
matchComponentOtherFile :: [ComponentInfo] -> String | |
-> Match (FilePath, ComponentInfo) | |
matchComponentOtherFile cs = | |
matchFile | |
[ (file, c) | |
| c <- cs | |
, file <- cinfoHsFiles c | |
++ cinfoCFiles c | |
++ cinfoJsFiles c | |
] | |
matchComponentModuleFile :: [ComponentInfo] -> String | |
-> Match (FilePath, ComponentInfo) | |
matchComponentModuleFile cs str = do | |
matchFile | |
[ (normalise (d </> toFilePath m), c) | |
| c <- cs | |
, d <- cinfoSrcDirs c | |
, m <- cinfoModules c | |
] | |
(dropExtension (normalise str)) | |
-- utils | |
matchFile :: [(FilePath, a)] -> FilePath -> Match (FilePath, a) | |
matchFile fs = | |
increaseConfidenceFor | |
. matchInexactly caseFold fst fs | |
matchDirectoryPrefix :: [(FilePath, a)] -> FilePath -> Match (FilePath, a) | |
matchDirectoryPrefix dirs filepath = | |
tryEach $ | |
[ (file, x) | |
| (dir,x) <- dirs | |
, file <- maybeToList (stripDirectory dir) ] | |
where | |
stripDirectory :: FilePath -> Maybe FilePath | |
stripDirectory dir = | |
joinPath `fmap` stripPrefix (splitDirectories dir) filepathsplit | |
filepathsplit = splitDirectories filepath | |
------------------------------ | |
-- Matching monad | |
-- | |
-- | A matcher embodies a way to match some input as being some recognised | |
-- value. In particular it deals with multiple and ambiguous matches. | |
-- | |
-- There are various matcher primitives ('matchExactly', 'matchInexactly'), | |
-- ways to combine matchers ('matchPlus', 'matchPlusShadowing') and finally we | |
-- can run a matcher against an input using 'findMatch'. | |
-- | |
data Match a = NoMatch Confidence [MatchError] | |
| ExactMatch Confidence [a] | |
| InexactMatch Confidence [a] | |
deriving Show | |
type Confidence = Int | |
data MatchError = MatchErrorExpected String String -- thing got | |
| MatchErrorNoSuch String String [String] -- thing got alts | |
| MatchErrorIn String String MatchError -- kind thing | |
deriving (Show, Eq) | |
instance Functor Match where | |
fmap _ (NoMatch d ms) = NoMatch d ms | |
fmap f (ExactMatch d xs) = ExactMatch d (fmap f xs) | |
fmap f (InexactMatch d xs) = InexactMatch d (fmap f xs) | |
instance Applicative Match where | |
pure a = ExactMatch 0 [a] | |
(<*>) = ap | |
instance Alternative Match where | |
empty = NoMatch 0 [] | |
(<|>) = matchPlus | |
instance Monad Match where | |
return = pure | |
NoMatch d ms >>= _ = NoMatch d ms | |
ExactMatch d xs >>= f = addDepth d | |
$ msum (map f xs) | |
InexactMatch d xs >>= f = addDepth d . forceInexact | |
$ msum (map f xs) | |
instance MonadPlus Match where | |
mzero = empty | |
mplus = matchPlus | |
(<//>) :: Match a -> Match a -> Match a | |
(<//>) = matchPlusShadowing | |
infixl 3 <//> | |
addDepth :: Confidence -> Match a -> Match a | |
addDepth d' (NoMatch d msgs) = NoMatch (d'+d) msgs | |
addDepth d' (ExactMatch d xs) = ExactMatch (d'+d) xs | |
addDepth d' (InexactMatch d xs) = InexactMatch (d'+d) xs | |
forceInexact :: Match a -> Match a | |
forceInexact (ExactMatch d ys) = InexactMatch d ys | |
forceInexact m = m | |
-- | Combine two matchers. Exact matches are used over inexact matches | |
-- but if we have multiple exact, or inexact then the we collect all the | |
-- ambiguous matches. | |
-- | |
-- This operator is associative, has unit 'mzero' and is also commutative. | |
-- | |
matchPlus :: Match a -> Match a -> Match a | |
matchPlus (ExactMatch d1 xs) (ExactMatch d2 xs') = | |
ExactMatch (max d1 d2) (xs ++ xs') | |
matchPlus a@(ExactMatch _ _ ) (InexactMatch _ _ ) = a | |
matchPlus a@(ExactMatch _ _ ) (NoMatch _ _ ) = a | |
matchPlus (InexactMatch _ _ ) b@(ExactMatch _ _ ) = b | |
matchPlus (InexactMatch d1 xs) (InexactMatch d2 xs') = | |
InexactMatch (max d1 d2) (xs ++ xs') | |
matchPlus a@(InexactMatch _ _ ) (NoMatch _ _ ) = a | |
matchPlus (NoMatch _ _ ) b@(ExactMatch _ _ ) = b | |
matchPlus (NoMatch _ _ ) b@(InexactMatch _ _ ) = b | |
matchPlus a@(NoMatch d1 ms) b@(NoMatch d2 ms') | |
| d1 > d2 = a | |
| d1 < d2 = b | |
| otherwise = NoMatch d1 (ms ++ ms') | |
-- | Combine two matchers. This is similar to 'matchPlus' with the | |
-- difference that an exact match from the left matcher shadows any exact | |
-- match on the right. Inexact matches are still collected however. | |
-- | |
-- This operator is associative, has unit 'mzero' and is not commutative. | |
-- | |
matchPlusShadowing :: Match a -> Match a -> Match a | |
matchPlusShadowing a@(ExactMatch _ _) _ = a | |
matchPlusShadowing a b = matchPlus a b | |
------------------------------ | |
-- Various match primitives | |
-- | |
matchErrorExpected :: String -> String -> Match a | |
matchErrorExpected thing got = NoMatch 0 [MatchErrorExpected thing got] | |
matchErrorNoSuch :: String -> String -> [String] -> Match a | |
matchErrorNoSuch thing got alts = NoMatch 0 [MatchErrorNoSuch thing got alts] | |
expecting :: String -> String -> Match a -> Match a | |
expecting thing got (NoMatch 0 _) = matchErrorExpected thing got | |
expecting _ _ m = m | |
orNoSuchThing :: String -> String -> [String] -> Match a -> Match a | |
orNoSuchThing thing got alts (NoMatch 0 _) = matchErrorNoSuch thing got alts | |
orNoSuchThing _ _ _ m = m | |
orNoThingIn :: String -> String -> Match a -> Match a | |
orNoThingIn kind name (NoMatch n ms) = | |
NoMatch n [ MatchErrorIn kind name m | m <- ms ] | |
orNoThingIn _ _ m = m | |
increaseConfidence :: Match () | |
increaseConfidence = ExactMatch 1 [()] | |
increaseConfidenceFor :: Match a -> Match a | |
increaseConfidenceFor m = m >>= \r -> increaseConfidence >> return r | |
nubMatchesBy :: (a -> a -> Bool) -> Match a -> Match a | |
nubMatchesBy _ (NoMatch d msgs) = NoMatch d msgs | |
nubMatchesBy eq (ExactMatch d xs) = ExactMatch d (nubBy eq xs) | |
nubMatchesBy eq (InexactMatch d xs) = InexactMatch d (nubBy eq xs) | |
-- | Lift a list of matches to an exact match. | |
-- | |
exactMatches, inexactMatches :: [a] -> Match a | |
exactMatches [] = mzero | |
exactMatches xs = ExactMatch 0 xs | |
inexactMatches [] = mzero | |
inexactMatches xs = InexactMatch 0 xs | |
tryEach :: [a] -> Match a | |
tryEach = exactMatches | |
------------------------------ | |
-- Top level match runner | |
-- | |
-- | Given a matcher and a key to look up, use the matcher to find all the | |
-- possible matches. There may be 'None', a single 'Unambiguous' match or | |
-- you may have an 'Ambiguous' match with several possibilities. | |
-- | |
findMatch :: Match a -> MaybeAmbiguous a | |
findMatch match = case match of | |
NoMatch _ msgs -> None msgs | |
ExactMatch _ [x] -> Unambiguous x | |
InexactMatch _ [x] -> Unambiguous x | |
ExactMatch _ [] -> error "findMatch: impossible: ExactMatch []" | |
InexactMatch _ [] -> error "findMatch: impossible: InexactMatch []" | |
ExactMatch _ xs -> Ambiguous True xs | |
InexactMatch _ xs -> Ambiguous False xs | |
data MaybeAmbiguous a = None [MatchError] | Unambiguous a | Ambiguous Bool [a] | |
deriving Show | |
------------------------------ | |
-- Basic matchers | |
-- | |
-- | A primitive matcher that looks up a value in a finite 'Map'. The | |
-- value must match exactly. | |
-- | |
matchExactly :: Ord k => (a -> k) -> [a] -> (k -> Match a) | |
matchExactly key xs = | |
\k -> case Map.lookup k m of | |
Nothing -> mzero | |
Just ys -> exactMatches ys | |
where | |
m = Map.fromListWith (++) [ (key x, [x]) | x <- xs ] | |
-- | A primitive matcher that looks up a value in a finite 'Map'. It checks | |
-- for an exact or inexact match. We get an inexact match if the match | |
-- is not exact, but the canonical forms match. It takes a canonicalisation | |
-- function for this purpose. | |
-- | |
-- So for example if we used string case fold as the canonicalisation | |
-- function, then we would get case insensitive matching (but it will still | |
-- report an exact match when the case matches too). | |
-- | |
matchInexactly :: (Ord k, Ord k') => (k -> k') -> (a -> k) | |
-> [a] -> (k -> Match a) | |
matchInexactly cannonicalise key xs = | |
\k -> case Map.lookup k m of | |
Just ys -> exactMatches ys | |
Nothing -> case Map.lookup (cannonicalise k) m' of | |
Just ys -> inexactMatches ys | |
Nothing -> mzero | |
where | |
m = Map.fromListWith (++) [ (key x, [x]) | x <- xs ] | |
-- the map of canonicalised keys to groups of inexact matches | |
m' = Map.mapKeysWith (++) cannonicalise m | |
------------------------------ | |
-- Utils | |
-- | |
caseFold :: String -> String | |
caseFold = lowercase | |
------------------------------ | |
-- Example inputs | |
-- | |
{- | |
ex1pinfo :: [PackageInfo] | |
ex1pinfo = | |
[ addComponent (CExeName (mkUnqualComponentName "foo-exe")) [] ["Data.Foo"] $ | |
PackageInfo { | |
pinfoId = PackageIdentifier (mkPackageName "foo") (mkVersion [1]), | |
pinfoLocation = LocalUnpackedPackage "/the/foo", | |
pinfoDirectory = Just ("/the/foo", "foo"), | |
pinfoPackageFile = Just ("/the/foo/foo.cabal", "foo/foo.cabal"), | |
pinfoComponents = [] | |
} | |
, PackageInfo { | |
pinfoId = PackageIdentifier (mkPackageName "bar") (mkVersion [1]), | |
pinfoLocation = LocalUnpackedPackage "/the/foo", | |
pinfoDirectory = Just ("/the/bar", "bar"), | |
pinfoPackageFile = Just ("/the/bar/bar.cabal", "bar/bar.cabal"), | |
pinfoComponents = [] | |
} | |
] | |
where | |
addComponent n ds ms p = | |
p { | |
pinfoComponents = | |
ComponentInfo n (componentStringName (pinfoId p) n) | |
p ds (map mkMn ms) | |
[] [] [] | |
: pinfoComponents p | |
} | |
mkMn :: String -> ModuleName | |
mkMn = ModuleName.fromString | |
-} | |
{- | |
stargets = | |
[ TargetComponent (CExeName "foo") WholeComponent | |
, TargetComponent (CExeName "foo") (ModuleTarget (mkMn "Foo")) | |
, TargetComponent (CExeName "tst") (ModuleTarget (mkMn "Foo")) | |
] | |
where | |
mkMn :: String -> ModuleName | |
mkMn = fromJust . simpleParse | |
ex_pkgid :: PackageIdentifier | |
Just ex_pkgid = simpleParse "thelib" | |
-} | |
{- | |
ex_cs :: [ComponentInfo] | |
ex_cs = | |
[ (mkC (CExeName "foo") ["src1", "src1/src2"] ["Foo", "Src2.Bar", "Bar"]) | |
, (mkC (CExeName "tst") ["src1", "test"] ["Foo"]) | |
] | |
where | |
mkC n ds ms = ComponentInfo n (componentStringName n) ds (map mkMn ms) | |
mkMn :: String -> ModuleName | |
mkMn = fromJust . simpleParse | |
pkgid :: PackageIdentifier | |
Just pkgid = simpleParse "thelib" | |
-} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment