glean-0.1.0.0: glean/glass/Glean/Glass/Handler/Symbols.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 ConstraintKinds #-}
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Glean.Glass.Handler.Symbols
(
-- ** find references
findReferenceRanges
-- * working with symbol ids
, symbolLocation
, describeSymbol
, resolveSymbols
-- * searching by string
, searchSymbol
-- ** by relationship
, searchRelated
-- ** a large report of all information we know about a symbol
, searchRelatedNeighborhood
) where
import Control.Monad
import Control.Exception ( throwIO )
import Control.Monad.Catch ( throwM )
import Data.Bifunctor (second)
import Data.Either.Extra (eitherToMaybe)
import Data.Hashable
import Data.List as List ( sortOn )
import Data.List.Extra ( nubOrd, nubOrdOn, groupOn, groupSortOn )
import Data.List.NonEmpty (NonEmpty(..), nonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Maybe
import Data.Ord ( comparing )
import Data.Text ( Text )
import qualified Data.Set as Set
import Data.Set ( Set )
import qualified Control.Concurrent.Async as Async
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import Util.List ( uniq, uniqBy )
import Glean.Angle as Angle ( Angle )
import qualified Glean
import Glean.Haxl as Glean ( keyOf, haxlRepo )
import Glean.Haxl.Repos as Glean
import qualified Glean.Schema.CodemarkupTypes.Types as Code
import qualified Glean.Schema.Code.Types as Code
import Glean.Glass.Base (GleanPath(GleanPath))
import Glean.Glass.Describe
( mkSymbolDescription
, describeEntity
, mkBriefSymbolDescription
)
import Glean.Glass.Handler.Utils
import Glean.Glass.Logging
import Glean.Glass.Repos
import Glean.Glass.Path ( fromGleanPath )
import Glean.Glass.Range
import Glean.Glass.SymbolId
import Glean.Glass.Types
import Glean.Glass.RepoMapping (
supportsCxxDeclarationSources
)
import Glean.Glass.Search.Class ( ResultLocation )
import qualified Glean.Glass.Env as Glass
import qualified Glean.Glass.Query as Query
import Glean.Glass.NameSearch (
SearchCase(..), SearchType(..),
SearchScope(..), SearchMode(..),
SearchQuery(..), SingleSymbol, FeelingLuckyResult(..),
QueryExpr(..), RepoSearchResult, SymbolSearchData(..),
toSearchResult, ToSearchResult(..), AngleSearch(..), srEntity,
buildLuckyContainerQuery, buildSearchQuery, dedupSearchResult
)
import Glean.Glass.XRefs ( GenXRef(..) )
import Glean.Glass.Search as Search
( CodeEntityLocation(..),
SearchEntity(..),
SearchResult(Many, None, One, rest, message, initial),
searchEntity,
searchEntityLocation )
import Glean.Glass.Utils
import Glean.Glass.Attributes.SymbolKind
( symbolKindToSymbolKind, symbolKindFromSymbolKind )
import qualified Glean.Glass.SearchRelated as Search
import Glean.Glass.SearchRelated
( RelatedLocatedEntities(childRL, parentRL),
Recursive(NotRecursive) )
import Glean.Glass.Neighborhood ( searchNeighborhood )
import qualified Glean.Glass.Handler.Utils as Utils
import Glean.Glass.SymbolKind (findSymbolKind)
import Glean.Glass.Env (Env' (tracer, sourceControl, useSnapshotsForSymbolsList))
-- | Symbol-based find-references.
findReferenceRanges
:: Glass.Env -> SymbolId -> RequestOptions -> IO [LocationRange]
findReferenceRanges env@Glass.Env{..} sym opts@RequestOptions{..} =
withSymbol "findReferenceRanges" env opts sym
$ \gleanDBs _dbInfo (repo, lang, toks) ->
fetchSymbolReferenceRanges env repo lang toks limit
GleanBackend{..}
where
limit = fmap fromIntegral requestOptions_limit
-- | Resolve a symbol identifier to its range-based location in the latest db
-- This is about 10x cheaper than a full describeSymbol() call
symbolLocation :: Glass.Env -> SymbolId -> RequestOptions -> IO SymbolLocation
symbolLocation env@Glass.Env{..} sym opts =
withSymbol "symbolLocation" env opts sym $
\gleanDBs dbInfo (scmRepo, lang, toks) ->
backendRunHaxl GleanBackend{..} env $ do
r <- Search.searchEntityLocation lang toks
(entity, err) <- case r of
None t -> throwM (ServerException t)
One e -> return (e, Nothing)
Many { initial = e, message = t } ->
return
(e, Just (GlassExceptionReason_entitySearchFail t))
let SearchEntity { entityRepo, decl = (_,file,rangespan,_) } = entity
let scmRevs = scmRevisions dbInfo
(, fmap logError err) <$> withRepo entityRepo (do
location <- rangeSpanToLocationRange scmRepo file rangespan
repo_hash <- getRepoHashForLocation location scmRevs <$> Glean.haxlRepo
return $ SymbolLocation {
symbolLocation_location = location,
symbolLocation_revision = repo_hash
})
-- | Describe characteristics of a symbol
describeSymbol
:: Glass.Env -> SymbolId -> RequestOptions
-> IO SymbolDescription
describeSymbol env@Glass.Env{..} symId opts =
withSymbol "describeSymbol" env opts symId $
\gleanDBs dbInfo (scmRepo, lang, toks) ->
backendRunHaxl GleanBackend{..} env $ do
r <- Search.searchEntityLocation lang toks
(first :| rest, err) <- case r of
None t -> throwM (ServerException t)
One e -> return (e :| [], Nothing)
Many { initial = e, rest = es } -> return (e :| es, Nothing) -- log?
let scmRevs = scmRevisions dbInfo
desc0 <- withRepo (entityRepo first) $ mkSymbolDescription symId scmRevs
scmRepo (toCodeEntityLoc first) Nothing
-- now combine with up to N additional results, possibly across repos
-- we have e.g. Optional across fbsource.arvr and fbsource.*
-- but typical case is Hack namespaces
descN <- forM rest $ \this -> withRepo (entityRepo this) $
mkSymbolDescription symId scmRevs
scmRepo (toCodeEntityLoc this) Nothing
let !desc = foldr combineDescriptions desc0 descN
pure (desc, err)
resolveSymbols
:: Glass.Env
-> ResolveSymbolsRequest
-> RequestOptions
-> IO ResolveSymbolsResult
resolveSymbols env@Glass.Env{..} (ResolveSymbolsRequest symIds) opts = do
results <- forM symIds $ \symId -> do
-- Try to resolve the symbol, returning any error encountered
(resolutions, err) <- withSymbol "resolveSymbols" env opts symId $
\gleanDBs dbInfo (scmRepo, lang, toks) ->
backendRunHaxl GleanBackend{..} env $ do
r <- Search.searchEntityLocation lang toks
(entities, err) <- case r of
None t ->
return ([], Just $ SymbolResolutionFailure symId
(GlassExceptionReason_entitySearchFail t))
One e -> return ([e], Nothing)
Many{initial = e, rest = es} -> return (e : es, Nothing)
let scmRevs = scmRevisions dbInfo
resolutions <- forM entities $ \this ->
let SearchEntity
{ entityRepo
, decl = (_entity, file, rangespan, _text)
} = this
in withRepo entityRepo $ do
location <- rangeSpanToLocationRange scmRepo file rangespan
repoHash <-
getRepoHashForLocation location scmRevs <$> Glean.haxlRepo
SymbolBasicDescription{..} <-
mkBriefSymbolDescription
symId
scmRevs
scmRepo
(toCodeEntityLoc this)
Nothing
pure SymbolResolution
{ symbolResolution_qname = symbolBasicDescription_name
, symbolResolution_location = location
, symbolResolution_revision = repoHash
, symbolResolution_kind = symbolBasicDescription_kind
, symbolResolution_language = symbolBasicDescription_language
, symbolResolution_signature = symbolBasicDescription_signature
}
pure ((resolutions, err), Nothing)
pure (ResolvedSymbol symId resolutions err)
return $ ResolveSymbolsResult results
toCodeEntityLoc ::
SearchEntity (ResultLocation Code.Entity) -> CodeEntityLocation
toCodeEntityLoc SearchEntity{decl = (decl, file, rangespan, name)} =
CodeEntityLocation decl file rangespan name
-- | Given a description for a symbol, fold in extra comments, annotations
-- and locations from zero or more additional occurences of the same symbol
-- Should only be necessary for rare non-unique symbols, like Hack namespaces
--
combineDescriptions
:: SymbolDescription -> SymbolDescription -> SymbolDescription
combineDescriptions y x =
x { symbolDescription_annotations = symbolDescription_annotations x <>
symbolDescription_annotations y -- <> Maybe [a]
, symbolDescription_pretty_comments = uniq $
symbolDescription_pretty_comments x <>
symbolDescription_pretty_comments y
, symbolDescription_sym_other_locations =
filter (/= symbolDescription_sym_location x) $
uniq ( -- collapse duplicate syms
symbolDescription_sym_location y : -- copy the single unique value
symbolDescription_sym_other_locations x
)
}
-- | Search for entities by string name with kind and language filters
searchSymbol
:: Glass.Env
-> SymbolSearchRequest
-> RequestOptions
-> IO SymbolSearchResult
searchSymbol
env@Glass.Env{..}
req@SymbolSearchRequest{..}
opts@RequestOptions{..} = do
case selectGleanDBs repoMapping scmRepo languageSet of
Left err -> throwIO $ ServerException err
Right rs -> case sFeelingLucky of
Normal -> joinSearchResults mlimit terse sorted <$>
Async.mapConcurrently
(continueOnErrors. uncurry searchSymbolsIn)
(Map.toList rs)
-- lucky mode is quite different, as it has to make priority choices
FeelingLucky -> joinLuckyResults <$>
Async.mapConcurrently
(continueOnErrors . uncurry searchLuckySymbolsIn)
(Map.toList rs)
where
continueOnErrors = fmap fst -- TODO support strict errors
scmRepo = symbolSearchRequest_repo_name
languageSet = symbolSearchRequest_language
SymbolSearchOptions{..} = symbolSearchRequest_options
terse = not symbolSearchOptions_detailedResults
sorted = symbolSearchOptions_sortResults
mlimit = fromIntegral <$> requestOptions_limit
-- inner limit can be higher if we are sampling/sorting into kind/lang sets
mlimitInner = if sorted then fmap (*5) mlimit else mlimit
sCase = if symbolSearchOptions_ignoreCase then Insensitive else Sensitive
sType = if symbolSearchOptions_exactMatch then Exact else Prefix
sScope = if symbolSearchOptions_namespaceSearch then Scope else NoScope
sString = Text.strip symbolSearchRequest_name -- drop leading whitespace
sKinds = map symbolKindFromSymbolKind (Set.elems symbolSearchRequest_kinds)
sLangs = languageExpandCpp $ mapMaybe languageToCodeLang
(Set.elems languageSet)
sFeelingLucky =
if symbolSearchOptions_feelingLucky then FeelingLucky else Normal
querySpec = SearchQuery{..}
searchQs = buildSearchQuery sFeelingLucky querySpec
-- run the same query across more than one glean db
searchSymbolsIn
:: RepoName
-> Set GleanDBName
-> IO (RepoSearchResult, Maybe ErrorLogger)
searchSymbolsIn repo dbs = Glass.withAllocationLimit env $
case nonEmpty (Set.toList dbs) of
Nothing -> pure ([], Nothing)
Just names ->
withGleanDBs "searchSymbol" env opts req repo names $
\gleanDBs dbInfo -> do
res <- backendRunHaxl GleanBackend{..} env $
Glean.queryAllRepos $ do
let scmRevs = scmRevisions dbInfo
res <- mapM (runSearch querySpec
repo scmRevs mlimitInner terse sString) searchQs
return (nubOrd (concat res))
-- remove latter duplicates in n*log n
pure (res, Nothing)
-- In lucky mode, we avoid flattening, instead selecting from the first
-- unique result found in priority order. We don't de-dup as we go.
searchLuckySymbolsIn
:: RepoName -> Set GleanDBName -> IO (FeelingLuckyResult, Maybe ErrorLogger)
searchLuckySymbolsIn repo dbs = case nonEmpty (Set.toList dbs) of
Nothing -> pure (FeelingLuckyResult [], Nothing)
Just names ->
withGleanDBs "feelingLucky" env opts req repo names $
\gleanDBs dbInfo -> do
res <- backendRunHaxl GleanBackend{..} env $
Glean.queryEachRepo $ do
-- we can conveniently limit to 2 matches per inner search
let scmRevs = scmRevisions dbInfo
rs <- forM searchQs $
runSearch querySpec repo scmRevs (Just 2) terse sString
return (map (nubOrdOn symbolIdOrder) rs)
pure (FeelingLuckyResult res, Nothing)
-- cheap way to fix up entities with non-unique locations: uniq on symbol id
symbolIdOrder :: (SymbolResult, a) -> SymbolId
symbolIdOrder = symbolResult_symbol . fst
-- Run a specific Query.AngleSearch query, with optional time boxing
runSearch
:: SearchQuery
-> RepoName
-> ScmRevisions
-> Maybe Int
-> Bool
-> Text
-> AngleSearch
-> RepoHaxl u w [(SymbolResult, Maybe SymbolDescription)]
runSearch querySpec repo scmRevs mlimit terse sString (Search query) = do
(ctx,names) <- case query of
Complete q -> (Nothing,) <$> searchWithTimeLimit mlimit queryTimeLimit q
InheritedScope sCase term searchFn -> do
names <- luckyParentSearch queryTimeLimit querySpec term
case names of
(_,[]) -> pure (Nothing,[])
(baseEntity,parentNames) -> do
syms <- searchWithTimeLimit mlimit queryTimeLimit
(searchFn sCase parentNames)
return (baseEntity, syms)
mapM (processSymbolResult terse sString repo scmRevs ctx) names
where
queryTimeLimit = 5000 -- milliseconds, make this a flag?
-- | Do a lucky search for the parent entity and return it. It will anchor
-- our later search, and we can generate a link to it later.
luckyParentSearch
:: Int
-> SearchQuery
-> NonEmpty Text
-> RepoHaxl u w (Maybe SymbolSearchData, [NonEmpty Text])
luckyParentSearch queryTimeLimit querySpec term = do
parentSet <- mapM (runLuckyParentSearch queryTimeLimit) parentQs
case findUnique parentSet of
Found baseEntity -> do -- this is the anchor container, now search parents
parents <- uniq . map Search.parentEntity <$>
Search.searchRecursiveEntities maxInheritedDepth
RelationDirection_Parent RelationType_Extends
(srEntity baseEntity)
xs <- forM parents $ fmap flattenScope . eThrow <=< toQualifiedName
return (Just baseEntity, xs)
_ -> pure (Nothing, []) -- not sufficiently unique. can't proceed
where
parentQs = buildLuckyContainerQuery querySpec term
maxInheritedDepth = 1000
-- this isn't the full scope (i.e. nested container parents) , but 2 levels
flattenScope qn = case unName (qualifiedName_container qn) of
"" -> unName (qualifiedName_localName qn) :| []
parent -> parent :| [unName (qualifiedName_localName qn)]
runLuckyParentSearch :: Int -> AngleSearch -> RepoHaxl u w [SymbolSearchData]
runLuckyParentSearch timeLimit (Search query) = do
result <- case query of
Complete q -> searchWithTimeLimit (Just 2) timeLimit q
InheritedScope{} -> return [] -- no inner recursion please
mapM toSearchResult result
-- n.b. we need the RepoName (i.e. "fbsource" to construct symbol ids)
processSymbolResult
:: ToSearchResult a
=> Bool
-> Text
-> RepoName
-> ScmRevisions
-> Maybe SymbolSearchData
-> a
-> RepoHaxl u w (SymbolResult, Maybe SymbolDescription)
processSymbolResult terse sString repo scmRevs mContext result = do
SymbolSearchData{..} <- toSearchResult result
let Code.Location{..} = srLocation
symbolResult_location <- rangeSpanToLocationRange
repo location_file location_location
path <- GleanPath <$> Glean.keyOf location_file
symbolResult_symbol <- toSymbolId (fromGleanPath repo path) srEntity
symbolResult_qname <- eThrow =<< toQualifiedName srEntity
let symbolResult_kind = symbolKindToSymbolKind <$> srKind
symbolResult_context <- case mContext of
Just ctx -> Just <$> processContext repo ctx
Nothing -> case symbolResult_kind of
Just kind
| kind `Set.member` atomicKinds -- use parent container
-> parentContainer repo srEntity
_ -> pure Nothing
let symbolResult_language = entityLanguage srEntity
symbolResult_name = location_name
symbolResult_score = Map.singleton
"exactness"
(fromIntegral (fromEnum
(scoreResult sString location_name)))
basics = SymbolResult{..} -- just sym id, kind, lang, location
(basics,) <$>
if terse then pure Nothing else
Just <$> describeEntity scmRevs srEntity basics
-- | Leaf kinds, those that don't contain other symbols. We will try to
-- put in parent context
atomicKinds :: Set SymbolKind
atomicKinds = Set.fromList
[ SymbolKind_Method
, SymbolKind_Field
, SymbolKind_Constant
, SymbolKind_Type
, SymbolKind_Value
, SymbolKind_Function
, SymbolKind_Variable
, SymbolKind_Enumerator
, SymbolKind_Operator
]
-- | A little bit of processing for the related symbol context
processContext
:: RepoName -> SymbolSearchData -> Glean.RepoHaxl u w SymbolContext
processContext repo SymbolSearchData{..} = do
let Code.Location{..} = srLocation
path <- GleanPath <$> Glean.keyOf location_file
symbolContext_symbol <- toSymbolId (fromGleanPath repo path) srEntity
symbolContext_qname <- eThrow =<< toQualifiedName srEntity
let symbolContext_kind = symbolKindToSymbolKind <$> srKind
return SymbolContext{..}
-- this takes first N results. We could try other strategies (such as
-- selecting evenly by language, first n in alpha order by sym or file, ..
--
-- This is also where we sort the groups (kinds) so that the top N
-- search results appear sampled and ranked.
--
joinSearchResults
:: Maybe Int
-> Bool
-> Bool
-> [RepoSearchResult]
-> SymbolSearchResult
joinSearchResults mlimit terse sorted xs = SymbolSearchResult syms $
if terse then [] else catMaybes descs
where
uniqXs = dedupSearchResult <$> xs
(syms,descs) = unzip $ nubOrd $ case (mlimit, sorted) of
(Nothing, _) -> flattened
(Just n, False) -> take n flattened
-- codehub/aka "sorted" mode grouping, ranking and sampling
(Just n, True) -> takeFairN n (concatMap sortResults uniqXs)
flattened = concat uniqXs
--
-- DFS to first singleton result.
--
-- Search matrix for feeling lucky
-- - scm repo
-- - dbs within repo
-- - queries in order within db
--
-- Second result will be the anchor container if it was an inherited search
--
joinLuckyResults :: [FeelingLuckyResult] -> SymbolSearchResult
joinLuckyResults allResults = case findLucky allResults of
Nothing -> SymbolSearchResult [] []
Just (result, desc) -> SymbolSearchResult [result] (catMaybes [desc])
where
-- little state machine across logical scm repos (e.g. "fbsource")
findLucky :: [FeelingLuckyResult] -> Maybe SingleSymbol
findLucky [] = Nothing
findLucky (FeelingLuckyResult scmRepo : rest) =
case findUniqueInner scmRepo of
Continue -> findLucky rest
Stop -> Nothing
Found result -> Just result
-- across a particular scm repo's glean dbs (e.g. arvr.cxx, fbcode.cxx)
findUniqueInner :: [[[a]]] -> MaybeResult a
findUniqueInner [] = Continue
findUniqueInner (db : dbs) = case findUnique db of
Continue -> findUniqueInner dbs
Stop -> Stop -- could try to be smart and merge duplicates
Found x -> Found x
-- results across each underlying glean query.
-- if we see a unique result that's our winner
findUnique :: [[a]] -> MaybeResult a
findUnique [] = Continue
findUnique (q : qs) = case hasUniqueResult q of
Continue -> findUnique qs -- continue in priority order
Stop -> Stop -- can't proceed
Found x -> Found x -- got it!
hasUniqueResult :: [a] -> MaybeResult a
hasUniqueResult results = case results of
[] -> Continue
[unique] -> Found unique
_ -> Stop -- more than 1 result, terminate
data MaybeResult a
= Continue -- so far, no results, keep going
| Found a -- one perfect result
| Stop -- yikes, too many results. abandon ship
-- | Sort the results of one query set
sortResults
:: RepoSearchResult
-> [[(SymbolResult, Maybe SymbolDescription)]]
sortResults xs = map (List.sortOn relevance) (groupSortOn features xs)
where
-- we group on the language/kind sets to produce result sets
-- then sort those results by score and alpha (note: not groupSortOn with
-- relevance as that would create too many classes to select from)
features (SymbolResult{..},_desc) = Feature
symbolResult_language (scoreKind symbolResult_kind)
-- we could improve this slightly with a qualified vs global qname check
relevance (SymbolResult{..},_desc) =
( symbolResult_score
, scoreKind symbolResult_kind
, scoreScope symbolResult_qname
, symbolResult_name
, qualifiedName_container symbolResult_qname)
-- Match feelingLuck() server-side classes
containerishKinds :: Set SymbolKind
containerishKinds = Set.fromList
[ SymbolKind_Namespace
, SymbolKind_Class_
, SymbolKind_Trait
, SymbolKind_Interface
, SymbolKind_Module
]
scoreScope :: QualifiedName -> Int
scoreScope qname = case qualifiedName_container qname of
Name "" -> 0 -- free or global things have empty container names
_ -> 1
scoreKind :: Maybe SymbolKind -> Int
scoreKind kind = case kind of
Just k
| k `Set.member` containerishKinds -> 1
| k == SymbolKind_Function || k == SymbolKind_Method -> 2
| otherwise -> 3
_unknown -> 4
-- | We do some light ranking of the results
scoreResult :: Text -> Text -> MatchType
scoreResult query result
| result == query = ExactMatch
| Text.toLower result == queryLC = ExactInsensitiveMatch
| otherwise = Otherwise
where
queryLC = Text.toLower query
-- A projection from the search result to features for fair sampling groups
--
-- todo: we might want to have fewer symbolkind groups (e.g. combine class-like
-- things)
--
data Feature = Feature !Language {-# UNPACK #-}!KindSort
deriving (Eq, Ord)
-- Containerish, function/method, other
type KindSort = Int
-- | Glass needs to mark results by how good they are
-- For scope searches we don't care so much as the scope will filter
-- results. But for bare string searches we need to do a bit of work
data MatchType
= ExactMatch -- "FBID"
| ExactInsensitiveMatch -- "fbid" ~ "FBID"
| Otherwise
deriving (Eq, Ord, Bounded, Enum)
-- | Symbol search for references as ranges.
fetchSymbolReferenceRanges
:: Glean.Backend b
=> Glass.Env
-> RepoName
-> Language
-> [Text]
-> Maybe Int
-> GleanBackend b
-> IO ([LocationRange], Maybe ErrorLogger)
fetchSymbolReferenceRanges env scsrepo lang toks limit b =
backendRunHaxl b env $ do
er <- symbolToAngleEntities lang toks
let logDBs x = logError (gleanDBs b) <> x
case er of
Left err -> return ([], Just $ logDBs err)
Right (entities, searchErr) -> do
ranges <- forM entities $ \(entityRepo, query) ->
withRepo entityRepo $ do
let convert (targetFile, rspan) =
rangeSpanToLocationRange scsrepo targetFile rspan
uses <- searchWithLimit limit $ Query.findReferenceRangeSpan query
mapM convert uses
return (nubOrd $ concat ranges, fmap (logDBs . logError) searchErr)
-- | Search for a symbol and return an Angle query that identifies the entities
-- Return the angle query and the Glean.repo to which it applies.
--
-- Todo: this entity query only works on the db it came from.
-- Should be in the type
--
symbolToAngleEntities
:: Language
-> [Text]
-> ReposHaxl u w
(Either ErrorLogger
(NonEmpty (Glean.Repo, Angle Code.Entity), Maybe GlassExceptionReason))
symbolToAngleEntities lang toks = do
r <- Search.searchEntity lang toks -- search everywhere for something a match
(entities, searchErr) <- case r of
None t -> throwM (ServerException t) -- return [] ?
One e -> return (e:|[], Nothing)
-- n.b. picks the first entity only
Many { initial = e, rest = es, message = t } ->
return (e:|es, Just (GlassExceptionReason_entitySearchFail t))
let
eithers =
NonEmpty.map
(\SearchEntity{entityRepo, decl} ->(entityRepo,) <$> entityToAngle decl)
entities
return $ case allOrError eithers of
Left err -> Left (logError (GlassExceptionReason_entityNotSupported err))
Right results -> Right (results, searchErr)
newtype GetStaticAttributes =
GetStaticAttributes (Glean.Repo, Code.Entity, Maybe Language)
deriving (Eq, Hashable)
parentContainer
:: RepoName -> Code.Entity -> Glean.RepoHaxl u w (Maybe SymbolContext)
parentContainer repo ent = do
parents <- Search.searchRelatedEntities 2 Search.ShowAll NotRecursive
RelationDirection_Parent RelationType_Contains ent repo
let mParent = Search.parentRL <$> listToMaybe parents
case mParent of
Nothing -> return Nothing
Just (p, symbolContext_symbol) -> do
mQName <- eitherToMaybe <$> toQualifiedName (fst4 p)
return $ do -- :: Maybe
symbolContext_qname <- mQName
let symbolContext_kind = Nothing
pure SymbolContext{..}
--
-- | Relational search. Given a symbol id find symbols related to it.
-- Relationships could be parent/child or oo inheritance, for example.
--
searchRelated
:: Glass.Env
-> SymbolId
-> RequestOptions
-> SearchRelatedRequest
-> IO SearchRelatedResult
searchRelated env@Glass.Env{..} sym opts@RequestOptions{..}
SearchRelatedRequest{..} =
withSymbol "searchRelated" env opts sym $
\gleanDBs_ dbInfo (repo, lang, toks) ->
let gleanDBs = filterDBs lang gleanDBs_ in
backendRunHaxl GleanBackend{..} env $ do
entity <- searchFirstEntity lang toks
withRepo (entityRepo entity) $ do
(symPairs, desc, merr) <- case searchRelatedRequest_relatedBy of
RelationType_Calls -> do
(entityPairs, merr) <-
searchRelatedCalls
repo searchRelatedRequest_relation entity lang
desc <- descriptions repo entityPairs dbInfo
let symPairs = symbolIdPairs entityPairs
return (symPairs, desc, merr)
RelationType_Generates -> do
(symPairs, merr) <-
searchRelatedGenerate
repo searchRelatedRequest_relation entity lang
return (symPairs, Map.empty, merr)
_ -> do
(entityPairs, merr) <- do
relatedLocatedEntities <-
Search.searchRelatedEntities limit
Search.ShowAll
searchRecursively
searchRelatedRequest_relation
searchRelatedRequest_relatedBy
(fst4 (decl entity))
repo
return ((,Nothing) <$> relatedLocatedEntities, Nothing)
desc <- descriptions repo entityPairs dbInfo
let symPairs = symbolIdPairs entityPairs
return (symPairs, desc, merr)
let result = SearchRelatedResult
{ searchRelatedResult_edges = symPairs
, searchRelatedResult_symbolDetails = desc
}
pure (result, merr)
where
symbolIdPairs
:: [(RelatedLocatedEntities, Maybe [LocationRange])] -> [RelatedSymbols]
symbolIdPairs entityPairs =
map (\(Search.RelatedLocatedEntities{..}, ranges) ->
RelatedSymbols (snd parentRL) (snd childRL) ranges
) entityPairs
descriptions
:: RepoName
-> [(RelatedLocatedEntities, Maybe [LocationRange])]
-> GleanDBInfo
-> RepoHaxl u w (Map.Map Text SymbolDescription)
descriptions repo entityPairs dbInfo =
if searchRelatedRequest_detailedResults
then do
let uniqSymIds = uniqBy (comparing snd) $ concat
[ [ e1, e2 ]
| (Search.RelatedLocatedEntities e1 e2, _) <- entityPairs ]
let scmRevs = scmRevisions dbInfo
descs <- mapM (mkDescribe repo scmRevs) uniqSymIds
-- carefully in parallel!
pure $ Map.fromAscList descs
else pure mempty
-- building map of sym id -> descriptions, by first occurence
mkDescribe repo scmRevs e@(_,SymbolId rawSymId) = (rawSymId,) <$>
describe repo scmRevs e
describe repo scmRevs ((entity, entityFile, entityRange, entityName), symId)
= mkSymbolDescription symId scmRevs repo CodeEntityLocation{..} Nothing
searchRecursively
| searchRelatedRequest_recursive = Search.Recursive
| otherwise = Search.NotRecursive
limit = fromIntegral $ case requestOptions_limit of
Just x | x < rELATED_SYMBOLS_MAX_LIMIT -> x
_ -> rELATED_SYMBOLS_MAX_LIMIT
-- Caller hieararchy for C++ requires cxx1.DeclarationSources,
-- which may not be supported by all DBs.
filterDBs :: Language -> NonEmpty (GleanDBName,a) -> NonEmpty (GleanDBName,a)
filterDBs lang
| RelationType_Calls <- searchRelatedRequest_relatedBy
, RelationDirection_Parent <- searchRelatedRequest_relation
, Language_Cpp <- lang
= NonEmpty.fromList .
NonEmpty.filter (supportsCxxDeclarationSources . fst)
| otherwise
= id
searchRelatedGenerate
:: RepoName
-> RelationDirection
-> SearchEntity (ResultLocation Code.Entity)
-> Language
-> RepoHaxl u w
([RelatedSymbols], Maybe ErrorLogger)
searchRelatedGenerate repoName RelationDirection_Parent child _ = do
let SearchEntity{decl = (decl, _file, _rangespan, _name)} = child
case entityToAngle decl of
Left err -> do
repo <- Glean.haxlRepo
return
([], Just (logError (GlassExceptionReason_entityNotSupported err)
<> logError repo)
)
Right query -> do
results <- searchWithLimit Nothing $
Query.generatedEntityToIdlEntity query
idlDef <- forM results $ \(entity, file) -> do
gleanPath <- GleanPath <$> Glean.keyOf file
parentSymbol <- toSymbolId (fromGleanPath repoName gleanPath) entity
return $ RelatedSymbols parentSymbol sym Nothing
return (idlDef, Nothing)
-- TODO implement RelationDirection_Child
searchRelatedGenerate _ _ _ _ = return ([], Nothing)
-- Implements Call hierarchy (check LSP spec)
searchRelatedCalls
:: RepoName
-> RelationDirection
-> SearchEntity (ResultLocation Code.Entity)
-> Language
-> RepoHaxl u w
([(RelatedLocatedEntities, Maybe [LocationRange])], Maybe ErrorLogger)
searchRelatedCalls _ RelationDirection__UNKNOWN{} _ _ = error "unreachable"
-- Incoming calls, mapping a symbol to all its callers symbols.
searchRelatedCalls repoName RelationDirection_Parent child Language_Cpp = do
-- For C++ we use a more efficient predicate, albeit without call sites
let SearchEntity{decl = (decl, file, rangespan, name)} = child
case entityToAngle decl of
Left err -> do
repo <- Glean.haxlRepo
return ([], Just (logError (GlassExceptionReason_entityNotSupported err)
<> logError repo)
)
Right query -> do
let childRL = ((decl, file, rangespan, name), sym)
limit = fromIntegral <$> requestOptions_limit
results <- searchWithLimit limit $
Query.findReferenceEntitiesFast query
callers <- forM results $ \(entity, loc) -> do
let path = Code.location_file loc
gleanPath <- GleanPath <$> Glean.keyOf path
symbol <- toSymbolId (fromGleanPath repoName gleanPath) entity
let name = Code.location_name loc
range = Code.location_location loc
parentRL = ((entity, path, range, name), symbol)
return Search.RelatedLocatedEntities{..}
return ((,Nothing) <$> callers, Nothing)
searchRelatedCalls repoName RelationDirection_Parent child _ = do
let SearchEntity{decl = (decl, file, rangespan, name)} = child
case entityToAngle decl of
Left err -> do
repo <- Glean.haxlRepo
return
([], Just (logError (GlassExceptionReason_entityNotSupported err)
<> logError repo)
)
Right query -> do
let childRL = ((decl, file, rangespan, name), sym)
-- limit until we optimize the query to use DeclarationSource
limit = maybe 200 fromIntegral requestOptions_limit
timeLimit = 2000 -- ms
results <- searchWithTimeLimit (Just limit) timeLimit $
Query.findReferenceEntities query
callers <- forM results $ \(path, entity, loc, callSite) -> do
gleanPath <- GleanPath <$> Glean.keyOf path
symbol <- toSymbolId (fromGleanPath repoName gleanPath) entity
let name = Code.location_name loc
range = Code.location_location loc
parentRL = ((entity, path, range, name), symbol)
location <- rangeSpanToLocationRange repoName path callSite
return (Search.RelatedLocatedEntities{..}, location)
let groups = groupLocatedEntitiesOn parentRL callers
return (second Just <$> groups, Nothing)
-- | Outgoing calls, maps a symbol to all the symbols it references.
searchRelatedCalls repoName RelationDirection_Child parent lang = do
let SearchEntity{decl = (decl, file, rangespan, name)} = parent
parentRL = ((decl, file, rangespan, name), sym)
parentRange <-
locationRange_range <$> rangeSpanToLocationRange repoName file rangespan
(xrefs_, _, _) <- Utils.documentSymbolsForLanguage Nothing (Just lang)
(Utils.ExtraSymbolOpts True False Nothing)
(Glean.getId file)
-- only consider non-xlang xrefs
let xrefs = [x | PlainXRef x <- xrefs_]
xrefsRanges <- forM xrefs $ \(Code.XRefLocation{..}, entity) -> do
gleanPath <- GleanPath <$>
Glean.keyOf (Code.location_file xRefLocation_target)
symbol <- toSymbolId (fromGleanPath repoName gleanPath) entity
sourceRange <- rangeSpanToLocationRange repoName file xRefLocation_source
kind <- findSymbolKind entity
return (sourceRange, symbol, kind)
let callees =
[ (Search.RelatedLocatedEntities{..}, callRange)
| ((Code.XRefLocation{..}, entity), (callRange, symbol, Right kind))
<- zip xrefs xrefsRanges
, kind `elem`
[ SymbolKind_Constructor
, SymbolKind_Function
, SymbolKind_Method
, SymbolKind_Macro
]
, rangeContains parentRange (locationRange_range callRange)
, let childRL =
(( entity
, Code.location_file xRefLocation_target
, Code.location_location xRefLocation_target
, Code.location_name xRefLocation_target
), symbol)
]
let groups = groupLocatedEntitiesOn childRL callees
return (second Just <$> groups, Nothing)
groupLocatedEntitiesOn f locatedEntities =
[ (locatedEntity, locs)
| group@((locatedEntity, _) : _) <-
groupOn (snd . f . fst) locatedEntities
, let locs = map snd group
]
--
-- Extract the "API" neighborhood of a symbol
--
searchRelatedNeighborhood
:: Glass.Env
-> SymbolId
-> RequestOptions
-> RelatedNeighborhoodRequest
-> IO RelatedNeighborhoodResult
searchRelatedNeighborhood env@Glass.Env{..} sym opts@RequestOptions{..} req =
withSymbol "searchRelatedNeighborhood" env opts sym $
\gleanDBs dbInfo (repo, lang, toks) ->
backendRunHaxl GleanBackend{..} env $ do
baseEntity <- searchFirstEntity lang toks
let lang = entityLanguage (fst4 (decl baseEntity))
(,Nothing) <$> searchNeighborhood limit req sym repo
(scmRevisions dbInfo) lang baseEntity
where
limit = fromIntegral $ case requestOptions_limit of
Just x | x < rELATED_SYMBOLS_MAX_LIMIT -> x
_ -> rELATED_SYMBOLS_MAX_LIMIT
searchFirstEntity
:: Language
-> [Text]
-> Glean.ReposHaxl u w (SearchEntity (ResultLocation Code.Entity))
searchFirstEntity lang toks = do
r <- Search.searchEntityLocation lang toks
case r of
None t -> throwM (ServerException t)
One e -> return e
Many { initial = e } -> return e