packages feed

glean-0.1.0.0: glean/glass/Glean/Glass/SearchRelated.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}

module Glean.Glass.SearchRelated
  ( searchRelatedEntities
  , searchInheritedEntities
  , searchRecursiveEntities
  , Recursive(..)
  , SearchStyle(..)
  , RelatedLocatedEntities(..)
  , RelatedEntities(..)
  , LocatedEntity
  , InheritedContainer
  , edgesToTopoMap
  ) where

import Control.Monad (forM)
import Control.Monad.Catch (MonadThrow(throwM))
import Data.Hashable ( Hashable(..) )
import Data.HashSet ( HashSet )
import Data.List (groupBy, sortOn)
import Data.Function (on)
import Data.HashMap.Strict ( HashMap )
import qualified Data.HashMap.Strict as HM
import GHC.Generics (Generic)
import qualified Data.HashSet as HashSet

import qualified Glean
import Glean.Angle as Angle
import qualified Glean.Schema.CodemarkupTypes.Types as Code
import qualified Glean.Schema.Codemarkup.Types as Code
import qualified Glean.Schema.Code.Types as Code

import Glean.Glass.Search.Class
import Glean.Glass.Base (GleanPath (..))
import Glean.Glass.Path
import Glean.Glass.SymbolId (entityToAngle, toSymbolId)
import Glean.Glass.Types
import Glean.Glass.Utils (searchRecursiveWithLimit)
import Glean.Haxl.Repos (RepoHaxl)

-- | Whether to expand relationships recursively
data Recursive
  = Recursive
  | NotRecursive
  deriving Eq

-- | Whether to hide uninteresting things
data SearchStyle
  = ShowAll
  | HideUninteresting

-- | Pairs of edges of related entities
data RelatedEntities = RelatedEntities
  { parentEntity :: !Code.Entity
  , parentLocation :: {-# UNPACK #-}!Code.Location
  , childEntity :: !Code.Entity
  , childLocation :: {-# UNPACK #-}!Code.Location
  } deriving (Eq,Generic,Hashable)

-- | Pairs of edges of related entities, with all metadata
data RelatedLocatedEntities = RelatedLocatedEntities
  { parentRL :: LocatedEntity
  , childRL :: LocatedEntity
  } deriving (Generic,Eq,Hashable)

type LocatedEntity = (ResultLocation Code.Entity, SymbolId)

-- Convenience type for a parent with a set of contained children
type InheritedContainer = (LocatedEntity, [LocatedEntity])

styleToCode :: SearchStyle -> Code.SearchStyle
styleToCode ShowAll = Code.SearchStyle_ShowAll
styleToCode HideUninteresting = Code.SearchStyle_HideUninteresting

-- | Flatten results into the container-level topological ordering
-- from child to set of parents it inherits from.
edgesToTopoMap
  :: [RelatedLocatedEntities] -> HashMap SymbolId (HashSet SymbolId)
edgesToTopoMap edges = HM.fromListWith HashSet.union
  [ (snd (childRL e), HashSet.singleton (snd (parentRL e))) | e <- edges ]

-- returns all inherited members in container entity as "RelatedEntity"
-- parentEntity is the entity defining the member
-- childEntity is the member itself
runSearchInherited
  :: SearchStyle
  -> Int
  -> Angle Code.Entity
  -> RepoHaxl u w [RelatedEntities]
runSearchInherited style limit angle = do
  (entities, _truncated) <- searchRecursiveWithLimit (Just limit) $
    query angle
  pure $
    [ RelatedEntities
      { parentEntity = parentEntity_parent
      , childEntity = childEntity_child
      , parentLocation = parentEntity_location
      , childLocation = childEntity_location
      }
    | (Code.ParentEntity{..}, Code.ChildEntity{..}) <- entities ]
  where
    styleTy = styleToCode style
    query
      :: Angle Code.Entity
      -> Angle (Code.ParentEntity, Code.ChildEntity)
    query entity =
      vars $ \(parent :: Angle Code.ParentEntity)
          (child :: Angle Code.ChildEntity) ->
        tuple (parent, child) `where_` [
          wild .= predicate @Code.SearchInheritedEntities (
            rec $
              field @"base" entity $
              field @"parent" parent $
              field @"child" child $
              field @"style" (enum styleTy)
            end)
          ]

-- returns all inherited members in container entity
-- grouped by parentEntity
searchInheritedEntities
  :: SearchStyle
  -> Int
  -> Code.Entity
  -> RepoName
  -> RepoHaxl u w [InheritedContainer]
searchInheritedEntities style limit entity repo = do
  angle <- case entityToAngle entity of
        Right angle -> return angle
        Left t -> throwM (ServerException t)
  relatedEntities <- runSearchInherited style limit angle >>= toSymbolIds repo
  return $ groupChildEntities relatedEntities
  where
    groupChildEntities :: [RelatedLocatedEntities] -> [InheritedContainer]
    groupChildEntities xs =
      map (\g -> (parentRL (head g), map childRL g)) grouped
      where
        sorted = sortOn parentRL xs
        grouped = groupBy ((==) `on` parentRL) sorted

--
-- Given some search parameters, find entities by relation
--
searchRelatedEntities
  :: Int
  -> SearchStyle
  -> Recursive
  -> RelationDirection
  -> RelationType
  -> Code.Entity
  -> RepoName
  -> RepoHaxl u w [RelatedLocatedEntities]
searchRelatedEntities limit hide recurse dir rel entity repo =
  toSymbolIds repo =<< searchRelation opts limit [entity] HashSet.empty
  where
    opts = SearchOptions limit hide recurse rel dir

-- | For internal searches, we don't need the symbol id. So we can be slightly
-- more efficient. TODO: we could also avoid the location in the result entirely
-- used for lucky search/context resolution
searchRecursiveEntities
  :: Int -> RelationDirection -> RelationType -> Code.Entity
  -> RepoHaxl u w [RelatedEntities]
searchRecursiveEntities limit dir rel entity =
    searchRelation opts limit [entity] HashSet.empty
  where
    opts = SearchOptions limit ShowAll Recursive rel dir

-- | Lift entity search results into pairs of entities that we found,
-- along with their location and symbol id
--
-- TODO: we will be recomputing these for some symbols many times,
-- and in some cases we are dropping them as well. Can we defer this until
-- later?
--
toSymbolIds
  :: RepoName -> [RelatedEntities] -> RepoHaxl u w [RelatedLocatedEntities]
toSymbolIds repo edges = mapM locatePairs edges
  where
    locatePairs RelatedEntities{..} = RelatedLocatedEntities
      <$> mkLocate parentEntity parentLocation
      <*> mkLocate childEntity childLocation
    mkLocate entity Code.Location{..} = do
      symId <- symbol entity location_file
      return ((entity,location_file,location_location,location_name), symId)
    symbol entity file = do
      path <- GleanPath <$> Glean.keyOf file
      toSymbolId (fromGleanPath repo path) entity

-- | Constants to determine how we search
data SearchOptions = SearchOptions {
  totalLimit :: Int,
  style :: SearchStyle,
  recursive :: Recursive,
  relation :: RelationType,
  direction :: RelationDirection
}

--
-- Search driver, expand search until done, returning pairs of edges
-- of entity relationships.
--
searchRelation
  :: SearchOptions
  -> Int
  -> [Code.Entity]
  -> HashSet RelatedEntities
  -> RepoHaxl u w [RelatedEntities]
searchRelation opts@SearchOptions{..} limit toVisit visited =
    runSearch searchFn opts limit toVisit visited
  where
    -- the search flavor is known up front, so we can partially apply
    baseSearchFn = \ty angle -> runSearchRelated totalLimit style angle ty
    searchFn = case (relation, direction) of
      (RelationType_Extends, RelationDirection_Parent) ->
        baseSearchFn Code.RelationType_ExtendsParentOfChild
      (RelationType_Extends, RelationDirection_Child) ->
        baseSearchFn Code.RelationType_ExtendsChildOfParent
      (RelationType_Contains, RelationDirection_Parent) ->
        baseSearchFn Code.RelationType_ContainsParentOfChild
      (RelationType_Contains, RelationDirection_Child) ->
        baseSearchFn Code.RelationType_ContainsChildOfParent
      (RelationType_RequireExtends, RelationDirection_Parent) ->
        baseSearchFn Code.RelationType_RequireExtendsParentOfChild
      (RelationType_RequireImplements, RelationDirection_Parent) ->
        baseSearchFn Code.RelationType_RequireImplementsParentOfChild
      (RelationType_RequireClass, RelationDirection_Parent) ->
        baseSearchFn Code.RelationType_RequireClassParentOfChild
      _ -> \_angle -> pure []

-- recursively search up to limit
runSearch
  :: ([Angle Code.Entity] -> RepoHaxl u w [RelatedEntities])
  -> SearchOptions
  -> Int
  -> [Code.Entity]
  -> HashSet RelatedEntities
  -> RepoHaxl u w [RelatedEntities]
runSearch searchFn opts@SearchOptions{..} !limit toVisit visited = do
  angle <- forM toVisit $ \entity ->
    case entityToAngle entity of
      Right angle -> return angle
      Left t -> throwM (ServerException t)
  justVisited <- searchFn angle
  let newlyVisited = HashSet.fromList justVisited `HashSet.difference` visited
      visited' = visited `HashSet.union` newlyVisited
      toVisit = HashSet.toList $ case direction of
        RelationDirection_Parent -> HashSet.map parentEntity newlyVisited
        RelationDirection_Child -> HashSet.map childEntity newlyVisited
        _ -> HashSet.empty -- unknown direction
      recLimit = limit - length visited'
  if
    recursive == Recursive && recLimit > 0 && recLimit < limit &&
    not (null toVisit)
  then
    runSearch searchFn opts recLimit toVisit visited'
  else
    return $! HashSet.toList visited'

runSearchRelated
  :: Int
  -> SearchStyle
  -> [Angle Code.Entity]
  -> Code.RelationType
  -> RepoHaxl u w [RelatedEntities]
runSearchRelated limit style angle searchType = do
  (entities, _truncated) <- searchRecursiveWithLimit (Just limit) $
    searchRelatedEntitiesQ searchType styleType entities
  pure $
    [ RelatedEntities
      { parentEntity = parentEntity_parent
      , childEntity = childEntity_child
      , parentLocation = parentEntity_location
      , childLocation = childEntity_location
      }
    | Code.SearchRelatedEntities{..} <- entities
    , Just Code.SearchRelatedEntities_key{..} <- [searchRelatedEntities_key]
    , let Code.ParentEntity{..} = searchRelatedEntities_key_parent
    , let Code.ChildEntity{..} = searchRelatedEntities_key_child
    ]
  where
    entities = elementsOf (array angle)
    styleType = styleToCode style

--
-- unified search by relation
--
searchRelatedEntitiesQ
  :: Code.RelationType
  -> Code.SearchStyle
  -> Angle Code.Entity
  -> Angle Code.SearchRelatedEntities
searchRelatedEntitiesQ queryTy styleTy entity = case queryTy of
    Code.RelationType_ExtendsParentOfChild -> keyedByChild
    Code.RelationType_ContainsParentOfChild -> keyedByChild
    Code.RelationType_ExtendsChildOfParent -> keyedByParent
    Code.RelationType_ContainsChildOfParent -> keyedByParent
    Code.RelationType_RequireClassParentOfChild -> keyedByChild
    Code.RelationType_RequireImplementsParentOfChild -> keyedByChild
    Code.RelationType_RequireExtendsParentOfChild -> keyedByChild
    Code.RelationType__UNKNOWN _ -> error "Unkonwn Code.RelationType"
  where
    keyedByChild = predicate @Code.SearchRelatedEntities $
      rec $ -- bind to child entity
        field @"query" (enum queryTy) $
        field @"style" (enum styleTy) $
        field @"child" (rec $ field @"child" entity end)
      end
    keyedByParent = predicate @Code.SearchRelatedEntities $
      rec $ -- search by parent entities
        field @"query" (enum queryTy) $
        field @"style" (enum styleTy) $
        field @"parent" (rec $ field @"parent" entity end)
      end