canontra-0.1.0.0: src/Canontra/Fingerprint/Dependency.hs
{- |
Module : Canontra.Fingerprint.Dependency
Description : F3 rich dependency fingerprinting.
Dependency fingerprints focus exclusively on external and internal linkages.
By distilling the import graph, relative references, and resolved usage classifications
(unused, direct call, inheritance, type-only, value ref) independently of local business logic,
canontra enables instant detection of dependency updates or modular coupling changes.
-}
module Canontra.Fingerprint.Dependency
( computeF3
, extractDependencies
, extractRichDependencyGraph
) where
import Data.List (sort)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import Canontra.Canonical.Serialize (canonicalizeRichDependencyGraph)
import Canontra.Fingerprint.Source (hashBytes)
import Canontra.IR.Declaration
import Canontra.IR.Dependency
import Canontra.IR.Expression
import Canontra.IR.Program
import Canontra.Normalize.Normalize (normalizeProgram)
import Canontra.Types (Fingerprint)
computeF3 :: Program -> Fingerprint -- e.g. computeF3 prog -> F3 rich dependency hash
computeF3 prog =
let normProg = normalizeProgram prog
rdg = extractRichDependencyGraph normProg
canonBytes = canonicalizeRichDependencyGraph rdg
in hashBytes canonBytes
extractDependencies :: Program -> [ImportDecl] -- e.g. gathers all import statements across modules
extractDependencies (Program modules _) = concatMap modImports modules
extractRichDependencyGraph :: Program -> RichDependencyGraph
extractRichDependencyGraph prog@(Program modules _) =
let allImps = concatMap modImports modules
allCalls = collectCallNames prog
allBases = collectBaseNames prog
allTypes = collectTypeNames prog
allValRefs = collectValRefNames prog
resolvedImps = sort $ concatMap (toResolvedImports allCalls allBases allTypes allValRefs) allImps
intraDeps = sort $ collectIntraModuleDeps prog
in RichDependencyGraph resolvedImps intraDeps
toResolvedImports :: Set Text -> Set Text -> Set Text -> Set Text -> ImportDecl -> [ResolvedImport]
toResolvedImports calls bases types valRefs imp = case imp of
ImportModule modName maybeAlias ->
let sym = maybe (lastPart modName) id maybeAlias
usage = classifyUsage sym calls bases types valRefs
in [ResolvedImport modName Nothing maybeAlias (countLeadingDots modName) usage]
ImportFrom modName target -> case target of
ImportAll ->
[ResolvedImport modName (Just "*") Nothing (countLeadingDots modName) (DepValueRef ["*"])]
ImportSymbols syms ->
[ let boundName = maybe symName id maybeAlias
usage = classifyUsage boundName calls bases types valRefs
in ResolvedImport modName (Just symName) maybeAlias (countLeadingDots modName) usage
| (symName, maybeAlias) <- syms
]
where
lastPart m = case T.splitOn "." m of
[] -> m
xs -> last xs
classifyUsage :: Text -> Set Text -> Set Text -> Set Text -> Set Text -> DependencyUsage
classifyUsage sym calls bases types valRefs
| Set.member sym bases = DepInheritance [sym]
| Set.member sym calls = DepDirectCall [sym]
| Set.member sym valRefs = DepValueRef [sym]
| Set.member sym types = DepTypeOnly [sym]
| otherwise = DepUnused
countLeadingDots :: Text -> Int
countLeadingDots t = T.length (T.takeWhile (== '.') t)
collectCallNames :: Program -> Set Text
collectCallNames (Program modules _) =
foldMap modCalls modules
where
modCalls (Module _ _ decls stmts) =
foldMap declCalls decls `Set.union` foldMap stmtCalls stmts
declCalls (DeclFunction fn) = foldMap stmtCalls (fnBody fn)
declCalls (DeclClass cls) = foldMap (foldMap stmtCalls . fnBody) (clsMethods cls)
declCalls (DeclStruct st) = foldMap (foldMap stmtCalls . fnBody) (stMethods st)
declCalls (DeclTrait tr) = foldMap (foldMap stmtCalls . fnBody) (trMethods tr)
declCalls (DeclImpl imp) = foldMap (foldMap stmtCalls . fnBody) (impMethods imp)
declCalls (DeclReceiver _ fn) = foldMap stmtCalls (fnBody fn)
declCalls (DeclVariable _ _) = Set.empty
declCalls (DeclInterface _) = Set.empty
declCalls (DeclTypeAlias _ _) = Set.empty
stmtCalls stmt = case stmt of
StmtAssign targets val -> foldMap exprCalls (val : targets)
StmtAnnAssign target ty v -> foldMap exprCalls (target : ty : maybe [] pure v)
StmtAugAssign t _ v -> foldMap exprCalls [t, v]
StmtExpr e -> exprCalls e
StmtReturn me -> maybe Set.empty exprCalls me
StmtIf c b e -> Set.unions [exprCalls c, foldMap stmtCalls b, foldMap stmtCalls e]
StmtWhile c b e -> Set.unions [exprCalls c, foldMap stmtCalls b, foldMap stmtCalls e]
StmtFor t i b e -> Set.unions [exprCalls t, exprCalls i, foldMap stmtCalls b, foldMap stmtCalls e]
StmtAsyncFor t i b e -> Set.unions [exprCalls t, exprCalls i, foldMap stmtCalls b, foldMap stmtCalls e]
StmtTry b h e f -> Set.unions [foldMap stmtCalls b, foldMap (\(me, _, hb) -> maybe Set.empty exprCalls me `Set.union` foldMap stmtCalls hb) h, foldMap stmtCalls e, foldMap stmtCalls f]
StmtWith items b -> Set.unions (map (\(e, ma) -> exprCalls e `Set.union` maybe Set.empty exprCalls ma) items ++ [foldMap stmtCalls b])
StmtAsyncWith items b -> Set.unions (map (\(e, ma) -> exprCalls e `Set.union` maybe Set.empty exprCalls ma) items ++ [foldMap stmtCalls b])
StmtAssert e me -> exprCalls e `Set.union` maybe Set.empty exprCalls me
StmtRaise me mc -> maybe Set.empty exprCalls me `Set.union` maybe Set.empty exprCalls mc
StmtDelete es -> foldMap exprCalls es
StmtMatch s cs -> exprCalls s `Set.union` foldMap (\mc -> exprCalls (mcPattern mc) `Set.union` maybe Set.empty exprCalls (mcGuard mc) `Set.union` foldMap stmtCalls (mcBody mc)) cs
_ -> Set.empty
exprCalls expr = case expr of
ExprCall (ExprId name) args kwargs ->
Set.insert name (Set.unions (map exprCalls args ++ map (exprCalls . snd) kwargs))
ExprCall (ExprAttr (ExprId obj) _) args kwargs ->
Set.insert obj (Set.unions (map exprCalls args ++ map (exprCalls . snd) kwargs))
ExprCall target args kwargs ->
Set.unions (exprCalls target : map exprCalls args ++ map (exprCalls . snd) kwargs)
ExprBinary _ e1 e2 -> exprCalls e1 `Set.union` exprCalls e2
ExprUnary _ e -> exprCalls e
ExprAttr e _ -> exprCalls e
ExprSubscript e idx -> exprCalls e `Set.union` exprCalls idx
ExprList es -> foldMap exprCalls es
ExprTuple es -> foldMap exprCalls es
ExprDict pairs -> foldMap (\(k, v) -> exprCalls k `Set.union` exprCalls v) pairs
ExprSet es -> foldMap exprCalls es
ExprLambda _ body -> exprCalls body
ExprTernary c t f -> Set.unions [exprCalls c, exprCalls t, exprCalls f]
ExprListComp item comps -> exprCalls item `Set.union` foldMap compCalls comps
ExprDictComp k v comps -> exprCalls k `Set.union` exprCalls v `Set.union` foldMap compCalls comps
ExprSetComp item comps -> exprCalls item `Set.union` foldMap compCalls comps
ExprGenerator item comps -> exprCalls item `Set.union` foldMap compCalls comps
ExprWalrus _ val -> exprCalls val
ExprAwait e -> exprCalls e
ExprYield me -> maybe Set.empty exprCalls me
ExprYieldFrom e -> exprCalls e
ExprStarred e -> exprCalls e
ExprKwStarred e -> exprCalls e
_ -> Set.empty
compCalls (CompFor t iter ifs) = Set.unions [exprCalls t, exprCalls iter, foldMap exprCalls ifs]
collectBaseNames :: Program -> Set Text
collectBaseNames (Program modules _) =
Set.fromList [b | Module _ _ decls _ <- modules, DeclClass cls <- decls, b <- clsBases cls]
collectTypeNames :: Program -> Set Text
collectTypeNames (Program modules _) =
Set.fromList $ concatMap modTypes modules
where
modTypes (Module _ _ decls _) = concatMap declTypes decls
declTypes (DeclFunction fn) =
maybe [] pure (fnReturnType fn) ++ [t | p <- fnParams fn, Just t <- [paramType p]]
declTypes (DeclClass cls) =
concatMap (declTypes . DeclFunction) (clsMethods cls)
declTypes (DeclStruct st) =
[t | (_, Just t) <- stFields st] ++ concatMap (declTypes . DeclFunction) (stMethods st)
declTypes (DeclInterface iface) =
concatMap (declTypes . DeclFunction) (ifMethods iface)
declTypes (DeclReceiver _ fn) =
declTypes (DeclFunction fn)
declTypes (DeclTrait tr) =
concatMap (declTypes . DeclFunction) (trMethods tr)
declTypes (DeclImpl imp) =
concatMap (declTypes . DeclFunction) (impMethods imp)
declTypes (DeclVariable _ mTy) = maybe [] pure mTy
declTypes (DeclTypeAlias _ mTy) = maybe [] pure mTy
collectValRefNames :: Program -> Set Text
collectValRefNames (Program modules _) =
Set.fromList $ concatMap modValRefs modules
where
modValRefs (Module _ _ decls stmts) =
concatMap declValRefs decls ++ concatMap stmtValRefs stmts
declValRefs (DeclFunction fn) = concatMap stmtValRefs (fnBody fn)
declValRefs (DeclClass cls) = concatMap (concatMap stmtValRefs . fnBody) (clsMethods cls)
declValRefs (DeclStruct st) = concatMap (concatMap stmtValRefs . fnBody) (stMethods st)
declValRefs (DeclTrait tr) = concatMap (concatMap stmtValRefs . fnBody) (trMethods tr)
declValRefs (DeclImpl imp) = concatMap (concatMap stmtValRefs . fnBody) (impMethods imp)
declValRefs (DeclReceiver _ fn) = concatMap stmtValRefs (fnBody fn)
declValRefs (DeclVariable _ _) = []
declValRefs (DeclInterface _) = []
declValRefs (DeclTypeAlias _ _) = []
stmtValRefs stmt = case stmt of
StmtAssign targets val -> concatMap exprValRefs (val : targets)
StmtExpr e -> exprValRefs e
_ -> []
exprValRefs expr = case expr of
ExprId name -> [name]
ExprAttr (ExprId obj) _ -> [obj]
_ -> []
collectIntraModuleDeps :: Program -> [(Text, Text)]
collectIntraModuleDeps (Program modules _) =
concatMap modIntraDeps modules
where
modIntraDeps (Module _ _ decls _) =
let topFnNames = Set.fromList [fnName fn | DeclFunction fn <- decls]
topClsNames = Set.fromList [clsName cls | DeclClass cls <- decls]
allTopNames = Set.union topFnNames topClsNames
in [ (callerName, target)
| DeclFunction fn <- decls
, let callerName = fnName fn
, target <- Set.toList (collectCallNames (Program [Module "" [] [DeclFunction fn] []] ""))
, Set.member target allTopNames
, target /= callerName
]