{- 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