2626import tempfile
2727from enum import Enum
2828from pathlib import Path
29- from typing import Any
3029
3130from git import Repo
3231from git .diff import NULL_TREE
@@ -45,6 +44,7 @@ class ReachabilityStatus(str, Enum):
4544
4645
4746def normalize_text (content ):
47+ """Normalize content (bytes) into a UTF-8 decoded string."""
4848 if content is None :
4949 return ""
5050
@@ -55,6 +55,10 @@ def normalize_text(content):
5555
5656
5757def detect_language_with_scancode (file_path , content ):
58+ """
59+ Detect the programming language of the text
60+ content using get_file_info function
61+ """
5862 content = normalize_text (content )
5963
6064 if not content :
@@ -74,12 +78,12 @@ def detect_language_with_scancode(file_path, content):
7478
7579
7680class GitRepositoryContext :
77- def __init__ (self , vcs_url : str ):
81+ def __init__ (self , vcs_url ):
7882 self .vcs_url = vcs_url
7983 self .repo_path = None
8084 self ._repo = None
8185
82- def __enter__ (self ) -> "GitRepositoryContext" :
86+ def __enter__ (self ):
8387 self .repo_path = tempfile .mkdtemp (prefix = "symbol-reachability-" )
8488 try :
8589 self ._repo = Repo .clone_from (self .vcs_url , self .repo_path )
@@ -101,12 +105,25 @@ def _cleanup(self):
101105
102106
103107class PatchAnalyzer :
104- def __init__ (self , repo : Repo , commit_hash : str ):
108+ def __init__ (self , repo : Repo , commit_hash ):
105109 self .repo = repo
106110 self .commit = repo .commit (commit_hash )
107111 self .parent_commit = self .commit .parents [0 ] if self .commit .parents else None
108112
109113 def get_changed_files (self ):
114+ """
115+ Retrieve all files changed by the commit along with their
116+ vulnerable and fixed contents.
117+
118+ For each changed file, a dictionary entry is created with two
119+ keys:
120+
121+ - vulnerable_text: The file content before the commit (empty
122+ string for newly added files).
123+ - fixed_text: The file content after the commit (empty string
124+ for deleted files).
125+
126+ """
110127 diffs = (
111128 self .parent_commit .diff (self .commit , create_patch = False )
112129 if self .parent_commit
@@ -170,6 +187,15 @@ def compute_changed_lines(cls, vulnerable_text, fixed_text):
170187
171188 @classmethod
172189 def diff_changed_symbols (cls , vuln_meta , fixed_meta ):
190+ """
191+ Compare the vulnerable and fixed symbol metadata and return the
192+ symbols that are unique to each side (i.e., whose body text
193+ differs between the two versions).
194+
195+ A symbol key is considered "vulnerable-only" if its body text
196+ does not match the corresponding symbol in fixed_meta, and
197+ vice versa.
198+ """
173199 vuln_only = {
174200 key : metadata
175201 for key , metadata in vuln_meta .items ()
@@ -183,6 +209,19 @@ def diff_changed_symbols(cls, vuln_meta, fixed_meta):
183209 return vuln_only , fixed_only
184210
185211 def collect_patch_symbols (self ):
212+ """
213+ Collect all changed symbols across every file modified by the
214+ commit, grouped by programming language.
215+
216+ For each changed file, the analyzer:
217+ - Retrieves the vulnerable and fixed file contents.
218+ - Computes which lines were removed and added.
219+ - Extracts symbols that intersect those changed lines using
220+ Tree-sitter parsing SymbolExtractor
221+ - Diffs the extracted symbols to find those whose body text
222+ actually changed.
223+ - Buckets the results by programming language.
224+ """
186225 by_language = {}
187226 changed_files = self .get_changed_files ()
188227
@@ -221,9 +260,16 @@ def collect_patch_symbols(self):
221260 return by_language
222261
223262 @classmethod
224- def build_symbol_metadata (
225- cls , nodes , extractor : SymbolExtractor , index : dict = None
226- ):
263+ def build_symbol_metadata (cls , nodes , extractor : SymbolExtractor , index ):
264+ """
265+ Build metadata dictionaries for a list of Tree-sitter AST nodes
266+ representing changed symbols.
267+
268+ For each node, the qualified name, body text, SHA-256 fingerprint,
269+ and start/end line numbers are extracted and stored in a
270+ dictionary keyed by the qualified name. If duplicate qualified
271+ names are encountered, a numeric suffix is appended to disambiguate.
272+ """
227273 if not nodes or not extractor :
228274 return {}
229275
@@ -259,6 +305,23 @@ def build_symbol_metadata(
259305 def analyze (
260306 cls , vulnerable_text , fixed_text , removed_lines , added_lines , file_path
261307 ):
308+ """
309+ Analyze the vulnerable and fixed versions of a single file to
310+ extract changed symbols.
311+
312+ The method performs the following steps:
313+
314+ - Detects the programming language of the file (using the
315+ fixed version first, falling back to the vulnerable version).
316+ - Verifies the language is supported by Tree-sitter queries.
317+ - Parses both versions into ASTs using Tree-sitter.
318+ - Extracts symbols whose line ranges intersect the removed
319+ lines (vulnerable side) or added lines (fixed side).
320+ - Builds metadata for each set of changed symbols.
321+ - Diffs the two metadata sets to find symbols whose body text
322+ actually changed between versions.
323+
324+ """
262325 vulnerable_text = normalize_text (vulnerable_text )
263326 fixed_text = normalize_text (fixed_text )
264327
@@ -317,6 +380,23 @@ def analyze(
317380
318381
319382def generate_reachability_report (patch , repo , candidate_resources , logger = None ):
383+ """
384+ Generate and store a symbol reachability report for a single
385+ vulnerability patch against a set of candidate codebase resources.
386+
387+ - Creates a PatchAnalyzer for the given commit.
388+ - Collects changed symbols from the patch, grouped by language.
389+ - For each candidate resource whose language matches a patch
390+ language, builds a ResourceAnalyzer index and uses
391+ ResourcePatchMatcher to match vulnerable and fixed
392+ symbols.
393+ - If matches are found, constructs a report dict containing
394+ evidence, matched symbol names, and a ReachabilityStatus.
395+ - Appends the report to the resource's extra_data under the
396+ symbols_reachability key (avoiding duplicates for the same
397+ commit hash).
398+
399+ """
320400 vcs_url = patch .get ("vcs_url" )
321401 commit_hash = patch .get ("commit_hash" )
322402
@@ -384,6 +464,20 @@ def generate_reachability_report(patch, repo, candidate_resources, logger=None):
384464
385465
386466def analyze_and_store_symbol_reachability_results (project , logger = None ):
467+ """
468+ Analyze all vulnerability patches for a project and store the
469+ resulting symbol reachability reports
470+
471+ The function iterates over all package vulnerabilities associated
472+ with the project, groups their patches by repository URL, clones
473+ each repository (using :class:`GitRepositoryContext`), checks out
474+ each patch's commit, and calls :func:`generate_reachability_report`
475+ to determine whether the vulnerable/fixed symbols from the patch
476+ are reachable in the project's codebase resources.
477+
478+ Only non-binary, non-archive, non-media files are considered as
479+ candidate resources.
480+ """
387481 candidate_resources = project .codebaseresources .files ().filter (
388482 is_binary = False , is_archive = False , is_media = False
389483 )
@@ -414,6 +508,10 @@ def analyze_and_store_symbol_reachability_results(project, logger=None):
414508
415509
416510def classify_reachability (evidence ):
511+ """
512+ Classify the reachability status of a vulnerability based on the
513+ collected evidence from :class:`ResourcePatchMatcher`.
514+ """
417515 if not evidence :
418516 return ReachabilityStatus .NOT_REACHABLE
419517
@@ -435,14 +533,18 @@ def classify_reachability(evidence):
435533
436534
437535class ResourceAnalyzer :
438- def __init__ (self , resource_text : str , language : str ):
536+ def __init__ (self , resource_text , language ):
439537 self .resource_text = normalize_text (resource_text )
440538 self .language = language
441539
442540 def process_node (
443- self , node , extractor , definitions_index , definitions : set , fingerprints : set
444- ) -> str | None :
445- """Extract the qualified name, update definitions, and fingerprint"""
541+ self , node , extractor , definitions_index , definitions , fingerprints
542+ ):
543+ """
544+ Process a single AST node to extract its qualified name, add it
545+ to the definitions set, compute its fingerprint, and add the
546+ fingerprint to the fingerprints set.
547+ """
446548 qualified_name = extractor ._build_qualified_name (node , definitions_index )
447549 if not qualified_name :
448550 return None
@@ -456,7 +558,16 @@ def process_node(
456558
457559 return qualified_name
458560
459- def build_index (self ) -> dict | None :
561+ def build_index (self ):
562+ """
563+ Build the full symbol index for the resource by parsing it
564+ with Tree-sitter and extracting all definitions, fingerprints,
565+ imports, and the reverse call graph.
566+
567+ The method iterates over all functions, classes, and constants
568+ in the resource's AST. For functions, it also extracts call
569+ expressions to populate the callers_of reverse call graph.
570+ """
460571 if not is_supported_language (self .language ) or not self .resource_text :
461572 return None
462573
@@ -503,15 +614,24 @@ def build_index(self) -> dict | None:
503614
504615
505616class ResourcePatchMatcher :
506- def __init__ (self , resource_index : dict ):
617+ def __init__ (self , resource_index ):
507618 self .resource_index = resource_index
508619 self .definitions = resource_index .get ("definitions" , set ())
509620 self .fingerprints = resource_index .get ("fingerprints" , set ())
510621 self .imports = resource_index .get ("imports" , {})
511622 self .callers_of = resource_index .get ("callers_of" , {})
512623 self .separator = resource_index .get ("separator" , "." )
513624
514- def match (self , patch_symbols_metadata : dict [str , Any ]) -> dict [str , Any ]:
625+ def match (self , patch_symbols_metadata ):
626+ """
627+ Match a set of patch symbols against the resource index and
628+ return evidence for each matched symbol.
629+
630+ For each symbol in patch_symbols_metadata, the method
631+ checks whether it is defined, imported, called, or has an
632+ exact fingerprint match in the resource. If at least one of
633+ these conditions is true, an evidence entry is created.
634+ """
515635 if not patch_symbols_metadata or not self .resource_index :
516636 return {}
517637
0 commit comments