glean/tools/search/Search.hs (341 lines of code) (raw):
{-
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, TypeApplications #-}
module Search (main) where
import Control.Monad
import Data.Aeson as Aeson
import Data.Aeson.Encode.Pretty (encodePretty)
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Foldable
import Data.Char
import Data.Int
import Data.Maybe
import Data.Text ( Text, pack )
import qualified Data.Text as Text
import Data.Text.Prettyprint.Doc.Util
import Data.Text.Prettyprint.Doc
import Options.Applicative
import System.Process
import Text.Printf
import Util.EventBase
import Util.Log
import Util.OptParse
import qualified Glean
import Glean ( getFactKey, Nat(..) )
import Glean.Impl.ConfigProvider
import Glean.Pretty.Code ()
import Glean.Pretty.Cxx ()
import Glean.Pretty.Hs ()
import Glean.Pretty.Search ()
import Glean.Schema.Code.Types as Code
import Glean.Schema.CodeCxx.Types as Cxx
import Glean.Schema.CodeJava.Types as Java
import Glean.Schema.CodePp.Types as Pp
import Glean.Schema.Cxx1.Types as Cxx
import qualified Glean.Schema.Java.Types as Java
import Glean.Schema.Src.Types
import Glean.Search.Graph
import Glean.Search.Search
import Glean.Search.Types
import Glean.Util.ConfigProvider
import Glean.Util.Declarations
import Glean.Util.Range (locRange, HasSrcRange(..))
import Glean.Util.Some
data Command
= FindDeclarations
{ entity :: String
, refs :: Bool
, countRefs :: Bool
, json :: Bool
, mangled :: Bool
, limitRefs :: Maybe Int
, targetFile :: Maybe Text
, targetLine :: Maybe Nat
, showDeclId :: Bool
, caseSensitive :: Bool
}
| FindLocalGraph
{ declId :: Int64
, steps :: Int32
, dotFile :: Maybe String
}
data Config = Config
{ cfgService :: Glean.ThriftSource Glean.ClientConfig
, cfgCommand :: Command
}
options :: ParserInfo Config
options = info (helper <*> parser) fullDesc
where
parser :: Parser Config
parser = do
cfgService <- Glean.options
cfgCommand <- asum
[ findDeclarations
, findLocalGraph
-- there will be more commands later
]
return Config{..}
findDeclarations =
commandParser "find-decls" (progDesc "Find declarations by name") $ do
entity <- strArgument
( metavar "NAME"
<> help "Entity to search for")
refs <- switch
( long "refs"
<> help "Show references to the declaration(s)" )
countRefs <- switch
( long "count-refs"
<> help "Show count of references to the declaration(s)" )
json <- switch
( long "json"
<> help "Show output in JSON format" )
mangled <- switch
( long "mangled"
<> help "Unmangle the C++ name" )
limitRefs <- optional $ option (fromIntegral <$> auto @Int)
( long "limit-refs"
<> help "limit query results for refs" )
targetFile <- optional $ pack <$> strOption
( long "file"
<> help "Target file path" )
targetLine <- optional $ option (Nat . fromIntegral <$> auto @Int)
( long "line"
<> help "Target line number" )
showDeclId <- switch
( long "show-decl-id"
<> help "Print glean-id of the declarations" )
caseSensitive <- flag True False
( long "ignore-case"
<> short 'i'
<> help "Ignore case when finding matching declarations" )
return FindDeclarations{..}
findLocalGraph =
commandParser "find-local-graph"
(progDesc "Find local graph for a declaration with a given id") $ do
declId <- option (auto @Int64)
( long "decl-id"
<> help "The id of a function declaration" )
steps <- option (auto @Int32)
( long "steps"
<> help "Number of steps in graph searching" )
dotFile <- optional $ strOption
( long "dot-file"
<> help "write Dot graph to the file")
return FindLocalGraph{..}
main :: IO ()
main = do
withConfigOptions options $ \(cfg, cfgOpts) ->
withEventBaseDataplane $ \evb ->
withConfigProvider cfgOpts $ \(cfgAPI :: ConfigAPI) -> do
Glean.withRemoteBackend evb cfgAPI (cfgService cfg) $ \be -> do
doQuery (Some be) cfg
doQuery :: Some Glean.Backend -> Config -> IO ()
doQuery backend Config{..}
| FindDeclarations{..} <- cfgCommand = do
let
printResults :: String -> [EntityRefs] -> IO ()
printResults = if json then jsonResults else
prettyResults targetFile targetLine showDeclId refs countRefs
logMissing e name = logWarning $ show e <>
" (results from " <> Text.unpack name <> " omitted)"
unmangled <- if mangled then unmangle entity else pure entity
repos <- getSearchRepos backend logMissing
let
q = SearchQuery
{ query = Text.pack unmangled
, case_sensitive = caseSensitive
, languages = Nothing
}
results <- findEntities limitRefs backend repos q (refs || countRefs)
-- The above searches both the Cpp, etc and Haskell repos
let filteredResults = filter (matchesTarget targetFile targetLine) results
printResults entity filteredResults
| FindLocalGraph{..} <- cfgCommand = do
repo <- Glean.getLatestRepo backend "fbsource"
FindLocalGraphResult{..} <-
findLocalGraph backend repo declId (fromIntegral steps)
printf "Forward search:\n"
forM_ (zip [0::Int ..] findLocalGraphResult_source_vertices) $
\(i, sourceVertex) -> do
printf "%d:\n" i
let decl = sourceVertex_declaration sourceVertex
putDocW 80 $ pretty decl <> line
let overriding_methods = sourceVertex_overriding_methods sourceVertex
when (not $ null overriding_methods) $ do
printf "is overridden by:\n\n"
forM_ overriding_methods $ \(Override _ _ method) ->
putDocW 80 $ indent 2 (pretty method) <> line <> line
let called_functions = sourceVertex_called_functions sourceVertex
when (not $ null called_functions) $ do
printf "calls:\n\n"
forM_ called_functions $ \(FunctionCall _ _ func) ->
putDocW 80 $ indent 2 (pretty func) <> line <> line
let dropped_edges = sourceVertex_dropped_edges sourceVertex
when (dropped_edges > 0) $ do
when (not (null overriding_methods && null called_functions)) $
printf "and "
printf "is overridden by or calls %d more\n" dropped_edges
printf "\n"
putStrLn "Backward search:"
forM_ (zip [0::Int ..] findLocalGraphResult_target_vertices) $
\(i, targetVertex) -> do
printf "%d:\n" i
let decl = targetVertex_declaration targetVertex
putDocW 80 $ pretty decl <> line
let overridden_method = targetVertex_overridden_method targetVertex
when (isJust overridden_method) $ do
printf "is overriding:\n\n"
forM_ overridden_method $ \(Override method _ _) ->
putDocW 80 $ indent 2 (pretty method) <> line <> line
let calling_functions = targetVertex_calling_functions targetVertex
when (not $ null calling_functions) $ do
printf "is called by:\n\n"
forM_ calling_functions $ \(FunctionCall func _ _) ->
putDocW 80 $ indent 2 (pretty func) <> line <> line
let dropped_edges = targetVertex_dropped_edges targetVertex
when (dropped_edges > 0) $ do
when (not (isNothing overridden_method && null calling_functions)) $
printf "and "
printf "is overriding or called by %d more\n" dropped_edges
printf "\n"
-- This should be replaced by symbol view-based rendering in the future
prettyResults :: Maybe Text
-> Maybe Nat
-> Bool
-> Bool
-> Bool
-> String
-> [EntityRefs]
-> IO ()
prettyResults
targetFile
targetLine
showDeclId
refs
countRefs
entity
results = do
let
isFunDefn (Entity_cxx (Cxx.Entity_defn Cxx.Definition_function_{})) = True
isFunDefn _ = False
isFunDecl (Entity_cxx (Cxx.Entity_decl Cxx.Declaration_function_{})) = True
isFunDecl _ = False
isRecDefn (Entity_cxx (Cxx.Entity_defn Cxx.Definition_record_{})) = True
isRecDefn _ = False
isRecDecl (Entity_cxx (Cxx.Entity_decl Cxx.Declaration_record_{})) = True
isRecDecl _ = False
isEnumDefn (Entity_cxx (Cxx.Entity_defn Cxx.Definition_enum_{})) = True
isEnumDefn _ = False
isEnumDecl (Entity_cxx (Cxx.Entity_decl Cxx.Declaration_enum_{})) = True
isEnumDecl _ = False
isEnumerator (Entity_cxx Cxx.Entity_enumerator{}) = True
isEnumerator _ = False
isMacro Entity_pp{} = True
isMacro _ = False
isJavaClassDecl (Entity_java Java.Entity_class_{}) = True
isJavaClassDecl _ = False
isHackDeclaration Entity_hack{} = True
isHackDeclaration _ = False
classes :: [(String, Code.Entity -> Bool)]
classes =
[ ("C/C++ function definition", isFunDefn)
, ("C/C++ function declaration", isFunDecl)
, ("C/C++ record definition", isRecDefn)
, ("C/C++ record declaration", isRecDecl)
, ("C/C++ enum definition", isEnumDefn)
, ("C/C++ enum declaration", isEnumDecl)
, ("C/C++ enumerator", isEnumerator)
, ("C preprocessor macro", isMacro)
, ("Java class declaration", isJavaClassDecl)
, ("Hack declaration", isHackDeclaration)
]
forM_ classes $ \(desc, pred) -> do
let these = [ r | r@(EntityRefs _ ent _) <- results, pred ent ]
when (not (null these)) $ do
printf "%s has %d %ss" entity (length these) desc
forM_ targetFile $ \file -> do
printf " matching target file %s" file
when (isJust targetFile && isJust targetLine) $ printf " and"
forM_ targetLine $ \line -> do
printf " matching target line %d" (unNat line)
printf ":\n"
forM_ these $ \declRefs -> do
putDocW 80 $ indent 2 (pretty $ decl declRefs)
<> line
when showDeclId $
case decl declRefs of
Entity_cxx (Cxx.Entity_decl (Cxx.Declaration_function_
(FunctionDeclaration declId _))) ->
printf " declaration id: %d\n" declId
Entity_cxx (Cxx.Entity_defn (Cxx.Definition_function_
(FunctionDefinition _ (Just fdk)))) -> do
let decl = functionDefinition_key_declaration fdk
let declId = functionDeclaration_id decl
printf " declaration id: %d\n" declId
_ -> return ()
printf "\n"
when refs $ prettyXRefs (xrefs declRefs)
when countRefs $
print (indent 2 $ hsep
[ "Total References: ", pretty (length (xrefs declRefs)) ])
when (null results) $
printf "No declarations for %s found\n" entity
prettyXRefs :: [FileXRef] -> IO ()
prettyXRefs xrefs =
when (not (null xrefs)) $
print (indent 2 $ vsep [ "References:", indent 2 (vsep docs) ])
where docs = map pretty xrefs
jsonResults :: forall a . (ToJSON a) => String -> [a] -> IO ()
jsonResults _ results = LB.putStrLn $ encodePretty $ map toJSON results
unmangle :: String -> IO String
unmangle entity = do
result <- readProcess "c++filt" [] entity
return $ takeWhile (\c -> isAlphaNum c || c == ':' || c == '_') result
matchesRange :: Maybe Text -> Maybe Nat -> Maybe Range -> Bool
matchesRange targetFile targetLine mrange =
matchesFile && matchesLine where
matchesFile = case (targetFile, mrange) of
(Nothing, _) -> True
(Just file, Just Range{..})
-> file_key range_file == Just file
_ -> False
matchesLine = case (targetLine, mrange) of
(Nothing, _) -> True
(Just line, Just Range{range_lineBegin = rline})
-> rline == line
_ -> False
matchesTarget :: Maybe Text -> Maybe Nat -> EntityRefs -> Bool
matchesTarget targetFile targetLine EntityRefs{..} =
matchesRange targetFile targetLine $ declToRange decl
-- This should be replaced by derived predicates in the future
declToRange :: Code.Entity -> Maybe Range
declToRange decl =
case decl of
Code.Entity_cxx cxx -> cxxEntityToRange cxx
Code.Entity_pp (Pp.Entity_define defn) -> srcRange <$> getFactKey defn
Code.Entity_java (Java.Entity_class_ decl) ->
locRange . Java.classDeclaration_key_loc <$> getFactKey decl
_ -> Nothing
where
cxxEntityToRange cxx = case cxx of
Cxx.Entity_decl decl -> declarationSrcRange decl
Cxx.Entity_defn (Cxx.Definition_function_ defn) ->
srcRange <$>
(getFactKey =<<
(functionDefinition_key_declaration <$> getFactKey defn))
Cxx.Entity_defn (Cxx.Definition_record_ defn) ->
srcRange <$> (getFactKey =<<
(recordDefinition_key_declaration <$> getFactKey defn))
Cxx.Entity_defn (Cxx.Definition_enum_ defn) ->
srcRange <$> (getFactKey =<<
(enumDefinition_key_declaration <$> getFactKey defn))
Cxx.Entity_defn (Cxx.Definition_objcMethod defn) ->
srcRange <$> (getFactKey =<< getFactKey defn)
Cxx.Entity_defn (Cxx.Definition_objcContainer defn) ->
srcRange <$> (getFactKey =<<
(Cxx.objcContainerDefinition_key_declaration <$> getFactKey defn))
_ -> Nothing