diff --git a/src/DiffSummary.hs b/src/DiffSummary.hs index 001cd79d3..766e85306 100644 --- a/src/DiffSummary.hs +++ b/src/DiffSummary.hs @@ -2,7 +2,7 @@ module DiffSummary (DiffSummary(..), diffSummaries, DiffInfo(..), annotatedSummaries) where -import Prologue hiding (snd, intercalate) +import Prologue hiding (intercalate) import Diff import Patch import Term @@ -11,7 +11,8 @@ import Range import Syntax as S import Category as C import Data.Functor.Foldable as Foldable -import Data.Functor.Both +import Data.Functor.Both hiding (fst, snd) +import qualified Data.Functor.Both as Both import Data.Text as Text (intercalate) import Test.QuickCheck hiding (Fixed) import Patch.Arbitrary() @@ -26,6 +27,39 @@ data DiffInfo = LeafInfo { categoryName :: Text, termName :: Text } | ErrorInfo { errorSpan :: SourceSpan, categoryName :: Text } deriving (Eq, Show) +data Branch = BIndexed | BFixed | BCommented deriving (Show, Eq, Generic) + +data DiffSummary a = DiffSummary { + patch :: Patch a, + parentAnnotation :: Maybe (Category, Text) +} deriving (Eq, Functor, Show, Generic) + +annotatedSummaries :: DiffSummary DiffInfo -> [Text] +annotatedSummaries DiffSummary{..} = show . (P.<> maybeParentContext parentAnnotation) <$> summaries patch + +diffSummaries :: (HasCategory leaf, HasField fields Category, HasField fields Range) => Both (Source Char) -> Diff leaf (Record fields) -> [DiffSummary DiffInfo] +diffSummaries sources = para $ \diff -> + let diff' = free (Prologue.fst <$> diff) + annotateWithCategory :: [(Diff leaf (Record fields), [DiffSummary DiffInfo])] -> [DiffSummary DiffInfo] + annotateWithCategory children = maybeToList (prependSummary (Both.snd sources) <$> (afterTerm diff')) <*> (children >>= snd) in + case diff of + -- Skip comments and leaves since they don't have any changes + (Free (_ :< syntax)) -> annotateWithCategory (toList syntax) + (Pure patch) -> [ DiffSummary (mapPatch (termToDiffInfo beforeSource) (termToDiffInfo afterSource) patch) Nothing ] + where + (beforeSource, afterSource) = runJoin sources + + +summaries :: Patch DiffInfo -> [P.Doc] +summaries (Insert info) = (("Added" <+> "the") <+>) <$> toLeafInfos info +summaries (Delete info) = (("Deleted" <+> "the") <+>) <$> toLeafInfos info +summaries (Replace i1 i2) = zipWith (\a b -> "Replaced" <+> "the" <+> a <+> "with the" <+> b) (toLeafInfos i1) (toLeafInfos i2) + +toLeafInfos :: DiffInfo -> [Doc] +toLeafInfos LeafInfo{..} = pure $ squotes (toDoc termName) <+> (toDoc categoryName) +toLeafInfos BranchInfo{..} = pretty <$> branches +toLeafInfos err@ErrorInfo{} = pure $ pretty err + toTermName :: (HasCategory leaf, HasField fields Category, HasField fields Range) => Source Char -> Term leaf (Record fields) -> Text toTermName source term = case unwrap term of S.Fixed children -> fromMaybe "branch" $ (toCategoryName . category) . extract <$> head children @@ -81,9 +115,55 @@ toTermName source term = case unwrap term of termNameFromRange range = toText $ Source.slice range source range = characterRange . extract +maybeParentContext :: Maybe (Category, Text) -> Doc +maybeParentContext = maybe "" (\annotation -> + space <> "in the" <+> (toDoc $ snd annotation) <+> toDoc (toCategoryName $ fst annotation)) + +toDoc :: Text -> Doc +toDoc = string . toS + +termToDiffInfo :: (HasCategory leaf, HasField fields Category, HasField fields Range) => Source Char -> Term leaf (Record fields) -> DiffInfo +termToDiffInfo blob term = case unwrap term of + Leaf _ -> LeafInfo (toCategoryName term) (toTermName' term) + S.Indexed children -> BranchInfo (termToDiffInfo' <$> children) (toCategoryName term) BIndexed + S.Fixed children -> BranchInfo (termToDiffInfo' <$> children) (toCategoryName term) BFixed + S.FunctionCall identifier _ -> LeafInfo (toCategoryName term) (toTermName' identifier) + S.Ternary ternaryCondition _ -> LeafInfo (toCategoryName term) (toTermName' ternaryCondition) + S.Function identifier _ _ -> LeafInfo (toCategoryName term) (maybe "anonymous" toTermName' identifier) + S.Assignment identifier _ -> LeafInfo (toCategoryName term) (toTermName' identifier) + S.MathAssignment identifier _ -> LeafInfo (toCategoryName term) (toTermName' identifier) + -- Currently we cannot express the operator for an operator production from TreeSitter. Eventually we should be able to + -- use the term name of the operator identifier when we have that production value. Until then, I'm using a placeholder value + -- to indicate where that value should be when constructing DiffInfos. + Commented cs leaf -> BranchInfo (termToDiffInfo' <$> cs <> maybeToList leaf) (toCategoryName term) BCommented + S.Error sourceSpan _ -> ErrorInfo sourceSpan (toCategoryName term) + _ -> LeafInfo (toCategoryName term) (toTermName' term) + where toTermName' = toTermName blob + termToDiffInfo' = termToDiffInfo blob + +prependSummary :: (HasCategory leaf, HasField fields Range, HasField fields Category) => Source Char -> Term leaf (Record fields) -> DiffSummary DiffInfo -> DiffSummary DiffInfo +prependSummary source term summary = if (isNothing $ parentAnnotation summary) && hasIdentifier term + then summary { parentAnnotation = Just (category $ extract term, toTermName source term) } + else summary + where hasIdentifier term = case unwrap term of + S.FunctionCall{} -> True + S.Function id _ _ -> isJust id + S.Assignment{} -> True + S.MathAssignment{} -> True + S.MemberAccess{} -> True + S.MethodCall{} -> True + S.VarAssignment{} -> True + S.SubscriptAccess{} -> True + S.Class{} -> True + S.Method{} -> True + _ -> False + +-- The user-facing category name of 'a'. class HasCategory a where toCategoryName :: a -> Text +-- Instances + instance HasCategory Text where toCategoryName = identity @@ -135,16 +215,10 @@ instance HasCategory Category where instance (HasCategory leaf, HasField fields Category) => HasCategory (Term leaf (Record fields)) where toCategoryName = toCategoryName . category . extract -data Branch = BIndexed | BFixed | BCommented deriving (Show, Eq, Generic) instance Arbitrary Branch where arbitrary = oneof [ pure BIndexed, pure BFixed ] shrink = genericShrink -data DiffSummary a = DiffSummary { - patch :: Patch a, - parentAnnotations :: [Category] -} deriving (Eq, Functor, Show, Generic) - instance (Eq a, Arbitrary a) => Arbitrary (DiffSummary a) where arbitrary = DiffSummary <$> arbitrary <*> arbitrary shrink = genericShrink @@ -153,88 +227,3 @@ instance P.Pretty DiffInfo where pretty LeafInfo{..} = squotes (string $ toSL termName) <+> (string $ toSL categoryName) pretty BranchInfo{..} = mconcat $ punctuate (string "," <> space) (pretty <$> branches) pretty ErrorInfo{..} = "syntax error at" <+> (string . toSL $ displayStartEndPos errorSpan) <+> "in" <+> (string . toSL $ spanName errorSpan) - -annotatedSummaries :: DiffSummary DiffInfo -> [Text] -annotatedSummaries DiffSummary{..} = show . (P.<> maybeParentContext parentAnnotations) <$> summaries patch - -summaries :: Patch DiffInfo -> [P.Doc] -summaries (Insert info) = (("Added" <+> "the") <+>) <$> toLeafInfos info -summaries (Delete info) = (("Deleted" <+> "the") <+>) <$> toLeafInfos info -summaries (Replace i1 i2) = zipWith (\a b -> "Replaced" <+> "the" <+> a <+> "with the" <+> b) (toLeafInfos i1) (toLeafInfos i2) - -toLeafInfos :: DiffInfo -> [Doc] -toLeafInfos LeafInfo{..} = pure $ squotes (toDoc termName) <+> (toDoc categoryName) -toLeafInfos BranchInfo{..} = pretty <$> branches -toLeafInfos err@ErrorInfo{} = pure $ pretty err - -maybeParentContext :: [Category] -> Doc -maybeParentContext annotations = if null annotations - then "" - else space <> "in the" <+> (toDoc . intercalate "/" $ toCategoryName <$> annotations) <+> "context" -toDoc :: Text -> Doc -toDoc = string . toS - -diffSummaries :: (HasCategory leaf, HasField fields Category, HasField fields Range) => Both (Source Char) -> Diff leaf (Record fields) -> [DiffSummary DiffInfo] -diffSummaries sources = cata $ \case - -- Skip comments and leaves since they don't have any changes - (Free (_ :< Leaf _)) -> [] - Free (_ :< (S.Comment _)) -> [] - (Free (infos :< S.Indexed children)) -> annotateWithCategory infos <$> join children - (Free (infos :< S.Fixed children)) -> annotateWithCategory infos <$> join children - (Free (infos :< S.FunctionCall identifier children)) -> annotateWithCategory infos <$> join (Prologue.toList (identifier : children)) - (Free (infos :< S.Function id ps body)) -> annotateWithCategory infos <$> (fromMaybe [] id) <> (fromMaybe [] ps) <> body - (Free (infos :< S.Assignment id value)) -> annotateWithCategory infos <$> id <> value - (Free (infos :< S.MemberAccess base property)) -> annotateWithCategory infos <$> base <> property - (Free (infos :< S.SubscriptAccess base property)) -> annotateWithCategory infos <$> base <> property - (Free (infos :< S.MethodCall targetId methodId ps)) -> annotateWithCategory infos <$> targetId <> methodId <> ps - (Free (infos :< S.VarAssignment varId value)) -> annotateWithCategory infos <$> varId <> value - (Free (infos :< S.VarDecl decl)) -> annotateWithCategory infos <$> decl - (Free (infos :< S.Args args)) -> annotateWithCategory infos <$> join args - (Free (infos :< S.Switch expr cases)) -> annotateWithCategory infos <$> expr <> join cases - (Free (infos :< S.Case expr body)) -> annotateWithCategory infos <$> expr <> body - Free (infos :< (S.Ternary expr cases)) -> annotateWithCategory infos <$> expr <> join cases - Free (infos :< (S.MathAssignment id value)) -> annotateWithCategory infos <$> id <> value - Free (infos :< (S.Operator syntaxes)) -> annotateWithCategory infos <$> join syntaxes - Free (infos :< (S.Object kvs)) -> annotateWithCategory infos <$> join kvs - Free (infos :< (S.Return expr)) -> annotateWithCategory infos <$> fromMaybe [] expr - Free (infos :< (S.Pair a b)) -> annotateWithCategory infos <$> a <> b - Free (infos :< (S.Commented cs leaf)) -> annotateWithCategory infos <$> join cs <> fromMaybe [] leaf - Free (infos :< (S.Error _ children)) -> annotateWithCategory infos <$> join children - (Free (infos :< S.For exprs body)) -> annotateWithCategory infos <$> join exprs <> body - (Free (infos :< S.While expr body)) -> annotateWithCategory infos <$> expr <> body - (Free (infos :< S.DoWhile expr body)) -> annotateWithCategory infos <$> expr <> body - (Free (infos :< S.Throw expr)) -> annotateWithCategory infos <$> expr - (Free (infos :< S.Constructor expr)) -> annotateWithCategory infos <$> expr - (Free (infos :< S.Try expr catch finally)) -> annotateWithCategory infos <$> expr <> fromMaybe [] catch <> fromMaybe [] finally - (Free (infos :< S.Array children)) -> annotateWithCategory infos <$> join children - (Free (infos :< S.Class identifier superclass definitions)) -> annotateWithCategory infos <$> identifier <> fromMaybe [] superclass <> join definitions - (Free (infos :< S.Method identifier params definitions)) -> annotateWithCategory infos <$> identifier <> join params <> join definitions - (Pure (Insert term)) -> [ DiffSummary (Insert $ termToDiffInfo afterSource term) [] ] - (Pure (Delete term)) -> [ DiffSummary (Delete $ termToDiffInfo beforeSource term) [] ] - (Pure (Replace t1 t2)) -> [ DiffSummary (Replace (termToDiffInfo beforeSource t1) (termToDiffInfo afterSource t2)) [] ] - where - (beforeSource, afterSource) = runJoin sources - annotateWithCategory infos = prependSummary (category $ snd infos) - - -termToDiffInfo :: (HasCategory leaf, HasField fields Category, HasField fields Range) => Source Char -> Term leaf (Record fields) -> DiffInfo -termToDiffInfo blob term = case unwrap term of - Leaf _ -> LeafInfo (toCategoryName term) (toTermName' term) - S.Indexed children -> BranchInfo (termToDiffInfo' <$> children) (toCategoryName term) BIndexed - S.Fixed children -> BranchInfo (termToDiffInfo' <$> children) (toCategoryName term) BFixed - S.FunctionCall identifier _ -> LeafInfo (toCategoryName term) (toTermName' identifier) - S.Ternary ternaryCondition _ -> LeafInfo (toCategoryName term) (toTermName' ternaryCondition) - S.Function identifier _ _ -> LeafInfo (toCategoryName term) (maybe "anonymous" toTermName' identifier) - S.Assignment identifier _ -> LeafInfo (toCategoryName term) (toTermName' identifier) - S.MathAssignment identifier _ -> LeafInfo (toCategoryName term) (toTermName' identifier) - -- Currently we cannot express the operator for an operator production from TreeSitter. Eventually we should be able to - -- use the term name of the operator identifier when we have that production value. Until then, I'm using a placeholder value - -- to indicate where that value should be when constructing DiffInfos. - Commented cs leaf -> BranchInfo (termToDiffInfo' <$> cs <> maybeToList leaf) (toCategoryName term) BCommented - S.Error sourceSpan _ -> ErrorInfo sourceSpan (toCategoryName term) - _ -> LeafInfo (toCategoryName term) (toTermName' term) - where toTermName' = toTermName blob - termToDiffInfo' = termToDiffInfo blob - -prependSummary :: Category -> DiffSummary DiffInfo -> DiffSummary DiffInfo -prependSummary annotation summary = summary { parentAnnotations = annotation : parentAnnotations summary } diff --git a/src/Patch.hs b/src/Patch.hs index c4dcc8e69..a156f01f2 100644 --- a/src/Patch.hs +++ b/src/Patch.hs @@ -9,6 +9,7 @@ module Patch , patchSum , maybeFst , maybeSnd +, mapPatch ) where import Data.These @@ -51,6 +52,11 @@ unPatch (Replace a b) = These a b unPatch (Insert b) = That b unPatch (Delete a) = This a +mapPatch :: (a -> b) -> (a -> b) -> Patch a -> Patch b +mapPatch f _ (Delete a ) = Delete (f a) +mapPatch _ g (Insert b) = Insert (g b) +mapPatch f g (Replace a b) = Replace (f a) (g b) + -- | Calculate the cost of the patch given a function to compute the cost of a item. patchSum :: (a -> Integer) -> Patch a -> Integer patchSum termCost patch = maybe 0 termCost (before patch) + maybe 0 termCost (after patch) diff --git a/test/DiffSummarySpec.hs b/test/DiffSummarySpec.hs index dc648c4bf..e3f0710e3 100644 --- a/test/DiffSummarySpec.hs +++ b/test/DiffSummarySpec.hs @@ -31,10 +31,10 @@ testDiff :: Diff Text (Record '[Category, Range]) testDiff = free $ Free (pure arrayInfo :< Indexed [ free $ Pure (Insert (cofree $ literalInfo :< Leaf "a")) ]) testSummary :: DiffSummary DiffInfo -testSummary = DiffSummary { patch = Insert (LeafInfo "string" "a"), parentAnnotations = [] } +testSummary = DiffSummary { patch = Insert (LeafInfo "string" "a"), parentAnnotation = Nothing } replacementSummary :: DiffSummary DiffInfo -replacementSummary = DiffSummary { patch = Replace (LeafInfo "string" "a") (LeafInfo "symbol" "b"), parentAnnotations = [ ArrayLiteral ] } +replacementSummary = DiffSummary { patch = Replace (LeafInfo "string" "a") (LeafInfo "symbol" "b"), parentAnnotation = Just (Info.FunctionCall, "foo") } sources :: Both (Source Char) sources = both (fromText "[]") (fromText "[a]") @@ -43,7 +43,7 @@ spec :: Spec spec = parallel $ do describe "diffSummaries" $ do it "outputs a diff summary" $ do - diffSummaries sources testDiff `shouldBe` [ DiffSummary { patch = Insert (LeafInfo "string" "a"), parentAnnotations = [ ArrayLiteral ] } ] + diffSummaries sources testDiff `shouldBe` [ DiffSummary { patch = Insert (LeafInfo "string" "a"), parentAnnotation = Nothing } ] prop "equal terms produce identity diffs" $ \ a -> let term = toTerm (a :: ArbitraryTerm Text (Record '[Category, Range])) in @@ -53,7 +53,7 @@ spec = parallel $ do it "should print adds" $ annotatedSummaries testSummary `shouldBe` ["Added the 'a' string"] it "prints a replacement" $ do - annotatedSummaries replacementSummary `shouldBe` ["Replaced the 'a' string with the 'b' symbol in the array context"] + annotatedSummaries replacementSummary `shouldBe` ["Replaced the 'a' string with the 'b' symbol in the foo function call"] describe "DiffInfo" $ do prop "patches in summaries match the patches in diffs" $ \a -> let