duplo-1.6.9: src/Development/Duplo/JavaScript/Order.hs
{-# LANGUAGE TemplateHaskell #-}
module Development.Duplo.JavaScript.Order where
import Control.Applicative ((<$>))
import Control.Exception (throw)
import Control.Lens (makeLenses, ix)
import Control.Lens.Operators
import Control.Monad (liftM, when, void)
import Control.Monad.State.Lazy (get, put, state, execState)
import Control.Monad.Writer.Lazy (Writer, tell, runWriter)
import Data.Function (on)
import Data.List (findIndex, sortBy, nubBy)
import Data.Maybe (isJust, fromJust, fromMaybe)
import Development.Duplo.Types.JavaScript
import Language.JavaScript.Parser (JSNode(..), Node(..), TokenPosn(..))
makeLenses ''Module
-- | Reorder modules within the root node.
order :: JSNode -> JSNode
order jsNode =
-- Append the modules, in its rightful order, to all the non-applicable
-- nodes.
NN $ JSSourceElementsTop $ naNodes ++ aNodesWithSep
where
-- For hygiene
separator = NT (JSLiteral ";") (TokenPn 0 0 0) []
-- The "normal" output is the filtered non-applicable nodes and the
-- written-to nodes are of course the extracted ones.
(naNodes, aNodes) = runWriter $ extract jsNode
-- Reorder modules
orderedANodes = _node <$> reorder aNodes
-- Insert separators
aNodesWithSep = concat $ fmap (\n -> [n, separator]) orderedANodes
-- | Extract AMD modules to logger for re-ordering and the rest to output.
extract :: JSNode -> Writer [Module] [JSNode]
-- Go through all elements at top-level.
extract (NN (JSSourceElementsTop jsElements)) = mapM extract' jsElements
-- Impossible scenario
extract element = throw $ LanguageJavaScriptException element
extract' :: JSNode -> Writer [Module] JSNode
-- We are looking for a `define`!
extract' jsNode@(NN (JSExpression (NT (JSIdentifier "define") _ _:args:_))) = do
-- Save the module for processing.
tell [makeModule jsNode args]
-- Just return an empty string in place of the module.
return $ NT (JSIdentifier "") (TokenPn 0 0 0) []
-- Just pass everything else through.
extract' jsNode = return jsNode
-- | Turn a node into a module.
-- The root expression node
makeModule :: JSNode
-- The argument node
-> JSNode
-- The created module
-> Module
makeModule rootNode argNode =
Module moduleName deps rootNode Nothing
where
-- Extract the terminating nodes that are arguments.
(NN (JSArguments _ argNTs _)) = argNode
-- A simple pattern matching for the module declaration works, it
-- always follows this pattern.
(nameNT:_:depsNT:_) = argNTs
-- Extract the name, always first argument.
(NT (JSStringLiteral _ moduleName) _ _) = nameNT
-- Extract the module dependencies, always the third argument (second
-- being the comma separator).
(NN (JSArrayLiteral _ depsNodes _)) = depsNT
-- Only take the strings.
deps = map fromJust $ filter isJust $ map stringLiteralNT depsNodes
-- | Given a JSNode, return just the string literal, or nothing
stringLiteralNT :: JSNode -> Maybe String
stringLiteralNT (NT (JSStringLiteral _ string) _ _) = Just string
stringLiteralNT _ = Nothing
-- | Reorder all the applicable modules
reorder :: [Module] -> [Module]
reorder mods = nubbed
where
-- Score all the modules
scored = execState computeScores mods
-- Filter out initial modules that may have a score of `Nothing`
filtered = filter withScore scored
-- Sort by score
sorted = sortBy byDepScore filtered
-- Deduplicate, keeping the higher score ones
nubbed = reverse $ nubBy ((==) `on` _name) $ reverse sorted
withScore :: Module -> Bool
withScore aMod = case _score aMod of
Just _ -> True
Nothing -> False
byDepScore :: Module -> Module -> Ordering
byDepScore a b = compare (_score a) (_score b)
-- | Given a module list, find all the dependency scores of the constituent
-- modules.
computeScores :: OrderedModules [DepScore]
computeScores = do
mods <- get
-- Put the modules in state to be re-ordered as well as extract the
-- names by placing it in the State as values.
mapM (getDepScore []) $ fmap _name mods
-- | Given a module name, get its score.
getDepScore :: [ModuleName] -> ModuleName -> OrderedModules DepScore
getDepScore history modName = do
-- We go nuclear when there's circular dependency.
let history' = modName : history
-- Display the duplicate module if it's in the recorded modules
void $ when (modName `elem` history)
$ throw $ CircularDependencyException $ reverse history'
-- Take out the modules.
mods <- get
-- Assume module exist, as it should at this point.
let maybeIndex = findIndex ((modName ==) . _name) mods
let index = fromMaybe (throw $ ModuleNotFoundException modName) maybeIndex
-- Get the actual module.
let aMod = fromJust $ mods ^? ix index
-- The dependency list
let modDeps = _dependencies aMod
-- If there is a score, use it; otherwise, obviously go get it.
depScore <- case _score aMod of
-- Re-wrap with the score as the result.
Just modScore -> state $ const (modScore, mods)
-- Go through the dependencies' individual scores.
Nothing -> getDepScore' history' modDeps
-- Update the score.
let newMod = aMod & score .~ Just depScore
-- TODO: somehow can't get Lens to work. Doing the old fashion way.
let newMods = take index mods ++ [newMod] ++ drop (index + 1) mods
put newMods
-- Return the sscore.
return depScore
-- | Get a module's dependency score given its dependencies.
getDepScore' :: [ModuleName] -> [ModuleName] -> OrderedModules DepScore
getDepScore' history modNames =
liftM ((1 +) . sum) $ mapM (getDepScore history) modNames