ParseUtils.hs 12.1 KB
Newer Older
simonmar's avatar
simonmar committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
-----------------------------------------------------------------------------
-- |
-- Module      :  Distribution.ParseUtils
-- Copyright   :  (c) The University of Glasgow 2004
-- 
-- Maintainer  :  libraries@haskell.org
-- Stability   :  alpha
-- Portability :  portable
--
-- Utilities for parsing PackageDescription and InstalledPackageInfo.


{- All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.

    * Neither the name of the University nor the names of other
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -}

-- This module is meant to be local-only to Distribution...

-- #hide
module Distribution.ParseUtils (
47
48
        LineNo, PError(..), PWarning,
        locatedErrorMsg, showError, syntaxError, warning,
49
	runP, ParseResult(..),
simonmar's avatar
simonmar committed
50
	StanzaField(..), splitStanzas, Stanza, singleStanza,
ijones's avatar
ijones committed
51
	parseFilePathQ, parseTokenQ,
52
53
	parseModuleNameQ, parseDependency, parseOptVersion,
	parsePackageNameQ, parseVersionRangeQ,
54
	parseTestedWithQ, parseLicenseQ, parseExtensionQ, parseCommaList, parseOptCommaList,
ijones's avatar
ijones committed
55
56
	showFilePath, showToken, showTestedWith, showDependency, showFreeText,
	simpleField, listField, commaListField, optsField, 
57
	parseReadS, parseReadSQ, parseQuoted,
simonmar's avatar
simonmar committed
58
59
60
  ) where

import Text.PrettyPrint.HughesPJ
ijones's avatar
ijones committed
61
import Distribution.Compiler (CompilerFlavor)
simonmar's avatar
simonmar committed
62
63
64
65
import Distribution.License
import Distribution.Version
import Distribution.Package	( parsePackageName )
import Distribution.Compat.ReadP as ReadP hiding (get)
ijones's avatar
ijones committed
66
67
import Distribution.Compat.FilePath (platformPath)
import Control.Monad (liftM)
simonmar's avatar
simonmar committed
68
import Data.Char
ijones's avatar
ijones committed
69
import Language.Haskell.Extension (Extension)
simonmar's avatar
simonmar committed
70
71
72
73
74
75
76
77
78
79

-- -----------------------------------------------------------------------------

type LineNo = Int

data PError = AmbigousParse String LineNo
            | NoParse String LineNo
            | FromString String (Maybe LineNo)
        deriving Show

80
81
82
type PWarning = String

data ParseResult a = ParseFailed PError | ParseOk [PWarning] a
83
84
85
        deriving Show

instance Monad ParseResult where
86
	return x = ParseOk [] x
87
	ParseFailed err >>= _ = ParseFailed err
88
89
90
	ParseOk ws x >>= f = case f x of
	                       ParseFailed err -> ParseFailed err
			       ParseOk ws' x' -> ParseOk (ws'++ws) x'
91
	fail s = ParseFailed (FromString s Nothing)
simonmar's avatar
simonmar committed
92

93
runP :: LineNo -> String -> ReadP a a -> String -> ParseResult a
simonmar's avatar
simonmar committed
94
95
runP lineNo field p s =
  case [ x | (x,"") <- results ] of
96
    [a] -> ParseOk [] a
simonmar's avatar
simonmar committed
97
    []  -> case [ x | (x,ys) <- results, all isSpace ys ] of
98
             [a] -> ParseOk [] a
99
100
101
             []  -> ParseFailed (NoParse field lineNo)
             _   -> ParseFailed (AmbigousParse field lineNo)
    _   -> ParseFailed (AmbigousParse field lineNo)
simonmar's avatar
simonmar committed
102
103
  where results = readP_to_S p s

ijones's avatar
ijones committed
104
-- TODO: deprecated
simonmar's avatar
simonmar committed
105
showError :: PError -> String
ijones's avatar
ijones committed
106
107
108
109
showError e =
  case locatedErrorMsg e of
    (Just n,  s) -> "Line "++show n++": " ++ s
    (Nothing, s) -> s
simonmar's avatar
simonmar committed
110

ijones's avatar
ijones committed
111
112
locatedErrorMsg :: PError -> (Maybe LineNo, String)
locatedErrorMsg (AmbigousParse f n) = (Just n, "Ambigous parse in field '"++f++"'")
113
locatedErrorMsg (NoParse f n)       = (Just n, "Parse of field '"++f++"' failed: ")
ijones's avatar
ijones committed
114
115
116
117
locatedErrorMsg (FromString s n)    = (n, s)

syntaxError :: LineNo -> String -> ParseResult a
syntaxError n s = ParseFailed $ FromString s (Just n)
simonmar's avatar
simonmar committed
118

119
120
121
warning :: String -> ParseResult ()
warning s = ParseOk [s] ()

simonmar's avatar
simonmar committed
122
123
124
125
data StanzaField a 
  = StanzaField 
      { fieldName     :: String
      , fieldGet      :: a -> Doc
126
      , fieldSet      :: LineNo -> String -> a -> ParseResult a
simonmar's avatar
simonmar committed
127
128
129
130
      }

simpleField :: String -> (a -> Doc) -> (ReadP a a) -> (b -> a) -> (a -> b -> b) -> StanzaField b
simpleField name showF readF get set = StanzaField name
ijones's avatar
ijones committed
131
   (\st -> showF (get st))
simonmar's avatar
simonmar committed
132
133
134
135
   (\lineNo val st -> do
       x <- runP lineNo name readF val
       return (set x st))

ijones's avatar
ijones committed
136
137
138
139
140
141
142
commaListField :: String -> (a -> Doc) -> (ReadP [a] a) -> (b -> [a]) -> ([a] -> b -> b) -> StanzaField b
commaListField name showF readF get set = StanzaField name
   (\st -> fsep (punctuate comma (map showF (get st))))
   (\lineNo val st -> do
       xs <- runP lineNo name (parseCommaList readF) val
       return (set xs st))

simonmar's avatar
simonmar committed
143
144
listField :: String -> (a -> Doc) -> (ReadP [a] a) -> (b -> [a]) -> ([a] -> b -> b) -> StanzaField b
listField name showF readF get set = StanzaField name
ijones's avatar
ijones committed
145
   (\st -> fsep (map showF (get st)))
simonmar's avatar
simonmar committed
146
   (\lineNo val st -> do
ijones's avatar
ijones committed
147
       xs <- runP lineNo name (parseOptCommaList readF) val
simonmar's avatar
simonmar committed
148
149
150
151
152
       return (set xs st))

optsField :: String -> CompilerFlavor -> (b -> [(CompilerFlavor,[String])]) -> ([(CompilerFlavor,[String])] -> b -> b) -> StanzaField b
optsField name flavor get set = StanzaField name
   (\st -> case lookup flavor (get st) of
ijones's avatar
ijones committed
153
        Just args -> hsep (map text args)
simonmar's avatar
simonmar committed
154
155
156
157
158
159
160
161
162
163
164
165
166
167
        Nothing   -> empty)
   (\_ val st -> 
       let
         old_val  = get st
         old_args = case lookup flavor old_val of
                       Just args -> args
                       Nothing   -> []
         val'     = filter (\(f,_) -> f/=flavor) old_val
       in return (set ((flavor,words val++old_args) : val') st))

type Stanza = [(LineNo,String,String)]

-- |Split a string into blank line-separated stanzas of
-- "Field: value" groups
168
splitStanzas :: String -> ParseResult [Stanza]
169
splitStanzas = mapM mkStanza . map merge . groupStanzas . filter validLine . zip [1..] . map trimTrailingSpaces . lines
simonmar's avatar
simonmar committed
170
171
172
173
174
  where validLine (_,s) = case dropWhile isSpace s of
                            '-':'-':_ -> False      -- Comment
                            _         -> True
        groupStanzas :: [(Int,String)] -> [[(Int,String)]]
        groupStanzas [] = []
175
176
        groupStanzas xs = let (ys,zs) = break (null . snd) xs
                           in ys : groupStanzas (dropWhile (null . snd) zs)
simonmar's avatar
simonmar committed
177

178
179
trimTrailingSpaces :: String -> String
trimTrailingSpaces = reverse . dropWhile isSpace . reverse
simonmar's avatar
simonmar committed
180
181
182
183

-- |Split a file into "Field: value" groups, but blank lines have no
-- significance, unlike 'splitStanzas'.  A field value may span over blank
-- lines.
184
singleStanza :: String -> ParseResult Stanza
185
singleStanza = mkStanza . merge . filter validLine . zip [1..] . map trimTrailingSpaces . lines
ijones's avatar
ijones committed
186
187
188
189
  where validLine (_,s) = case dropWhile isSpace s of
                            '-':'-':_ -> False      -- Comment
                            []        -> False      -- blank line
                            _         -> True
simonmar's avatar
simonmar committed
190

ijones's avatar
ijones committed
191
merge :: [(a, [Char])] -> [(a, [Char])]
simonmar's avatar
simonmar committed
192
193
194
195
196
197
198
merge ((n,x):(_,c:s):ys) 
  | c == ' ' || c == '\t' = case dropWhile isSpace s of
                               ('.':s') -> merge ((n,x++"\n"++s'):ys)
                               s'       -> merge ((n,x++"\n"++s'):ys)
merge ((n,x):ys) = (n,x) : merge ys
merge []         = []

ijones's avatar
ijones committed
199
200
201
202
203
mkStanza :: [(Int,String)] -> ParseResult Stanza
mkStanza []          = return []
mkStanza ((n,xs):ys) =
  case break (==':') xs of
    (fld', ':':val) -> do
ijones's avatar
ijones committed
204
       let fld'' = map toLower fld'
205
206
207
208
209
210
211
212
       fld <- case () of
                _ | fld'' == "hs-source-dir"
                           -> do warning "The field \"hs-source-dir\" is deprecated, please use hs-source-dirs."
                                 return "hs-source-dirs"
                  | fld'' == "other-files"
                           -> do warning "The field \"other-files\" is deprecated, please use extra-source-files."
                                 return "extra-source-files"
                  | otherwise -> return fld''
ijones's avatar
ijones committed
213
214
215
       ss <- mkStanza ys
       checkDuplField fld ss
       return ((n, fld, dropWhile isSpace val):ss)
ijones's avatar
ijones committed
216
    (_, _)       -> syntaxError n "Invalid syntax (no colon after field name)"
ijones's avatar
ijones committed
217
  where
ijones's avatar
ijones committed
218
219
    checkDuplField _ [] = return ()
    checkDuplField fld ((n',fld',_):xs')
ijones's avatar
ijones committed
220
      | fld' == fld = syntaxError (max n n') $ "The field "++fld++" was already defined on line " ++ show (min n n')
ijones's avatar
ijones committed
221
      | otherwise   = checkDuplField fld xs'
simonmar's avatar
simonmar committed
222
223

-- |parse a module name
224
parseModuleNameQ :: ReadP r String
ijones's avatar
ijones committed
225
226
parseModuleNameQ = parseQuoted modu <++ modu
 where modu = do 
227
228
229
	  c <- satisfy isUpper
	  cs <- munch (\x -> isAlphaNum x || x `elem` "_'.")
	  return (c:cs)
simonmar's avatar
simonmar committed
230

231
parseFilePathQ :: ReadP r FilePath
ijones's avatar
ijones committed
232
parseFilePathQ = liftM platformPath parseTokenQ
simonmar's avatar
simonmar committed
233
234
235
236
237

parseReadS :: Read a => ReadP r a
parseReadS = readS_to_P reads

parseDependency :: ReadP r Dependency
238
parseDependency = do name <- parsePackageNameQ
simonmar's avatar
simonmar committed
239
                     skipSpaces
240
                     ver <- parseVersionRangeQ <++ return AnyVersion
simonmar's avatar
simonmar committed
241
242
243
                     skipSpaces
                     return $ Dependency name ver

ijones's avatar
ijones committed
244
parsePackageNameQ :: ReadP r String
245
parsePackageNameQ = parseQuoted parsePackageName <++ parsePackageName 
ijones's avatar
ijones committed
246
247

parseVersionRangeQ :: ReadP r VersionRange
248
249
parseVersionRangeQ = parseQuoted parseVersionRange <++ parseVersionRange

simonmar's avatar
simonmar committed
250
parseOptVersion :: ReadP r Version
251
252
253
254
255
256
257
258
259
260
261
parseOptVersion = parseQuoted ver <++ ver
  where ver = parseVersion <++ return noVersion
	noVersion = Version{ versionBranch=[], versionTags=[] }

parseTestedWithQ :: ReadP r (CompilerFlavor,VersionRange)
parseTestedWithQ = parseQuoted tw <++ tw
  where tw = do compiler <- parseReadS
		skipSpaces
		version <- parseVersionRange <++ return AnyVersion
		skipSpaces
		return (compiler,version)
simonmar's avatar
simonmar committed
262

263
264
parseLicenseQ :: ReadP r License
parseLicenseQ = parseQuoted parseReadS <++ parseReadS
simonmar's avatar
simonmar committed
265

266
267
268
269
-- urgh, we can't define optQuotes :: ReadP r a -> ReadP r a
-- because the "compat" version of ReadP isn't quite powerful enough.  In
-- particular, the type of <++ is ReadP r r -> ReadP r a -> ReadP r a
-- Hence the trick above to make 'lic' polymorphic.
simonmar's avatar
simonmar committed
270

271
272
parseExtensionQ :: ReadP r Extension
parseExtensionQ = parseQuoted parseReadS <++ parseReadS
simonmar's avatar
simonmar committed
273

274
275
276
277
-- | Parse something optionally wrapped in quotes.
parseReadSQ :: Read a => ReadP r a
parseReadSQ = parseQuoted parseReadS <++ parseReadS

ijones's avatar
ijones committed
278
279
parseTokenQ :: ReadP r String
parseTokenQ = parseReadS <++ munch1 (\x -> not (isSpace x) && x /= ',')
simonmar's avatar
simonmar committed
280
281
282
283
284
285

parseCommaList :: ReadP r a -- ^The parser for the stuff between commas
               -> ReadP r [a]
parseCommaList p = sepBy p separator
    where separator = skipSpaces >> ReadP.char ',' >> skipSpaces

ijones's avatar
ijones committed
286
287
288
289
290
parseOptCommaList :: ReadP r a -- ^The parser for the stuff between commas
               -> ReadP r [a]
parseOptCommaList p = sepBy p separator
    where separator = skipSpaces >> optional (ReadP.char ',') >> skipSpaces

291
292
293
parseQuoted :: ReadP r a -> ReadP r a
parseQuoted p = between (ReadP.char '"') (ReadP.char '"') p

simonmar's avatar
simonmar committed
294
295
296
297
-- --------------------------------------------
-- ** Pretty printing

showFilePath :: FilePath -> Doc
ijones's avatar
ijones committed
298
299
300
301
showFilePath = showToken

showToken :: String -> Doc
showToken str
302
303
 | not (any dodgy str) &&
   not (null str)       = text str
ijones's avatar
ijones committed
304
305
 | otherwise            = text (show str)
  where dodgy c = isSpace c || c == ','
simonmar's avatar
simonmar committed
306
307
308
309
310
311

showTestedWith :: (CompilerFlavor,VersionRange) -> Doc
showTestedWith (compiler,version) = text (show compiler ++ " " ++ showVersionRange version)

showDependency :: Dependency -> Doc
showDependency (Dependency name ver) = text name <+> text (showVersionRange ver)
312
313
314
315
316

-- | Pretty-print free-format text, ensuring that it is vertically aligned,
-- and with blank lines replaced by dots for correct re-parsing.
showFreeText :: String -> Doc
showFreeText s = vcat [text (if null l then "." else l) | l <- lines s]