{-# LANGUAGE RankNTypes #-} module SourceConstraints (Context(..), plugin, warnings) where import Bag import Control.Applicative import Control.Monad import Control.Monad.IO.Class import Data.Attoparsec.Text import Data.Bool import Data.Char import Data.Data import Data.Either import Data.Eq import Data.Foldable import Data.Function import Data.Generics.Aliases import Data.Generics.Text import Data.Maybe import Data.Ord import Data.Semigroup import Data.String (String) import Data.Text (Text, pack) import Data.Tuple import DynFlags import ErrUtils #if MIN_VERSION_GLASGOW_HASKELL(8,10,0,0) import GHC.Hs #else import HsDecls import HsExtension import HsSyn #endif import HscTypes import Module hiding (Module) import Outputable hiding ((<>), empty) import Plugins import Prelude(error) import SrcLoc import System.FilePath.Posix import SourceConstraints.LocalModule import qualified Data.List as List data Context = Context { dynFlags :: DynFlags , localModules :: [LocalModule] } plugin :: Plugin plugin = defaultPlugin { parsedResultAction = run , pluginRecompile = purePlugin } run :: [CommandLineOption] -> ModSummary -> HsParsedModule -> Hsc HsParsedModule run options summary parsedModule = do dynFlags <- getDynFlags runSourceConstraints dynFlags options summary parsedModule runSourceConstraints :: DynFlags -> [CommandLineOption] -> ModSummary -> HsParsedModule -> Hsc HsParsedModule runSourceConstraints dynFlags options ModSummary{ms_location = ModLocation{..}} parsedModule = do localModules <- mapM parseLocalModule (pack <$> options) when (allowLocation ml_hs_file) . emitWarnings $ warnings Context{..} (hpm_module parsedModule) pure parsedModule where allowLocation = maybe False (notElem "autogen/" . splitPath) emitWarnings :: Bag WarnMsg -> Hsc () emitWarnings = liftIO . printOrThrowWarnings dynFlags parseLocalModule :: Text -> Hsc LocalModule parseLocalModule = either localParseFailure pure . parseOnly localModuleParser localParseFailure :: String -> Hsc a localParseFailure = throwOneError . mkPlainErrMsg dynFlags noSrcSpan . text -- | Find warnings for node warnings :: (Data a, Data b, Typeable a) => Context -> GenLocated a b -> WarningMessages warnings context@Context{..} (L sourceSpan node) = unionManyBags [ maybe emptyBag mkWarning $ unlocatedWarning context node , locatedWarnings context node , descend node ] where mkWarning = unitBag . mkWarnMsg dynFlags (fromJust $ cast sourceSpan) neverQualify descend :: Data a => a -> WarningMessages descend = unionManyBags . gmapQ (descend `ext2Q` warnings context) data IEClass = Module String | Type String | Operator String | Function String deriving stock (Eq, Ord) locatedWarnings :: Data a => Context -> a -> WarningMessages locatedWarnings context@Context{..} node = singleWarnings `unionBags` mkQ emptyBag absentImportDeclList node where singleWarnings = listToBag $ catMaybes [ mkQ empty sortedImportStatement node , mkQ empty sortedIEThingWith node , mkQ empty sortedIEs node , mkQ empty sortedMultipleDeriving node ] absentImportDeclList :: HsModule GhcPs -> WarningMessages absentImportDeclList HsModule{..} = listToBag $ catMaybes (absentList <$> candidates) where absentList :: LImportDecl GhcPs -> Maybe ErrMsg absentList = \case (L _loc ImportDecl { ideclHiding = Just (False, L loc list) }) -> testList loc list _ -> empty testList :: SrcSpan -> [LIE GhcPs] -> Maybe ErrMsg testList srcSpan = \case [] -> empty _ -> pure $ notEmpty srcSpan notEmpty :: SrcSpan -> ErrMsg notEmpty src = mkWarnMsg dynFlags src neverQualify (text "Present import list for local module") candidates :: [LImportDecl GhcPs] candidates = List.filter isCandidate hsmodImports isCandidate :: LImportDecl GhcPs -> Bool isCandidate = \case (L _ ImportDecl{ideclName = L _ moduleName}) -> any (`isLocalModule` moduleName) localModules _ -> False sortedImportStatement :: HsModule GhcPs -> Maybe ErrMsg sortedImportStatement HsModule{..} = sortedLocated "import statement" context (render context) hsmodImports sortedMultipleDeriving :: [LHsDerivingClause GhcPs] -> Maybe ErrMsg sortedMultipleDeriving = sortedLocated "deriving clauses" context (render context) sortedIEs :: [LIE GhcPs] -> Maybe ErrMsg sortedIEs = sortedLocated "import/export declaration" context ieClass sortedIEThingWith :: IE GhcPs -> Maybe ErrMsg sortedIEThingWith = \case (IEThingWith _xIE _name _ieWildcard ieWith _ieFieldLabels) -> sortedLocated "import/export item with list" context (render context) ieWith _ -> empty classify str@('(':_) = Function str classify str@(x:_) = if isUpper x then Type str else Function str classify [] = error "Parser error" ieClass :: IE GhcPs -> IEClass ieClass = \case (IEVar _xIE name) -> mkClass classify name (IEThingAbs _xIE name) -> mkClass Type name (IEThingAll _xIE name) -> mkClass Type name (IEModuleContents _xIE name) -> mkClass Module name (IEThingWith _xIE name _ieWildcard _ieWith _ieFieldLabels) -> mkClass Type name ie -> error $ "Unsupported: " <> gshow ie mkClass :: (Outputable a, Outputable b) => (String -> IEClass) -> GenLocated a b -> IEClass mkClass constructor name = constructor $ render context name unlocatedWarning :: Data a => Context -> a -> Maybe SDoc unlocatedWarning _context = mkQ empty requireDerivingStrategy requireDerivingStrategy :: HsDerivingClause GhcPs -> Maybe SDoc requireDerivingStrategy = \case HsDerivingClause{deriv_clause_strategy = Nothing} -> pure $ text "Missing deriving strategy" _ -> empty render :: Outputable a => Context -> a -> String render Context{..} outputable = renderWithStyle dynFlags (ppr outputable) (defaultUserStyle dynFlags) sortedLocated :: forall a b . (Eq b, Ord b, Outputable a) => String -> Context -> (a -> b) -> [Located a] -> Maybe ErrMsg sortedLocated name context@Context{..} ordering nodes = mkWarning <$> find isViolation candidates where mkWarning :: ((Located a, b), (Located a, b)) -> ErrMsg mkWarning ((actualNode, _item), (expectedNode, _expected)) = mkWarnMsg dynFlags (getLoc actualNode) neverQualify (text $ "Unsorted " <> name <> ", expected: " <> render context expectedNode) isViolation :: ((Located a, b), (Located a, b)) -> Bool isViolation ((_actualNode, item), (_expectedNode, expected)) = item /= expected candidates :: [((Located a, b), (Located a, b))] candidates = let items = (\node -> (node, ordering $ unLoc node)) <$> nodes expected = List.sortBy (compare `on` snd) items in List.zip items expected