packages feed

fix-imports-2.5.0: src/FixImports/Parse.hs

{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE TypeFamilies #-} -- without it extractEntity unvar unhappy
module FixImports.Parse (
    -- * types
    Module
    , Comment(..)
    -- * parse
    , parse
    -- * extract
    , qualifications
    , unqualifieds
    , importRange
    -- ** Import
    , extractImports

    -- TESTING
    , unqualifiedValues, unqualifiedTypes
) where
import           Control.Applicative ((<|>))
import qualified Control.DeepSeq as DeepSeq
import qualified Control.Monad as Monad
import qualified Data.Char as Char
import qualified Data.List as List
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Maybe as Maybe
import           Data.Maybe (fromMaybe)
import qualified Data.Set as Set

import qualified Language.Haskell.GhclibParserEx.GHC.Parser as Parser
import qualified Language.Haskell.GhclibParserEx.GHC.Settings.Config as Config
import qualified Language.Haskell.GhclibParserEx.GHC.Driver.Session as ExSession

import qualified GHC.Data.FastString as FastString
import qualified GHC.Data.Strict as Strict
import qualified GHC.Driver.Session as Session
import qualified GHC.Driver.Ppr as Ppr
import qualified GHC.Hs as Hs
import qualified GHC.Hs.DocString as DocString
import qualified GHC.Parser.Annotation as Annotation
import qualified GHC.Parser.Lexer as Lexer
import qualified GHC.Types.Error as Error
import qualified GHC.Types.Name.Occurrence as Occurrence
import qualified GHC.Types.Name.Reader as Reader
import qualified GHC.Types.SourceText as SourceText
import qualified GHC.Types.SrcLoc as SrcLoc
import qualified GHC.Types.PkgQual as PkgQual
import qualified GHC.Data.Bag as Bag
import qualified GHC.Unit.Types as Unit.Types
import qualified GHC.Utils.Error as Utils.Error
import qualified Language.Haskell.Syntax.Module.Name as Name

import qualified Data.Generics.Uniplate.Data as Uniplate

import qualified FixImports.Types as Types
import qualified FixImports.Util as Util


-- * parse

type Module = Hs.HsModule Hs.GhcPs

type Src = String

-- | makeDynFlags forces parse into IO.  The reason seems to be that GHC
-- puts dyn flags in a global variable.
parse :: [Types.Extension] -> FilePath -> Src
    -> IO (Either String (Module, [Comment]))
parse extensions filename src = makeDynFlags extensions filename src >>= \case
    Left err -> return $ Left $ "parsing pragmas: " <> err
    Right dynFlags -> return $ parseFile filename dynFlags src

data Comment = Comment { _span :: !Types.SrcSpan, _comment :: !String }
    deriving (Eq, Ord, Show)

instance DeepSeq.NFData Comment where rnf (Comment _ cmt) = DeepSeq.rnf cmt

makeDynFlags :: [Types.Extension] -> FilePath -> Src
    -> IO (Either String Session.DynFlags)
makeDynFlags extensions fname src =
    ExSession.parsePragmasIntoDynFlags defaultDynFlags
        (extensions ++ defaultExtensions, []) fname src
    where defaultExtensions = Session.languageExtensions Nothing

defaultDynFlags :: Session.DynFlags
defaultDynFlags = Session.defaultDynFlags Config.fakeSettings

-- | Parser.parseFile seems to return an unlifted type, even though I can't
-- tell from the :t or the source.  But it means you can't pass its result
-- to a toplevel function.  Weird.
parseFile :: FilePath -> Session.DynFlags -> String
    -> Either String (Module, [Comment])
parseFile filename dynFlags src = case Parser.parseFile filename dynFlags src of
    Lexer.POk state val -> Right
        ( SrcLoc.unLoc val
        -- , []
        , List.sort $ map extractComment (Lexer.comment_q state)
            ++ case Lexer.header_comments state of
                Strict.Nothing -> []
                Strict.Just cs -> map extractComment cs
            ++ importComments (SrcLoc.unLoc val)
        -- Lexer.annotations_comments I think is supposed to have comments
        -- associated with their "attached" SrcSpan, whatever that is.
        -- In any case, it's empty for comments in the import block at least.
        )
    Lexer.PFailed state -> Left $ unlines $ concat
        [ map ("warn: "<>) (extract warns)
        -- Looks like errors already start with "error: ".  Dunno about warns.
        , extract errors
        ]
        where
        (warns, errors) = Lexer.getPsMessages state
        extract =
            map (Ppr.showSDoc dynFlags . Utils.Error.pprLocMsgEnvelopeDefault)
            . Bag.bagToList . Error.getMessages

-- | Simplify a comment, this means I lose all the docstring details, but
-- I think it's ok.
extractComment :: Hs.LEpaComment -> Comment
extractComment cmt =
    Comment (extractRealSrcSpan (Annotation.anchor (SrcLoc.getLoc cmt))) $
        case Annotation.ac_tok (SrcLoc.unLoc cmt) of
            Annotation.EpaDocComment doc -> case doc of
                DocString.MultiLineDocString _decorator cs ->
                    mconcat $ map (DocString.unpackHDSC . SrcLoc.unLoc) $
                        NonEmpty.toList cs
                DocString.NestedDocString _decorator c ->
                    DocString.unpackHDSC (SrcLoc.unLoc c)
                DocString.GeneratedDocString c -> DocString.unpackHDSC c
            Annotation.EpaLineComment s -> s
            Annotation.EpaBlockComment s -> s
            Annotation.EpaDocOptions s -> s
            Annotation.EpaEofComment -> ""

-- * extract

importComments :: Module -> [Comment]
importComments =
    map extractComment
        . concatMap (extract . extractAnn . Annotation.ann . SrcLoc.getLoc)
        . Hs.hsmodImports
    where
    extractAnn = \case
        Annotation.EpAnn _anchor _anns comments -> comments
        Annotation.EpAnnNotUsed -> Annotation.emptyComments
    extract = \case
        Annotation.EpaComments prior -> prior
        Annotation.EpaCommentsBalanced prior following -> prior ++ following

-- | Qualifications of all the qualified names in this module.
qualifications :: Module -> Set.Set Types.Qualification
qualifications mod = Set.fromList
    [ Types.Qualification $ Name.moduleNameString moduleName
    | Reader.Qual moduleName _occName <- Uniplate.universeBi mod
    ]

-- | All unqualified names which are referenced in the module.  This is a lot
-- more complicated than 'qualifications' because this needs to omit unqualified
-- names which are defined in here.
unqualifieds :: Module -> Set.Set Types.Name
unqualifieds mod = unqualifiedTypes mod <> unqualifiedValues mod

-- | TODO: this doesn't handle type operators yet.
unqualifiedTypes :: Module -> Set.Set Types.Name
unqualifiedTypes mod = Set.fromList $ do
    hsType :: Hs.HsType Hs.GhcPs <- Uniplate.universeBi mod
    Reader.Unqual occName <- Uniplate.universeBi hsType
    let var = Occurrence.occNameString occName
    -- I don't want type variables, since they aren't references.  I can just
    -- filter out lower-case, which seems to be good enough?
    Monad.guard $ case var of
        c : _ -> Char.isUpper c
        _ -> False
    return $ inferName var

unqualifiedValues :: Module -> Set.Set Types.Name
unqualifiedValues mod = Set.fromList $ do
    (Hs.FunBind { fun_matches } :: Hs.HsBindLR Hs.GhcPs Hs.GhcPs)
        <- Uniplate.universeBi mod
    let matches = map SrcLoc.unLoc $ SrcLoc.unLoc $ Hs.mg_alts fun_matches
    -- I think 'pats' is the binding names.
    -- let pats = concatMap Hs.m_pats matches
    let rhss = map Hs.m_grhss matches
    Reader.Unqual occName <- Uniplate.universeBi rhss
    return $ inferName $ Occurrence.occNameString occName

-- | Return half-open line range of import block, starting from (0 based) line
-- of first import to the line after the last one.
importRange :: Module -> (Int, Int)
importRange mod =
    get . unzip . map range
        . Maybe.mapMaybe (getSpan . Annotation.locA . SrcLoc.getLoc)
        . Hs.hsmodImports $ mod
    where
    -- This range is 1-based inclusive, and I want 0-based half-open, so
    -- subtract 1 from the start.
    get :: ([Int], [Int]) -> (Int, Int)
    get (starts@(_:_), ends@(_:_)) = (minimum starts - 1, maximum ends)
    -- No imports, pick the line after export list or module header.
    get _ = fromMaybe (0, 0) $ do
        span <- getSpan =<<
            (Annotation.locA . SrcLoc.getLoc <$> Hs.hsmodExports mod)
            <|> (Annotation.locA . SrcLoc.getLoc <$> Hs.hsmodName mod)
        return (SrcLoc.srcSpanEndLine span, SrcLoc.srcSpanEndLine span)
    range span = (SrcLoc.srcSpanStartLine span, SrcLoc.srcSpanEndLine span)
    getSpan (SrcLoc.RealSrcSpan span _) = Just span
    getSpan _ = Nothing

-- ** Import

extractImports :: Module -> [Types.Import]
extractImports = map extractImport . Hs.hsmodImports

extractImport :: Hs.LImportDecl Hs.GhcPs -> Types.Import
extractImport locDecl = Types.Import
    { _importName = Types.ModuleName $ Name.moduleNameString $ SrcLoc.unLoc $
        Hs.ideclName decl
    , _importPkgQualifier = case Hs.ideclPkgQual decl of
        PkgQual.NoRawPkgQual -> Nothing
        PkgQual.RawPkgQual stringLit ->
            Just $ FastString.unpackFS $ SourceText.sl_fs stringLit
    , _importIsBoot = Hs.ideclSource decl == Unit.Types.IsBoot
    , _importSafe = Hs.ideclSafe decl
    -- I don't distinguish Hs.QualifiedPost
    , _importQualified = Hs.ideclQualified decl /= Hs.NotQualified
    , _importAs = Types.Qualification . Name.moduleNameString . SrcLoc.unLoc
        <$> Hs.ideclAs decl
    , _importHiding = case Hs.ideclImportList decl of
        Just (Hs.EverythingBut, _) -> True
        _ -> False
    , _importEntities = case Hs.ideclImportList decl of
        Just (_, things) ->
            Just $ map (extractEntity . SrcLoc.unLoc) $ SrcLoc.unLoc things
        _ -> Nothing
    , _importSpan = extractSrcSpan $ Annotation.locA $ SrcLoc.getLoc locDecl

    }
    where decl = SrcLoc.unLoc locDecl

extractSrcSpan :: SrcLoc.SrcSpan -> Types.SrcSpan
extractSrcSpan (SrcLoc.RealSrcSpan span _) = extractRealSrcSpan span
extractSrcSpan (SrcLoc.UnhelpfulSpan fstr) =
    error $ "UnhelpfulSpan: " <> show fstr
    -- I think GHC uses these internally, in phases after Hs.GhcPs.

extractRealSrcSpan :: SrcLoc.RealSrcSpan -> Types.SrcSpan
extractRealSrcSpan span = Types.SrcSpan
    -- GHC SrcSpan has 1-based lines, I use 0-based ones.
    { _startLine = SrcLoc.srcSpanStartLine span - 1
    , _startCol = SrcLoc.srcSpanStartCol span
    , _endLine = SrcLoc.srcSpanEndLine span - 1
    , _endCol = SrcLoc.srcSpanEndCol span
    }

extractEntity :: Hs.IE Hs.GhcPs -> Either Types.Error Types.Entity
extractEntity = fmap entity . \case
    -- var
    Hs.IEVar _ var -> Right (unvar var, Nothing)
    -- Constructor
    Hs.IEThingAbs _ var -> Right (unvar var, Nothing)
    -- Constructor(..)
    Hs.IEThingAll _ var -> Right (unvar var, Just "(..)")
    -- Constructor(x, y)
    -- What is _wildcard?
    Hs.IEThingWith _ var _wildcard things -> Right
        ( unvar var
        , Just $ "(" <> List.intercalate ", " (map (varStr . unvar) things)
            <> ")"
        )
    -- Shouldn't happen, export only.
    Hs.IEModuleContents {} -> Left "IEModuleContents"
    Hs.IEGroup {} -> Left "IEGroup"
    Hs.IEDoc {} -> Left "IEDoc"
    Hs.IEDocNamed {} -> Left "IEDocNamed"
    where
    entity ((qual, var), list) = Types.Entity qual var list
    unvar var = case SrcLoc.unLoc var of
        Hs.IEName _ n -> (Nothing, toName n)
        Hs.IEPattern _ n -> (Just "pattern", toName n)
        Hs.IEType _ n -> (Just "type", toName n)
        -- It's a DataConCantHappen, so I think supposed to not happen?
        Hs.XIEWrappedName _ -> (Just "??", Types.Name "XIEWrappedName")
    varStr (Just qual, name) = qual <> " " <> Types.showName name
    varStr (Nothing, name) = Types.showName name
    toName = inferName . unRdrName . SrcLoc.unLoc

inferName :: String -> Types.Name
inferName var
    | all Util.haskellOpChar var = Types.Operator var
    | otherwise = Types.Name var

unRdrName :: Reader.RdrName -> String
unRdrName = \case
    Reader.Unqual occName -> Occurrence.occNameString occName
    Reader.Qual mod occName ->
        Name.moduleNameString mod <> "." <> Occurrence.occNameString occName
    -- TODO what is this?
    Reader.Orig _mod occName -> -- modName mod <>
        "+" <> Occurrence.occNameString occName
    -- TODO what is this?
    Reader.Exact _name -> "exact"