content: {-
    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.
  -}
  
  
  module Glean.Glass.Search.Class
    ( Search(..)
    , SearchResult(..)
    , SearchEntity(..)
    , ResultLocation
    , searchSymbolId
    , resultToDecl
    , CodeEntityLocation(..)
    , mapResultLocation
    ) where
  
  import Data.Text (Text, intercalate)
  import Util.Text (textShow)
  
  import Data.Typeable (Typeable)
  
  import qualified Glean
  
  import Glean.Angle ( Angle, Type )
  import Glean.Haxl.Repos as Glean
  
  import qualified Glean.Schema.Src.Types as Src
  import qualified Glean.Schema.CodemarkupTypes.Types as Code
  import qualified Glean.Schema.Code.Types as Code
  
  import Glean.Glass.Utils ( searchRecursiveWithLimit )
  
  -- Search-based inverse of Symbol.toSymbol :: a -> [Text]
  -- Decodes a symbol id to a code.Entity fact
  --
  -- Note: this is different to e.g. approximate string search, as we
  -- should _always_ be able to decode valid symbol ids back to their (unique*)
  -- entity.
  --
  -- There are cases where symbol ids are not unique:
  -- - weird code
  -- - hack namespaces
  -- - bugs/approximations in our encoder
  --
  -- We log the duplicates to glass_errors
  --
  class Search t where
    symbolSearch :: [Text] -> ReposHaxl u w (SearchResult t)
  
  -- | We have zero, one or multiple matches for entities
  data SearchResult t
    = None !Text -- no result found
    | One (SearchEntity t) -- preicsely one entity, yay.
    | Many -- oh dear, several
       { initial :: SearchEntity t
       , rest :: [SearchEntity t]
       , message :: !Text }
  
  data SearchEntity t =
    SearchEntity {
      entityRepo :: !Glean.Repo, -- vital to know which repo this came from
      decl :: !t
    }
  
  -- | Summary form used for describe()
  data CodeEntityLocation =
    CodeEntityLocation {
      entity :: !Code.Entity,
      entityFile :: !Src.File,
      entityRange :: !Code.RangeSpan,
      entityName :: !Text
    }
  
  -- Similar to SearchEntity , used in Angle data queries. Searches return this
  type ResultLocation t = (t, Src.File, Code.RangeSpan, Text)
  
  mapResultLocation :: (t -> t') -> ResultLocation t -> ResultLocation t'
  mapResultLocation f (res, file, range, name) = (f res, file, range, name)
  
  resultToDecl :: [(d, a, b)] -> [d]
  resultToDecl = map (\(x, _, _) -> x)
  
  -- We tend to map over the decl building up layers
  instance Functor SearchResult where
    fmap _ (None t) = None t
    fmap f (One e) = One (e { decl = f (decl e) })
    fmap f m@Many { initial = e, rest = es } =
      m { initial = e { decl = f (decl e)}
        , rest = map (\e -> e { decl = f (decl e) }) es }
  
  -- | Find matching code.Entity values by repo and language for the
  -- symbol id tokens.
  --
  -- symbol ids are "mostly" unique. This code checks explicitly if the
  -- symbol id search generated 0, 1 or >1 result. We always return the first
  -- match.
  --
  -- There are some scenarios where we might want to return all matches.
  --
  searchSymbolId :: (Typeable t, Show t, Type t)
    => [Text]
    -> Angle t
    -> ReposHaxl u w (SearchResult t)
  searchSymbolId toks query = do
    results <- Glean.queryAllRepos $ do
      repo <- Glean.haxlRepo
      (results,_truncated) <- searchRecursiveWithLimit (Just max_symbolid_matches)
        query
      return $ map (repo,) results
    let toksText = intercalate "/" toks
    return $ case results of
      [] -> None $ "runSearch: No results found for " <> toksText
      [(entityRepo, decl)] -> One SearchEntity{..}
      (firstResult:moreResults) ->
        Many { initial = uncurry mkSearchEntity firstResult
             , rest = map (uncurry mkSearchEntity) moreResults
             , message = "runSearch: " <> textShow (length results) <>
              " results found for " <> toksText
        }
    where
      mkSearchEntity entityRepo decl = SearchEntity{..}
  
      -- symbol ids do have collisions, but they should be rare. If they are
      -- expensive it means we have a bad query from symbold id to entity
      max_symbolid_matches = 10
  