packages feed

valiant-plugin-0.1.0.0: src/Valiant/Plugin/Traverse.hs

{-# LANGUAGE CPP #-}

-- | Walk the typechecked AST to find queryFile/queryFileAs/mkStatement call sites.
module Valiant.Plugin.Traverse
  ( QueryFileCall (..)
  , findQueryFileCalls
  ) where

import GHC.Hs
import GHC.Plugins
  ( Var, idType, varName, nameOccName, occNameString
  , GenLocated(..), SrcSpan
  )
import GHC.Tc.Types (TcGblEnv (..))
import GHC.Core.Type (Type)
import GHC.Data.Bag (bagToList)
import GHC.Data.FastString (unpackFS)

-- | A located queryFile call extracted from the AST.
data QueryFileCall = QueryFileCall
  { qfcSrcSpan :: SrcSpan
  , qfcFilePath :: String
  , qfcBindType :: Type
  , qfcBindName :: String
  }

-- | Find all queryFile/queryFileAs/mkStatement calls in the typechecked module.
-- After the parse-phase rewrite, queryFile calls become mkStatement calls.
-- We detect both forms to handle modules compiled with and without rewriting.
findQueryFileCalls :: TcGblEnv -> [QueryFileCall]
findQueryFileCalls env =
  concatMap findInBind (bagToList (tcg_binds env))

findInBind :: GenLocated l (HsBindLR GhcTc GhcTc) -> [QueryFileCall]
findInBind (L _ FunBind {fun_id = L _ var, fun_matches = matches}) =
  let bindName = occNameString (nameOccName (varName var))
      bindType = idType var
      calls = findCallsInMG matches
   in [ QueryFileCall sp path bindType bindName | (sp, path) <- calls ]
findInBind (L _ (XHsBindsLR AbsBinds {abs_binds = binds})) =
  concatMap findInBind (bagToList binds)
findInBind (L _ VarBind {var_id = var, var_rhs = rhs}) =
  let bindName = occNameString (nameOccName (varName var))
      bindType = idType var
      calls = findCallsInLExpr rhs
   in [ QueryFileCall sp path bindType bindName | (sp, path) <- calls ]
findInBind _ = []

findCallsInMG :: MatchGroup GhcTc (LHsExpr GhcTc) -> [(SrcSpan, String)]
findCallsInMG mg = case mg_alts mg of
  L _ alts -> concatMap (\(L _ m) -> findCallsInMatch m) alts

findCallsInMatch :: Match GhcTc (LHsExpr GhcTc) -> [(SrcSpan, String)]
findCallsInMatch Match {m_grhss = grhss} =
  concatMap (\(L _ g) -> findCallsInGRHS g) (grhssGRHSs grhss)

findCallsInGRHS :: GRHS GhcTc (LHsExpr GhcTc) -> [(SrcSpan, String)]
findCallsInGRHS (GRHS _ _ body) = findCallsInLExpr body

findCallsInLExpr :: LHsExpr GhcTc -> [(SrcSpan, String)]
findCallsInLExpr (L ann expr) = findCallsInExpr (getLocA (L ann expr)) expr

findCallsInExpr :: SrcSpan -> HsExpr GhcTc -> [(SrcSpan, String)]
findCallsInExpr sp expr =
  -- First, try to detect mkStatement (rewritten queryFile)
  case extractMkStatementCall expr of
    Just path -> [(sp, path)]
    Nothing -> case expr of
      -- Original queryFile "path.sql" (not rewritten)
      HsApp _ (L _ func) (L _ arg)
        | isQueryFileExpr func
        , Just path <- extractStringLit arg ->
            [(sp, path)]
      HsApp _ func arg ->
        findCallsInLExpr func ++ findCallsInLExpr arg
      OpApp _ l op r ->
        findCallsInLExpr l ++ findCallsInLExpr op ++ findCallsInLExpr r
      NegApp _ e _ -> findCallsInLExpr e
#if MIN_VERSION_ghc(9, 10, 0)
      HsPar _ e -> findCallsInLExpr e
#else
      HsPar _ _ e _ -> findCallsInLExpr e
#endif
      SectionL _ e1 e2 -> findCallsInLExpr e1 ++ findCallsInLExpr e2
      SectionR _ e1 e2 -> findCallsInLExpr e1 ++ findCallsInLExpr e2
      ExplicitTuple _ args _ ->
        concatMap (\case Present _ e -> findCallsInLExpr e; _ -> []) args
#if MIN_VERSION_ghc(9, 10, 0)
      HsLet _ _ e -> findCallsInLExpr e
#else
      HsLet _ _ _ _ e -> findCallsInLExpr e
#endif
      HsCase _ scrut mg ->
        findCallsInLExpr scrut ++ findCallsInMG mg
      HsIf _ c t f ->
        findCallsInLExpr c ++ findCallsInLExpr t ++ findCallsInLExpr f
      HsDo _ _ (L _ stmts) -> concatMap (\(L _ s) -> findCallsInStmt s) stmts
      ExplicitList _ exprs -> concatMap findCallsInLExpr exprs
      XExpr (WrapExpr (HsWrap _ inner)) -> findCallsInExpr sp inner
#if MIN_VERSION_ghc(9, 10, 0)
      XExpr (ExpandedThingTc _ inner) -> findCallsInExpr sp inner
#endif
      _ -> []

-- | Try to extract the file path from a mkStatement call.
-- After rewrite, the expression is:
--   mkStatement sqlStr oidsLit colsLit pathStr
-- Which in the AST is a chain of HsApp:
--   (((mkStatement `app` sql) `app` oids) `app` cols) `app` path
-- We collect all args from a left-nested application spine,
-- check the head is mkStatement, and take the 4th arg as the path.
extractMkStatementCall :: HsExpr GhcTc -> Maybe String
extractMkStatementCall expr = case collectArgs expr of
  (f, [_sql, _oids, _cols, path]) | isMkStatementExpr f ->
    extractStringLit (unLHsExpr path)
  _ -> Nothing

-- | Collect the function and arguments from a left-nested application spine.
-- (((f a1) a2) a3) → (f, [a1, a2, a3])
-- Uses reverse-accumulate internally to avoid O(n²) from repeated (++).
collectArgs :: HsExpr GhcTc -> (HsExpr GhcTc, [LHsExpr GhcTc])
collectArgs = go []
  where
    go !acc (HsApp _ (L _ f) arg) = go (arg : acc) f
    go !acc (XExpr (WrapExpr (HsWrap _ inner))) = go acc inner
    go !acc other = (other, acc) -- acc is already in correct order
    -- because we prepend as we peel from the outside in:
    -- (((f a1) a2) a3) → go [] expr → go [a3] (f a1 a2) → go [a2,a3] (f a1) → go [a1,a2,a3] f

unLHsExpr :: LHsExpr GhcTc -> HsExpr GhcTc
unLHsExpr (L _ e) = e

isMkStatementExpr :: HsExpr GhcTc -> Bool
isMkStatementExpr (HsVar _ (L _ var)) = isMkStatementName var
isMkStatementExpr (XExpr (WrapExpr (HsWrap _ inner))) = isMkStatementExpr inner
isMkStatementExpr _ = False

isMkStatementName :: Var -> Bool
isMkStatementName var =
  occNameString (nameOccName (varName var)) == "mkStatement"

findCallsInStmt :: StmtLR GhcTc GhcTc (LHsExpr GhcTc) -> [(SrcSpan, String)]
findCallsInStmt (BodyStmt _ body _ _) = findCallsInLExpr body
findCallsInStmt (BindStmt _ _ body) = findCallsInLExpr body
findCallsInStmt _ = []

isQueryFileExpr :: HsExpr GhcTc -> Bool
isQueryFileExpr (HsVar _ (L _ var)) = isQueryFileName var
isQueryFileExpr (XExpr (WrapExpr (HsWrap _ inner))) = isQueryFileExpr inner
isQueryFileExpr _ = False

isQueryFileName :: Var -> Bool
isQueryFileName var =
  let occ = occNameString (nameOccName (varName var))
   in occ == "queryFile" || occ == "queryFileAs" || occ == "query"

extractStringLit :: HsExpr GhcTc -> Maybe String
extractStringLit (HsLit _ (HsString _ fs)) = Just (unpackFS fs)
extractStringLit (HsOverLit _ (OverLit _ (HsIsString _ fs))) = Just (unpackFS fs)
extractStringLit (XExpr (WrapExpr (HsWrap _ inner))) = extractStringLit inner
extractStringLit _ = Nothing