Source code for ghtest.coverage_analysis

import ast
from typing import Any, Dict, List, Set

try:
    import coverage
except ImportError:
    coverage = None


[docs] class BranchAnalyzer(ast.NodeVisitor): def __init__(self, covered_lines: Set[int], missing_lines: Set[int]) -> None: self.covered_lines = covered_lines self.missing_lines = missing_lines self.missed_branches: List[Dict[str, Any]] = []
[docs] def visit_If(self, node: ast.If) -> None: self.check_branch(node) self.generic_visit(node)
[docs] def visit_While(self, node: ast.While) -> None: self.check_branch(node) self.generic_visit(node)
[docs] def check_branch(self, node: ast.AST) -> None: # The line of the 'if' or 'while' statement start_line = node.lineno # If the condition itself wasn't executed, we can't do anything yet (unreachable) if start_line not in self.covered_lines: return # Check body coverage body_covered = self.is_block_covered(node.body) # Check orelse coverage orelse_covered = False if node.orelse: orelse_covered = self.is_block_covered(node.orelse) # We can't easily detect implicit else without CFG, but if body wasn't covered, # we definitely took the else path (or crashed). # If body WAS covered, we might have missed the else path. if not body_covered: self.missed_branches.append( {"line": start_line, "condition": node.test, "needed": True} ) if node.orelse and not orelse_covered: self.missed_branches.append( {"line": start_line, "condition": node.test, "needed": False} )
[docs] def is_block_covered(self, nodes: List[ast.AST]) -> bool: """Check if any line in the block was executed.""" for node in nodes: if hasattr(node, "lineno"): if node.lineno in self.covered_lines: return True return False
[docs] def analyze_file_coverage(filepath: str, cov_data: Any) -> List[Dict[str, Any]]: """ Analyze a single file to find missed branches using coverage data. Returns a list of missed branch hints. """ if coverage is None: return [] try: with open(filepath, "r", encoding="utf-8") as f: source = f.read() except OSError: return [] try: tree = ast.parse(source, filename=filepath) except SyntaxError: return [] try: # analysis() returns (filename, statements, excluded, missing, missing_formatted) analysis = cov_data.analysis(filepath) except coverage.misc.CoverageException: return [] executable_lines = set(analysis[1]) missing_lines = set(analysis[2]) # Index 2 is missing lines covered_lines = executable_lines - missing_lines analyzer = BranchAnalyzer(covered_lines, missing_lines) analyzer.visit(tree) return analyzer.missed_branches