packages feed

fourmolu-0.17.0.0: src/Ormolu/Diff/ParseResult.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeepSubsumption #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}

-- | This module allows us to diff two 'ParseResult's.
module Ormolu.Diff.ParseResult
  ( ParseResultDiff (..),
    diffParseResult,
  )
where

import Data.ByteString (ByteString)
import Data.Char (isSpace)
import Data.Foldable
import Data.Function
import Data.Generics
import Data.List (sortOn)
import Data.Text qualified as T
import GHC.Hs
import GHC.Types.SourceText
import GHC.Types.SrcLoc
import Ormolu.Config.Gen (ImportGrouping (ImportGroupSingle))
import Ormolu.Imports (normalizeImports)
import Ormolu.Parser.CommentStream
import Ormolu.Parser.Result
import Ormolu.Utils

-- | Result of comparing two 'ParseResult's.
data ParseResultDiff
  = -- | Two parse results are the same
    Same
  | -- | Two parse results differ
    Different [RealSrcSpan]
  deriving (Show)

instance Semigroup ParseResultDiff where
  Same <> a = a
  a <> Same = a
  Different xs <> Different ys = Different (xs ++ ys)

instance Monoid ParseResultDiff where
  mempty = Same

-- | Return 'Diff' of two 'ParseResult's.
diffParseResult ::
  ParseResult ->
  ParseResult ->
  ParseResultDiff
diffParseResult
  ParseResult
    { prCommentStream = cstream0,
      prParsedSource = hs0
    }
  ParseResult
    { prCommentStream = cstream1,
      prParsedSource = hs1
    } =
    diffCommentStream cstream0 cstream1
      <> diffHsModule
        hs0 {hsmodImports = concat . normalizeImports False mempty ImportGroupSingle $ hsmodImports hs0}
        hs1 {hsmodImports = concat . normalizeImports False mempty ImportGroupSingle $ hsmodImports hs1}

diffCommentStream :: CommentStream -> CommentStream -> ParseResultDiff
diffCommentStream (CommentStream cs) (CommentStream cs')
  | commentLines cs == commentLines cs' = Same
  | otherwise = Different []
  where
    commentLines = concatMap (toList . unComment . unLoc)

-- | Compare two modules for equality disregarding the following aspects:
--
--     * 'SrcSpan's
--     * ordering of import lists
--     * style (ASCII vs Unicode) of arrows, colons
--     * LayoutInfo (brace style) in extension fields
--     * Empty contexts in type classes
--     * Parens around derived type classes
--     * 'TokenLocation' (in 'LHsToken'/'LHsUniToken')
--     * 'EpaLocation'
diffHsModule :: HsModule GhcPs -> HsModule GhcPs -> ParseResultDiff
diffHsModule = genericQuery
  where
    genericQuery :: GenericQ (GenericQ ParseResultDiff)
    genericQuery x y
      -- 'ByteString' implements 'Data' instance manually and does not
      -- implement 'toConstr', so we have to deal with it in a special way.
      | Just x' <- cast x,
        Just y' <- cast y =
          if x' == (y' :: ByteString)
            then Same
            else Different []
      | typeOf x == typeOf y,
        toConstr x == toConstr y =
          mconcat $
            gzipWithQ
              ( genericQuery
                  `extQ` considerEqual @SrcSpan
                  `ext1Q` epAnnEq
                  `extQ` considerEqual @SourceText
                  `extQ` hsDocStringEq
                  `extQ` importDeclQualifiedStyleEq
                  `extQ` classDeclCtxEq
                  `extQ` derivedTyClsEq
                  `extQ` qualTyCtxEq
                  `extQ` dataDeclEq
                  `extQ` considerEqual @EpAnnComments -- ~ XCGRHSs GhcPs
                  `extQ` considerEqual @TokenLocation -- in LHs(Uni)Token
                  `extQ` considerEqual @EpaLocation
                  `extQ` considerEqual @EpLayout
                  `extQ` considerEqual @[AddEpAnn]
                  `extQ` considerEqual @AnnSig
                  `extQ` considerEqual @HsRuleAnn
                  `ext2Q` forLocated
                  -- unicode-related
                  `extQ` considerEqual @(EpUniToken "->" "→")
                  `extQ` considerEqual @(EpUniToken "::" "∷")
                  `extQ` considerEqual @EpLinearArrow
                  `extQ` considerEqualVia' compareAnnKeywordId
              )
              x
              y
      | otherwise = Different []

    considerEqualVia ::
      forall a.
      (Typeable a) =>
      (a -> a -> ParseResultDiff) ->
      a ->
      GenericQ ParseResultDiff
    considerEqualVia f x (cast -> Just x') = f x x'
    considerEqualVia _ _ _ = Different []

    considerEqualVia' f =
      considerEqualVia $ \x x' -> if f x x' then Same else Different []

    considerEqual :: forall a. (Typeable a) => a -> GenericQ ParseResultDiff
    considerEqual = considerEqualVia $ \_ _ -> Same

    epAnnEq :: EpAnn a -> b -> ParseResultDiff
    epAnnEq _ _ = Same

    importDeclQualifiedStyleEq = considerEqualVia' f
      where
        f QualifiedPre QualifiedPost = True
        f QualifiedPost QualifiedPre = True
        f x x' = x == x'

    hsDocStringEq :: HsDocString -> GenericQ ParseResultDiff
    hsDocStringEq = considerEqualVia' ((==) `on` (map (T.dropWhile isSpace) . splitDocString True))

    forLocated ::
      (Data e0, Data e1) =>
      GenLocated e0 e1 ->
      GenericQ ParseResultDiff
    forLocated x@(L mspn _) y =
      maybe id appendSpan (cast `ext1Q` (Just . epAnnLoc) $ mspn) (genericQuery x y)
      where
        epAnnLoc :: EpAnn ann -> SrcSpan
        epAnnLoc = locA
    appendSpan :: SrcSpan -> ParseResultDiff -> ParseResultDiff
    appendSpan s' d@(Different ss) =
      case s' of
        RealSrcSpan s _ ->
          if not $ any (`isRealSubspanOf` s) ss
            then Different (s : ss)
            else d
        UnhelpfulSpan _ -> d
    appendSpan _ d = d

    -- The order of contexts doesn't matter
    normalizeContext :: HsContext GhcPs -> HsContext GhcPs
    normalizeContext = sortOn showOutputable

    normalizeMContext :: Maybe (LHsContext GhcPs) -> Maybe (LHsContext GhcPs)
    normalizeMContext Nothing = Nothing
    normalizeMContext (Just (L _ [])) = Nothing
    normalizeMContext (Just (L ann ctx)) = Just (L ann $ normalizeContext ctx)

    qualTyCtxEq :: HsType GhcPs -> GenericQ ParseResultDiff
    qualTyCtxEq = considerEqualVia $ \lt rt -> genericQuery (normalizeQualTy lt) (normalizeQualTy rt)
      where
        normalizeQualTy :: HsType GhcPs -> HsType GhcPs
        normalizeQualTy (HsQualTy ann ctx body) = HsQualTy ann (fmap normalizeContext ctx) body
        normalizeQualTy ty = ty

    classDeclCtxEq :: TyClDecl GhcPs -> GenericQ ParseResultDiff
    classDeclCtxEq = considerEqualVia $ \lc rc -> genericQuery (normalizeClassDecl lc) (normalizeClassDecl rc)
      where
        normalizeClassDecl ClassDecl {tcdCtxt, ..} = ClassDecl {tcdCtxt = normalizeMContext tcdCtxt, ..}
        normalizeClassDecl d = d

    dataDeclEq :: HsDataDefn GhcPs -> GenericQ ParseResultDiff
    dataDeclEq = considerEqualVia $ \dd dd' -> genericQuery (normalizeDataDecl dd) (normalizeDataDecl dd')

    normalizeDataDecl :: HsDataDefn GhcPs -> HsDataDefn GhcPs
    normalizeDataDecl HsDataDefn {dd_ctxt, dd_derivs, ..} =
      HsDataDefn
        { -- The order of classes in the context doesn't matter
          dd_ctxt = normalizeMContext dd_ctxt,
          -- The order of deriving clauses doesn't matter. Note: need to normalize before sorting, otherwise
          -- we'll get a different sort order!
          dd_derivs = sortOn showOutputable ((fmap . fmap) normalizeDerivingClause dd_derivs),
          ..
        }

    normalizeDerivingClause :: HsDerivingClause GhcPs -> HsDerivingClause GhcPs
    normalizeDerivingClause HsDerivingClause {deriv_clause_tys, ..} =
      HsDerivingClause {deriv_clause_tys = fmap normalizeDerivClauseTys deriv_clause_tys, ..}

    normalizeDerivClauseTys :: DerivClauseTys GhcPs -> DerivClauseTys GhcPs
    normalizeDerivClauseTys (DctSingle ann ty) = DctMulti ann [ty]
    -- The order of types in deriving clauses doesn't matter
    normalizeDerivClauseTys (DctMulti ann tys) = DctMulti ann (sortOn showOutputable tys)

    derivedTyClsEq :: DerivClauseTys GhcPs -> GenericQ ParseResultDiff
    derivedTyClsEq = considerEqualVia $ \lc rc -> genericQuery (normalizeDerivClauseTys lc) (normalizeDerivClauseTys rc)

    compareAnnKeywordId x y =
      let go = curry $ \case
            (AnnCloseB, AnnCloseBU) -> True
            (AnnCloseQ, AnnCloseQU) -> True
            (AnnDarrow, AnnDarrowU) -> True
            (AnnDcolon, AnnDcolonU) -> True
            (AnnForall, AnnForallU) -> True
            (AnnLarrow, AnnLarrowU) -> True
            (AnnOpenB, AnnOpenBU) -> True
            (AnnOpenEQ, AnnOpenEQU) -> True
            (AnnRarrow, AnnRarrowU) -> True
            (Annlarrowtail, AnnlarrowtailU) -> True
            (Annrarrowtail, AnnrarrowtailU) -> True
            (AnnLarrowtail, AnnLarrowtailU) -> True
            (AnnRarrowtail, AnnRarrowtailU) -> True
            (_, _) -> False
       in go x y || go y x || x == y