tilia-0.0.1.0: src/Tilia/Pragma.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Pragma-related helpers.
module Tilia.Pragma
( movesPositions,
effectiveExtensions,
lookupExtension,
)
where
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Driver.Session qualified as GHC
import GHC.LanguageExtensions.Type (Extension (..))
-- | Recognize @{-# LINE #-}@ and @{-# COLUMN #-}@ pragmas.
movesPositions :: Text -> Bool
movesPositions = any positional . pragmaBodies
where
positional body =
T.toUpper (T.takeWhile (/= ' ') body) `elem` ["LINE", "COLUMN"]
-- | The extensions actually in force in a module.
effectiveExtensions ::
-- | What the package the module belongs to puts in force, which is its
-- @default-language@ and @default-extensions@ already resolved into a
-- set.
[Extension] ->
-- | The module's source, read here for its @LANGUAGE@ pragmas alone.
Text ->
[Extension]
effectiveExtensions package
| null package = pragmasOver onUnlessRefused
| otherwise = pragmasOver package
-- | The extensions that are on until something says otherwise.
onUnlessRefused :: [Extension]
onUnlessRefused = [ImplicitPrelude]
-- | Apply a module's @LANGUAGE@ pragmas to a starting set.
pragmasOver :: [Extension] -> Text -> [Extension]
pragmasOver initial = foldl' apply initial . concatMap pragmaNames . pragmaBodies
where
apply acc name = case T.stripPrefix "No" name >>= lookupExtension of
Just off -> filter (/= off) acc
Nothing -> case lookupExtension name of
Just on | on `notElem` acc -> acc <> [on]
_ -> acc
pragmaNames body =
let (keyword, names) = T.break (== ' ') body
in if T.toUpper keyword == "LANGUAGE"
then filter (not . T.null) (map T.strip (T.splitOn "," names))
else []
-- | The extension one writes this name for, if any compiler knows it.
lookupExtension :: Text -> Maybe Extension
lookupExtension name = Map.lookup name extensionsByName
-- | Every extension this compiler knows, by the name one writes in a
-- pragma.
extensionsByName :: Map Text Extension
extensionsByName =
Map.fromList
[(T.pack (GHC.flagSpecName f), GHC.flagSpecFlag f) | f <- GHC.xFlags]
-- | What every @{-# … #-}@ in a module has between its braces, each on one
-- line.
pragmaBodies :: Text -> [Text]
pragmaBodies = go
where
go = step . T.dropWhile (\c -> c /= '{' && c /= '-' && c /= '"')
step t
| T.null t = []
| Just body <- T.stripPrefix "{-#" t = case T.breakOn "#-}" body of
(_, after) | T.null after -> []
(inner, after) -> T.unwords (T.words inner) : go (T.drop 3 after)
| Just after <- T.stripPrefix "{-" t = go (skipBlock (1 :: Int) after)
| opensLineComment t = go (T.dropWhile (/= '\n') t)
| Just after <- T.stripPrefix "\"" t = go (skipString after)
| otherwise = go (T.drop 1 t)
opensLineComment t = case T.stripPrefix "--" t of
Nothing -> False
Just rest -> maybe True (not . symbolic . fst) (T.uncons rest)
symbolic c = c `elem` ("!#$%&*+./<=>?@\\^|-~:" :: String)
skipBlock 0 t = t
skipBlock n t0 = inBlock n (T.dropWhile (\c -> c /= '{' && c /= '-') t0)
inBlock n t
| T.null t = t
| Just after <- T.stripPrefix "{-" t = skipBlock (n + 1) after
| Just after <- T.stripPrefix "-}" t = skipBlock (n - 1) after
| otherwise = skipBlock n (T.drop 1 t)
skipString = inString . T.dropWhile (\c -> c /= '"' && c /= '\\')
inString t
| T.null t = t
| Just after <- T.stripPrefix "\\" t = skipString (T.drop 1 after)
| Just after <- T.stripPrefix "\"" t = after
| otherwise = skipString (T.drop 1 t)