module Puppet.Parser (
expression
, puppetParser
, runPParser
) where
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import qualified Data.HashSet as HS
import qualified Data.Maybe.Strict as S
import qualified Data.Foldable as F
import Data.Tuple.Strict hiding (fst,zip)
import Text.Regex.PCRE.ByteString.Utils
import Data.Char
import Control.Monad
import Control.Applicative
import Control.Lens hiding (noneOf)
import Puppet.Parser.Types
import Puppet.Utils
import Data.Scientific
import Text.Parsec.Error (ParseError)
import Text.Parsec.Expr
import Text.Parsec.Pos (SourcePos,SourceName)
import qualified Text.Parsec.Prim as PP
import Text.Parsec.Text ()
import Text.Parser.Char
import Text.Parser.Combinators
import Text.Parser.LookAhead
import Text.Parser.Token hiding (stringLiteral')
import Text.Parser.Token.Highlight
newtype Parser a = ParserT { unParser :: PP.ParsecT T.Text () Identity a}
deriving (Functor, Applicative, Alternative)
deriving instance Monad Parser
deriving instance Parsing Parser
deriving instance CharParsing Parser
deriving instance LookAheadParsing Parser
getPosition :: Parser SourcePos
getPosition = ParserT PP.getPosition
runPParser :: Parser a -> SourceName -> T.Text -> Either ParseError a
runPParser (ParserT p) = PP.parse p
type OP = PP.ParsecT T.Text () Identity
instance TokenParsing Parser where
someSpace = skipMany (simpleSpace <|> oneLineComment <|> multiLineComment)
where
simpleSpace = skipSome (satisfy isSpace)
oneLineComment = char '#' >> void (manyTill anyChar newline)
multiLineComment = try (string "/*") >> inComment
inComment = void (try (string "*/"))
<|> (skipSome (noneOf "*/") >> inComment)
<|> (oneOf "*/" >> inComment)
variable :: Parser Expression
variable = Terminal . UVariableReference <$> variableReference
stringLiteral' :: Parser T.Text
stringLiteral' = char '\'' *> interior <* symbolic '\''
where
interior = T.pack . concat <$> many (some (noneOf "'\\") <|> (char '\\' *> fmap escape anyChar))
escape '\'' = "'"
escape x = ['\\',x]
identifierStyle :: IdentifierStyle Parser
identifierStyle = IdentifierStyle "Identifier" (satisfy acceptable) (satisfy acceptable) HS.empty Identifier ReservedIdentifier
where
acceptable x = isAsciiLower x || isAsciiUpper x || isDigit x || (x == '_')
identl :: Parser Char -> Parser Char -> Parser T.Text
identl fstl nxtl = do
f <- fstl
nxt <- token $ many nxtl
return $ T.pack $ f : nxt
operator :: String -> Parser ()
operator = void . highlight Operator . try . symbol
reserved :: String -> Parser ()
reserved = reserve identifierStyle
variableName :: Parser T.Text
variableName = do
let acceptablePart = T.pack <$> ident identifierStyle
out <- qualif acceptablePart
when (out == "string") (fail "The special variable $string should never be used")
return out
qualif :: Parser T.Text -> Parser T.Text
qualif p = token $ do
header <- T.pack <$> option "" (try (string "::"))
( header <> ) . T.intercalate "::" <$> p `sepBy1` try (string "::")
qualif1 :: Parser T.Text -> Parser T.Text
qualif1 p = try $ do
r <- qualif p
unless ("::" `T.isInfixOf` r) (fail "This parser is not qualified")
return r
className :: Parser T.Text
className = qualif moduleName
typeName :: Parser T.Text
typeName = className
moduleName :: Parser T.Text
moduleName = genericModuleName False
resourceNameRef :: Parser T.Text
resourceNameRef = qualif (genericModuleName True)
genericModuleName :: Bool -> Parser T.Text
genericModuleName isReference = do
let acceptable x = isAsciiLower x || isDigit x || (x == '_')
firstletter = if isReference
then fmap toLower (satisfy isAsciiUpper)
else satisfy isAsciiLower
identl firstletter (satisfy acceptable)
parameterName :: Parser T.Text
parameterName = moduleName
variableReference :: Parser T.Text
variableReference = do
void (char '$')
v <- variableName
when (v == "string") (fail "The special variable $string must not be used")
return v
interpolableString :: Parser (V.Vector Expression)
interpolableString = V.fromList <$> between (char '"') (symbolic '"')
( many (interpolableVariableReference <|> doubleQuotedStringContent <|> fmap (Terminal . UString . T.singleton) (char '$')) )
where
doubleQuotedStringContent = Terminal . UString . T.pack . concat <$>
some ((char '\\' *> fmap stringEscape anyChar) <|> some (noneOf "\"\\$"))
stringEscape :: Char -> String
stringEscape 'n' = "\n"
stringEscape 't' = "\t"
stringEscape 'r' = "\r"
stringEscape '"' = "\""
stringEscape '\\' = "\\"
stringEscape '$' = "$"
stringEscape x = ['\\',x]
variableAccept x = isAsciiLower x || isAsciiUpper x || isDigit x || x == '_'
rvariableName = do
v <- T.pack . concat <$> some (string "::" <|> some (satisfy variableAccept))
when (v == "string") (fail "The special variable $string must not be used")
return v
rvariable = Terminal . UVariableReference <$> rvariableName
simpleIndexing = Lookup <$> rvariable <*> between (symbolic '[') (symbolic ']') expression
interpolableVariableReference = try $ do
void (char '$')
lookAhead anyChar >>= \c -> case c of
'{' -> between (symbolic '{') (char '}') ( try simpleIndexing
<|> rvariable
)
_ -> rvariable
regexp :: Parser T.Text
regexp = do
void (char '/')
T.pack . concat <$> many ( do { void (char '\\') ; x <- anyChar; return ['\\', x] } <|> some (noneOf "/\\") )
<* symbolic '/'
puppetArray :: Parser UValue
puppetArray = fmap (UArray . V.fromList) (brackets (expression `sepEndBy` comma)) <?> "Array"
puppetHash :: Parser UValue
puppetHash = fmap (UHash . V.fromList) (braces (hashPart `sepEndBy` comma)) <?> "Hash"
where
hashPart = (:!:) <$> (expression <* operator "=>")
<*> expression
puppetBool :: Parser Bool
puppetBool = (reserved "true" >> return True)
<|> (reserved "false" >> return False)
<?> "Boolean"
resourceReferenceRaw :: Parser (T.Text, [Expression])
resourceReferenceRaw = do
restype <- resourceNameRef <?> "Resource reference type"
resnames <- brackets (expression `sepBy1` comma) <?> "Resource reference values"
return (restype, resnames)
resourceReference :: Parser UValue
resourceReference = do
(restype, resnames) <- resourceReferenceRaw
return $ UResourceReference restype $ case resnames of
[x] -> x
_ -> Terminal (array resnames)
bareword :: Parser T.Text
bareword = identl (satisfy isAsciiLower) (satisfy acceptable) <?> "Bare word"
where
acceptable x = isAsciiLower x || isAsciiUpper x || isDigit x || (x == '_') || (x == '-')
genFunctionCall :: Bool -> Parser (T.Text, V.Vector Expression)
genFunctionCall nonparens = do
fname <- moduleName <?> "Function name"
let argsc sep e = (fmap (Terminal . UString) (qualif1 className) <|> e <?> "Function argument A") `sep` comma
terminalF = terminalG (fail "function hack")
expressionF = ParserT (buildExpressionParser expressionTable (unParser (token terminalF)) <?> "function expression")
withparens = parens (argsc sepEndBy expression)
withoutparens = argsc sepEndBy1 expressionF
args <- withparens <|> if nonparens
then withoutparens <?> "Function arguments B"
else fail "Function arguments C"
return (fname, V.fromList args)
functionCall :: Parser UValue
functionCall = do
(fname, args) <- genFunctionCall False
return $ UFunctionCall fname args
literalValue :: Parser UValue
literalValue = token (fmap UString stringLiteral' <|> fmap UString bareword <|> fmap UNumber numericalvalue <?> "Literal Value")
where
numericalvalue = integerOrDouble >>= \i -> case i of
Left x -> return (fromIntegral x)
Right y -> return (fromFloatDigits y)
terminalG :: Parser Expression -> Parser Expression
terminalG g = parens expression
<|> fmap (Terminal . UInterpolable) interpolableString
<|> (reserved "undef" *> return (Terminal UUndef))
<|> fmap (Terminal . URegexp) termRegexp
<|> variable
<|> fmap Terminal puppetArray
<|> fmap Terminal puppetHash
<|> fmap (Terminal . UBoolean) puppetBool
<|> fmap Terminal resourceReference
<|> g
<|> fmap Terminal literalValue
compileRegexp :: T.Text -> Parser CompRegex
compileRegexp p = case compile' compBlank execBlank (T.encodeUtf8 p) of
Right r -> return $ CompRegex p r
Left ms -> fail ("Can't parse regexp /" ++ T.unpack p ++ "/ : " ++ show ms)
termRegexp :: Parser CompRegex
termRegexp = regexp >>= compileRegexp
terminal :: Parser Expression
terminal = terminalG (fmap Terminal (fmap UHFunctionCall (try hfunctionCall) <|> try functionCall))
expression :: Parser Expression
expression = condExpression
<|> ParserT (buildExpressionParser expressionTable (unParser (token terminal)))
<?> "expression"
where
condExpression = do
selectedExpression <- try (token terminal <* symbolic '?')
let cas = do
c <- (symbol "default" *> return SelectorDefault)
<|> fmap SelectorValue (fmap UVariableReference variableReference
<|> fmap UBoolean puppetBool
<|> literalValue
<|> fmap UInterpolable interpolableString
<|> (URegexp <$> termRegexp))
void $ symbol "=>"
e <- expression
return (c :!: e)
cases <- braces (cas `sepEndBy1` comma)
return (ConditionalValue selectedExpression (V.fromList cases))
expressionTable :: [[Operator T.Text () Identity Expression]]
expressionTable = [ [ Postfix (chainl1 checkLookup (return (flip (.)))) ]
, [ Prefix ( operator' "-" >> return Negate ) ]
, [ Prefix ( operator' "!" >> return Not ) ]
, [ Infix ( operator' "." >> return FunctionApplication ) AssocLeft ]
, [ Infix ( reserved' "in" >> return Contains ) AssocLeft ]
, [ Infix ( operator' "/" >> return Division ) AssocLeft
, Infix ( operator' "*" >> return Multiplication ) AssocLeft
]
, [ Infix ( operator' "+" >> return Addition ) AssocLeft
, Infix ( operator' "-" >> return Substraction ) AssocLeft
]
, [ Infix ( operator' "<<" >> return LeftShift ) AssocLeft
, Infix ( operator' ">>" >> return RightShift ) AssocLeft
]
, [ Infix ( operator' "==" >> return Equal ) AssocLeft
, Infix ( operator' "!=" >> return Different ) AssocLeft
]
, [ Infix ( operator' "=~" >> return RegexMatch ) AssocLeft
, Infix ( operator' "!~" >> return NotRegexMatch ) AssocLeft
]
, [ Infix ( operator' ">=" >> return MoreEqualThan ) AssocLeft
, Infix ( operator' "<=" >> return LessEqualThan ) AssocLeft
, Infix ( operator' ">" >> return MoreThan ) AssocLeft
, Infix ( operator' "<" >> return LessThan ) AssocLeft
]
, [ Infix ( reserved' "and" >> return And ) AssocLeft
, Infix ( reserved' "or" >> return Or ) AssocLeft
]
]
where
checkLookup :: OP (Expression -> Expression)
checkLookup = flip Lookup <$> unParser (between (operator "[") (operator "]") expression)
operator' :: String -> OP ()
operator' = unParser . operator
reserved' :: String -> OP ()
reserved' = unParser . reserved
stringExpression :: Parser Expression
stringExpression = fmap (Terminal . UInterpolable) interpolableString <|> (reserved "undef" *> return (Terminal UUndef)) <|> fmap (Terminal . UBoolean) puppetBool <|> variable <|> fmap Terminal literalValue
variableAssignment :: Parser VarAss
variableAssignment = do
p <- getPosition
v <- variableReference
void $ symbolic '='
e <- expression
when (T.all isDigit v) (fail "Can't assign fully numeric variables")
pe <- getPosition
return (VarAss v e (p :!: pe))
nodeStmt :: Parser [Nd]
nodeStmt = do
p <- getPosition
reserved "node"
let toString (UString s) = s
toString (UNumber n) = scientific2text n
toString _ = error "Can't happen at nodeStmt"
nodename = (reserved "default" >> return NodeDefault) <|> fmap (NodeName . toString) literalValue
ns <- (fmap NodeMatch termRegexp <|> nodename) `sepBy1` comma
inheritance <- option S.Nothing (fmap S.Just (reserved "inherits" *> nodename))
st <- braces statementList
pe <- getPosition
return [Nd n st inheritance (p :!: pe) | n <- ns]
puppetClassParameters :: Parser (V.Vector (Pair T.Text (S.Maybe Expression)))
puppetClassParameters = V.fromList <$> parens (var `sepEndBy` comma)
where
toStrictMaybe (Just x) = S.Just x
toStrictMaybe Nothing = S.Nothing
var :: Parser (Pair T.Text (S.Maybe Expression))
var = (:!:)
<$> variableReference
<*> (toStrictMaybe <$> optional (symbolic '=' *> expression))
defineStmt :: Parser DefineDec
defineStmt = do
p <- getPosition
reserved "define"
name <- typeName
params <- option V.empty puppetClassParameters
st <- braces statementList
pe <- getPosition
return (DefineDec name params st (p :!: pe))
puppetIfStyleCondition :: Parser (Pair Expression (V.Vector Statement))
puppetIfStyleCondition = (:!:) <$> expression <*> braces statementList
unlessCondition :: Parser CondStatement
unlessCondition = do
p <- getPosition
reserved "unless"
(cond :!: stmts) <- puppetIfStyleCondition
pe <- getPosition
return (CondStatement (V.singleton (Not cond :!: stmts)) (p :!: pe))
ifCondition :: Parser CondStatement
ifCondition = do
p <- getPosition
reserved "if"
maincond <- puppetIfStyleCondition
others <- many (reserved "elsif" *> puppetIfStyleCondition)
elsecond <- option V.empty (reserved "else" *> braces statementList)
let ec = if V.null elsecond
then []
else [Terminal (UBoolean True) :!: elsecond]
pe <- getPosition
return (CondStatement (V.fromList (maincond : others ++ ec)) (p :!: pe))
caseCondition :: Parser CondStatement
caseCondition = do
let puppetRegexpCase = do
reg <- termRegexp
void $ symbolic ':'
stmts <- braces statementList
return [ (Terminal (URegexp reg), stmts) ]
defaultCase = do
try (reserved "default")
void $ symbolic ':'
stmts <- braces statementList
return [ (Terminal (UBoolean True), stmts) ]
puppetCase = do
compares <- expression `sepBy1` comma
void $ symbolic ':'
stmts <- braces statementList
return $ map (,stmts) compares
condsToExpression e (x, stmts) = f x :!: stmts
where f = case x of
(Terminal (UBoolean _))-> id
(Terminal (URegexp _)) -> RegexMatch e
_ -> Equal e
p <- getPosition
reserved "case"
expr1 <- expression
condlist <- braces (some (puppetRegexpCase <|> defaultCase <|> puppetCase))
pe <- getPosition
return (CondStatement (V.fromList (map (condsToExpression expr1) (concat condlist))) (p :!: pe) )
data OperatorChain a = OperatorChain a LinkType (OperatorChain a)
| EndOfChain a
instance F.Foldable OperatorChain where
foldMap f (EndOfChain x) = f x
foldMap f (OperatorChain a _ nx) = f a <> F.foldMap f nx
operatorChainStatement :: OperatorChain a -> a
operatorChainStatement (OperatorChain a _ _) = a
operatorChainStatement (EndOfChain x) = x
zipChain :: OperatorChain a -> [ ( a, a, LinkType ) ]
zipChain (OperatorChain a d nx) = (a, operatorChainStatement nx, d) : zipChain nx
zipChain (EndOfChain _) = []
depOperator :: Parser LinkType
depOperator = (operator "->" *> pure RBefore)
<|> (operator "~>" *> pure RNotify)
parseRelationships :: Parser a -> Parser (OperatorChain a)
parseRelationships p = do
g <- p
o <- optional depOperator
case o of
Just o' -> OperatorChain g o' <$> parseRelationships p
Nothing -> pure (EndOfChain g)
resourceGroup' :: Parser [ResDec]
resourceGroup' = do
let resourceName = token stringExpression
resourceDeclaration = do
p <- getPosition
names <- brackets (resourceName `sepEndBy1` comma) <|> fmap return resourceName
void $ symbolic ':'
vals <- fmap V.fromList (assignment `sepEndBy` comma)
pe <- getPosition
return [(n, vals, p :!: pe) | n <- names ]
groupDeclaration = (,) <$> many (char '@') <*> typeName <* symbolic '{'
(virts, rtype) <- try groupDeclaration
x <- resourceDeclaration `sepEndBy` (symbolic ';' <|> comma)
void $ symbolic '}'
virtuality <- case virts of
"" -> return Normal
"@" -> return Virtual
"@@" -> return Exported
_ -> fail "Invalid virtuality"
return [ ResDec rtype rname conts virtuality pos | (rname, conts, pos) <- concat x ]
assignment :: Parser (Pair T.Text Expression)
assignment = (:!:) <$> bw <*> (symbol "=>" *> expression)
where
bw = identl (satisfy isAsciiLower) (satisfy acceptable) <?> "Assignment key"
acceptable x = isAsciiLower x || isAsciiUpper x || isDigit x || (x == '_') || (x == '-')
searchExpression :: Parser SearchExpression
searchExpression = parens searchExpression <|> check <|> combine
where
combine = do
e1 <- parens searchExpression <|> check
opr <- (operator "and" *> return AndSearch) <|> (operator "or" *> return OrSearch)
e2 <- searchExpression
return (opr e1 e2)
check = do
attrib <- parameterName
opr <- (operator "==" *> return EqualitySearch) <|> (operator "!=" *> return NonEqualitySearch)
term <- stringExpression
return (opr attrib term)
resourceCollection :: Position -> T.Text -> Parser RColl
resourceCollection p restype = do
openchev <- some (char '<')
when (length openchev > 2) (fail "Too many brackets")
void $ symbolic '|'
e <- option AlwaysTrue searchExpression
void (char '|')
void (count (length openchev) (char '>'))
someSpace
overrides <- option [] $ braces (assignment `sepEndBy` comma)
let collectortype = if length openchev == 1
then Collector
else ExportedCollector
pe <- getPosition
return (RColl collectortype restype e (V.fromList overrides) (p :!: pe) )
classDefinition :: Parser ClassDecl
classDefinition = do
p <- getPosition
reserved "class"
ClassDecl <$> className
<*> option V.empty puppetClassParameters
<*> option S.Nothing (fmap S.Just (reserved "inherits" *> className))
<*> braces statementList
<*> ( (p :!:) <$> getPosition )
mainFunctionCall :: Parser MFC
mainFunctionCall = do
p <- getPosition
(fname, args) <- genFunctionCall True
pe <- getPosition
return (MFC fname args (p :!: pe))
mainHFunctionCall :: Parser SFC
mainHFunctionCall = do
p <- getPosition
fc <- try hfunctionCall
pe <- getPosition
return (SFC fc (p :!: pe))
dotCall :: Parser SFC
dotCall = do
p <- getPosition
ex <- expression
pe <- getPosition
hf <- case ex of
FunctionApplication e (Terminal (UHFunctionCall hf)) -> do
unless (S.isNothing (hf ^. hfexpr)) (fail "Can't call a function with . and ()")
return (hf & hfexpr .~ S.Just e)
Terminal (UHFunctionCall hf) -> do
when (S.isNothing (hf ^. hfexpr)) (fail "This function needs data to operate on")
return hf
_ -> fail "A method chained by dots."
unless (hf ^. hftype == HFEach) (fail "Expected 'each', the other types of method calls are not supported by language-puppet at the statement level.")
return (SFC hf (p :!: pe))
data ChainableStuff = ChainResColl RColl
| ChainResDecl ResDec
| ChainResRefr T.Text [Expression] PPosition
resourceDefaults :: Parser DefaultDec
resourceDefaults = do
p <- getPosition
rnd <- resourceNameRef
let assignmentList = V.fromList <$> assignment `sepEndBy1` comma
asl <- braces assignmentList
pe <- getPosition
return (DefaultDec rnd asl (p :!: pe))
resourceOverride :: Parser [ResOver]
resourceOverride = do
p <- getPosition
restype <- resourceNameRef
names <- brackets (expression `sepBy1` comma) <?> "Resource reference values"
assignments <- V.fromList <$> braces (assignment `sepEndBy` comma)
pe <- getPosition
return [ ResOver restype n assignments (p :!: pe) | n <- names ]
extractResRef :: ChainableStuff -> [(T.Text, Expression, PPosition)]
extractResRef (ChainResColl _) = []
extractResRef (ChainResDecl (ResDec rt rn _ _ pp)) = [(rt,rn,pp)]
extractResRef (ChainResRefr rt rns pp) = [(rt,rn,pp) | rn <- rns]
extractChainStatement :: ChainableStuff -> [Statement]
extractChainStatement (ChainResColl r) = [ResourceCollection r]
extractChainStatement (ChainResDecl d) = [ResourceDeclaration d]
extractChainStatement ChainResRefr{} = []
chainableStuff :: Parser [Statement]
chainableStuff = do
let withresname = do
p <- getPosition
restype <- resourceNameRef
lookAhead anyChar >>= \x -> case x of
'[' -> do
resnames <- brackets (expression `sepBy1` comma)
pe <- getPosition
pure (ChainResRefr restype resnames (p :!: pe))
_ -> ChainResColl <$> resourceCollection p restype
chain <- parseRelationships $ pure <$> try withresname <|> map ChainResDecl <$> resourceGroup'
let relations = do
(g1, g2, lt) <- zipChain chain
(rt1, rn1, _ :!: pe1) <- concatMap extractResRef g1
(rt2, rn2, ps2 :!: _ ) <- concatMap extractResRef g2
return (Dep (rt1 :!: rn1) (rt2 :!: rn2) lt (pe1 :!: ps2))
return $ map Dependency relations <> (chain ^.. folded . folded . to extractChainStatement . folded)
statement :: Parser [Statement]
statement =
(pure . SHFunctionCall <$> try dotCall)
<|> (pure . VariableAssignment <$> variableAssignment)
<|> (map Node <$> nodeStmt)
<|> (pure . DefineDeclaration <$> defineStmt)
<|> (pure . ConditionalStatement <$> unlessCondition)
<|> (pure . ConditionalStatement <$> ifCondition)
<|> (pure . ConditionalStatement <$> caseCondition)
<|> (pure . DefaultDeclaration <$> try resourceDefaults)
<|> (map ResourceOverride <$> try resourceOverride)
<|> chainableStuff
<|> (pure . ClassDeclaration <$> classDefinition)
<|> (pure . SHFunctionCall <$> mainHFunctionCall)
<|> (pure . MainFunctionCall <$> mainFunctionCall)
<?> "Statement"
statementList :: Parser (V.Vector Statement)
statementList = fmap (V.fromList . concat) (many statement)
puppetParser :: Parser (V.Vector Statement)
puppetParser = someSpace >> statementList
parseHFunction :: Parser HigherFuncType
parseHFunction = (reserved "each" *> pure HFEach)
<|> (reserved "map" *> pure HFMap )
<|> (reserved "reduce" *> pure HFReduce)
<|> (reserved "filter" *> pure HFFilter)
<|> (reserved "slice" *> pure HFSlice)
parseHParams :: Parser BlockParameters
parseHParams = between (symbolic '|') (symbolic '|') hp
where
acceptablePart = T.pack <$> ident identifierStyle
hp = do
vars <- (char '$' *> acceptablePart) `sepBy1` comma
case vars of
[a] -> return (BPSingle a)
[a,b] -> return (BPPair a b)
_ -> fail "Invalid number of variables between the pipes"
hfunctionCall :: Parser HFunctionCall
hfunctionCall = do
let toStrict (Just x) = S.Just x
toStrict Nothing = S.Nothing
HFunctionCall <$> parseHFunction
<*> fmap (toStrict . join) (optional (parens (optional expression)))
<*> parseHParams
<*> (symbolic '{' *> fmap (V.fromList . concat) (many (try statement)))
<*> fmap toStrict (optional expression) <* symbolic '}'