packages feed

import-style-plugin-0.1.0.0: src/ImportStylePlugin.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module ImportStylePlugin where

import Control.Monad (forM_, unless)
import Data.Functor (($>), (<&>))
import Data.List (intercalate, intersperse)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes)
import qualified Data.Set as Set
import Data.String (fromString)
import qualified GHC.Hs as Ghc
import qualified GHC.Plugins as Ghc
import qualified GHC.Tc.Types as Ghc
import ImportStylePlugin.Compat (getExplicitlyHiddenNames, getExplicitlyImportedNames, report)
import ImportStylePlugin.Config as Config

importPlugin ::
  ImportsStyle ->
  Ghc.TcGblEnv ->
  Ghc.TcM Ghc.TcGblEnv
importPlugin ImportsStyle{..} e@Ghc.TcGblEnv{tcg_rn_imports} = corrections $> e
 where
  mkWord = \case
    Error -> "must"
    Warning -> "should"

  corrections = forM_ tcg_rn_imports \(Ghc.L Ghc.SrcSpanAnn{..} importDecl@Ghc.ImportDecl{..}) ->
    let (Ghc.L _ (Ghc.moduleNameString -> moduleName)) = ideclName
     in do
          case qualificationStyle of
            _ | ideclQualified == Ghc.NotQualified -> pure ()
            Just Pre | ideclQualified /= Ghc.QualifiedPre -> report Error "Use prefix qualification" (Just locA)
            Just Post | ideclQualified /= Ghc.QualifiedPost -> report Error "Use postfix qualification" (Just locA)
            _ -> pure ()

          case Map.lookup (Config.ModuleName moduleName) bannedModules of
            Just Ban{..} -> report severity (fromString why) (Just locA)
            Nothing -> maybe
              do pure ()
              do
                \ImportRules{..} -> unless (any (isImportValid importDecl) rules) do
                  let header = fromString $ "Import " <> mkWord severity <> " satisfy the following rules:"
                  let allStringRules = Ghc.vcat $ (header :) $ intersperse "or" $ map (Ghc.vcat . stringifyRule severity) rules
                  report severity allStringRules (Just locA)
              do Map.lookup (Config.ModuleName moduleName) importRules

  stringifyRule severity ImportRule{..} =
    map (fromString . ("  * " <>)) $
      catMaybes
        [ qualification <&> \case
            Required -> "Module " <> word <> " be qualified"
            Forbidden -> "Module " <> word <> " not be qualified"
        , let common (Set.toList -> allowed) =
                "Alias " <> word <> " be " <> case allowed of
                  [name] -> name
                  _ -> "one of " <> commaSep allowed
           in aliases <&> \case
                Exactly allowed -> if Set.null allowed then "Import " <> word <> " not have alias" else common allowed
                OrOmitted allowed -> common allowed <> " or alias can be omitted"
        , importedNames <&> \case
            BlackList names -> "Module " <> word <> " not import the following names: " <> printNamesList names
            WhiteList names | Set.null names -> "Module" <> word <> " not import names explicitly"
            WhiteList names -> "Module " <> word <> " import only the following names: " <> printNamesList names
        ]
   where
    printNamesList = commaSep . Set.toList
    commaSep = intercalate ", "

    word = mkWord severity

  isImportValid :: Ghc.ImportDecl Ghc.GhcRn -> ImportRule -> Bool
  isImportValid decl@Ghc.ImportDecl{..} ImportRule{..} =
    isQualificationValid
      && areImportedNamesValid
      && isAliasValid
   where
    isQualificationValid = case qualification of
      Just Required -> ideclQualified /= Ghc.NotQualified
      Just Forbidden -> ideclQualified == Ghc.NotQualified
      Nothing -> True

    isAliasValid = case aliases of
      Just (Exactly allowedAliases) -> oneOfAllowed False allowedAliases
      Just (OrOmitted allowedAliases) -> oneOfAllowed True allowedAliases
      Nothing -> True
     where
      oneOfAllowed def allowed = maybe
        do def
        do \(Ghc.L _ name) -> Ghc.moduleNameString name `Set.member` allowed
        do ideclAs

    areImportedNamesValid = case importedNames of
      Just (WhiteList allowedNames) ->
        maybe False (all (`Set.member` allowedNames)) (getExplicitlyImportedNames decl)
      Just (BlackList forbiddenNames) ->
        maybe True (Set.null . (forbiddenNames `Set.difference`) . Set.fromList) (getExplicitlyHiddenNames decl)
          && maybe False (not . any (`Set.member` forbiddenNames)) (getExplicitlyImportedNames decl)
      Nothing -> True