module EVM.Flatten (flatten) where
import EVM.Dapp (DappInfo, dappSources)
import EVM.Solidity (sourceAsts)
import EVM.Demand (demand)
import Control.Lens (preview, view, universe)
import Data.Aeson (Value (String))
import Data.Aeson.Lens (key, _String, _Array)
import qualified Data.Graph.Inductive.Graph as Fgl
import qualified Data.Graph.Inductive.PatriciaTree as Fgl
import qualified Data.Graph.Inductive.Query.BFS as Fgl
import qualified Data.Graph.Inductive.Query.DFS as Fgl
import Data.SemVer (SemVerRange, parseSemVerRange)
import qualified Data.SemVer as SemVer
import Control.Monad (forM)
import Data.ByteString (ByteString)
import Data.Foldable (foldl', toList)
import Data.List (sort, nub)
import Data.Map (Map, (!))
import Data.Maybe (mapMaybe, isJust)
import Data.Monoid ((<>))
import Data.Text (Text, unpack, pack)
import Data.Text.Encoding (encodeUtf8)
import Text.Read (readMaybe)
import qualified Data.Map as Map
import qualified Data.Text as Text
import qualified Data.ByteString as BS
type FileGraph = Fgl.Gr Text ()
importsFrom :: Value -> [Text]
importsFrom ast =
let
allNodes :: [Value]
allNodes = universe ast
resolveImport :: Value -> Maybe Text
resolveImport node =
case preview (key "name") node of
Just (String "ImportDirective") ->
preview (key "attributes" . key "absolutePath" . _String) node
_ ->
Nothing
in mapMaybe resolveImport allNodes
flatten :: DappInfo -> Text -> IO ()
flatten dapp target = do
let
graph :: FileGraph
graph = Fgl.mkGraph nodes edges
nodes :: [(Int, Text)]
nodes = zip [1..] (Map.keys asts)
edges =
[ (indices ! s, indices ! t, ())
| (s, v) <- Map.toList asts
, t <- importsFrom v ]
indices :: Map Text Int
indices = Map.fromList [(v, k) | (k, v) <- nodes]
asts :: Map Text Value
asts = view (dappSources . sourceAsts) dapp
case Map.lookup target indices of
Nothing ->
error "didn't find contract AST"
Just root -> do
let
subgraph :: Fgl.Gr Text ()
subgraph = Fgl.subgraph (Fgl.bfs root graph) graph
ordered :: [Text]
ordered = reverse (Fgl.topsort' subgraph)
pragma :: Text
pragma = maximalPragma (Map.elems asts)
sources <-
forM ordered $ \path -> do
src <- BS.readFile (unpack path)
pure $ mconcat
[ "////// ", encodeUtf8 path, "\n"
, stripImportsAndPragmas src (asts ! path), "\n"
]
demand target; demand pragma; demand sources
putStrLn $ "// hevm: flattened sources of " <> unpack target
putStrLn (unpack pragma)
BS.putStr (mconcat sources)
maximalPragma :: [Value] -> Text
maximalPragma asts =
case mapMaybe versions asts of
[] -> error "no Solidity version pragmas in any source files"
xs ->
"pragma solidity "
<> pack (show (rangeIntersection xs))
<> ";\n"
where
rangeIntersection :: [SemVerRange] -> SemVerRange
rangeIntersection = foldr1 SemVer.And . nub . sort
versions :: Value -> Maybe SemVerRange
versions ast = fmap grok components
where
pragma :: Maybe Value
pragma =
case filter (nodeIs "PragmaDirective") (universe ast) of
[x] -> Just x
[] -> Nothing
_ -> error "multiple version pragmas"
components :: Maybe [Value]
components = fmap toList
(pragma >>= preview (key "attributes" . key "literals" . _Array))
grok :: [Value] -> SemVerRange
grok = \case
String "solidity" : xs ->
let
rangeText = mconcat [x | String x <- xs]
in
case parseSemVerRange rangeText of
Right r -> r
Left _ ->
error ("failed to parse SemVer range " ++ show rangeText)
x ->
error ("unrecognized pragma: " ++ show x)
nodeIs :: Text -> Value -> Bool
nodeIs t x = isSourceNode && hasRightName
where
isSourceNode =
isJust (preview (key "src") x)
hasRightName =
Just t == preview (key "name" . _String) x
stripImportsAndPragmas :: ByteString -> Value -> ByteString
stripImportsAndPragmas bs ast = stripAstNodes bs ast p
where
p x = nodeIs "ImportDirective" x || nodeIs "PragmaDirective" x
stripAstNodes :: ByteString -> Value -> (Value -> Bool) -> ByteString
stripAstNodes bs ast p =
cutRanges [sourceRange node | node <- universe ast, p node]
where
sourceRange :: Value -> (Int, Int)
sourceRange v =
case preview (key "src" . _String) v of
Just (Text.splitOn ":" -> [readAs -> Just i, readAs -> Just n, _]) ->
(i, i + n)
_ ->
error "internal error: no source position for AST node"
cutRanges :: [(Int, Int)] -> ByteString
cutRanges (sort -> rs) = fst (foldl' f (bs, 0) rs)
where
f (bs', n) (i, j) =
( cut bs' (i + n) (j + n)
, n + length ("/* */" :: String))
cut :: ByteString -> Int -> Int -> ByteString
cut x i j =
let (a, b) = BS.splitAt i x
in a <> "/* " <> BS.take (j - i) b <> " */" <> BS.drop (j - i) b
readAs :: Read a => Text -> Maybe a
readAs = readMaybe . Text.unpack