@@ -1482,6 +1482,317 @@ def walk_calls(node, caller_nid: str) -> None:
14821482 return {"nodes" : nodes , "edges" : clean_edges }
14831483
14841484
1485+ # ── Zig ───────────────────────────────────────────────────────────────────────
1486+
1487+ def extract_zig (path : Path ) -> dict :
1488+ """Extract functions, structs, enums, unions, and imports from a .zig file."""
1489+ try :
1490+ import tree_sitter_zig as tszig
1491+ from tree_sitter import Language , Parser
1492+ except ImportError :
1493+ return {"nodes" : [], "edges" : [], "error" : "tree_sitter_zig not installed" }
1494+
1495+ try :
1496+ language = Language (tszig .language ())
1497+ parser = Parser (language )
1498+ source = path .read_bytes ()
1499+ tree = parser .parse (source )
1500+ root = tree .root_node
1501+ except Exception as e :
1502+ return {"nodes" : [], "edges" : [], "error" : str (e )}
1503+
1504+ stem = path .stem
1505+ str_path = str (path )
1506+ nodes : list [dict ] = []
1507+ edges : list [dict ] = []
1508+ seen_ids : set [str ] = set ()
1509+ function_bodies : list [tuple [str , Any ]] = []
1510+
1511+ def add_node (nid : str , label : str , line : int ) -> None :
1512+ if nid not in seen_ids :
1513+ seen_ids .add (nid )
1514+ nodes .append ({"id" : nid , "label" : label , "file_type" : "code" ,
1515+ "source_file" : str_path , "source_location" : f"L{ line } " })
1516+
1517+ def add_edge (src : str , tgt : str , relation : str , line : int ,
1518+ confidence : str = "EXTRACTED" , weight : float = 1.0 ) -> None :
1519+ edges .append ({"source" : src , "target" : tgt , "relation" : relation ,
1520+ "confidence" : confidence , "source_file" : str_path ,
1521+ "source_location" : f"L{ line } " , "weight" : weight })
1522+
1523+ file_nid = _make_id (stem )
1524+ add_node (file_nid , path .name , 1 )
1525+
1526+ def _extract_import (node ) -> None :
1527+ for child in node .children :
1528+ if child .type == "builtin_function" :
1529+ bi = None
1530+ args = None
1531+ for c in child .children :
1532+ if c .type == "builtin_identifier" :
1533+ bi = _read_text (c , source )
1534+ elif c .type == "arguments" :
1535+ args = c
1536+ if bi in ("@import" , "@cImport" ) and args :
1537+ for arg in args .children :
1538+ if arg .type in ("string_literal" , "string" ):
1539+ raw = _read_text (arg , source ).strip ('"' )
1540+ module_name = raw .split ("/" )[- 1 ].split ("." )[0 ]
1541+ if module_name :
1542+ tgt_nid = _make_id (module_name )
1543+ add_edge (file_nid , tgt_nid , "imports_from" ,
1544+ node .start_point [0 ] + 1 )
1545+ return
1546+ elif child .type == "field_expression" :
1547+ _extract_import (child )
1548+ return
1549+
1550+ def walk (node , parent_struct_nid : str | None = None ) -> None :
1551+ t = node .type
1552+
1553+ if t == "function_declaration" :
1554+ name_node = node .child_by_field_name ("name" )
1555+ if name_node :
1556+ func_name = _read_text (name_node , source )
1557+ line = node .start_point [0 ] + 1
1558+ if parent_struct_nid :
1559+ func_nid = _make_id (parent_struct_nid , func_name )
1560+ add_node (func_nid , f".{ func_name } ()" , line )
1561+ add_edge (parent_struct_nid , func_nid , "method" , line )
1562+ else :
1563+ func_nid = _make_id (stem , func_name )
1564+ add_node (func_nid , f"{ func_name } ()" , line )
1565+ add_edge (file_nid , func_nid , "contains" , line )
1566+ body = node .child_by_field_name ("body" )
1567+ if body :
1568+ function_bodies .append ((func_nid , body ))
1569+ return
1570+
1571+ if t == "variable_declaration" :
1572+ name_node = None
1573+ value_node = None
1574+ for child in node .children :
1575+ if child .type == "identifier" :
1576+ name_node = child
1577+ elif child .type in ("struct_declaration" , "enum_declaration" ,
1578+ "union_declaration" , "builtin_function" ,
1579+ "field_expression" ):
1580+ value_node = child
1581+
1582+ if value_node and value_node .type == "struct_declaration" :
1583+ if name_node :
1584+ struct_name = _read_text (name_node , source )
1585+ line = node .start_point [0 ] + 1
1586+ struct_nid = _make_id (stem , struct_name )
1587+ add_node (struct_nid , struct_name , line )
1588+ add_edge (file_nid , struct_nid , "contains" , line )
1589+ for child in value_node .children :
1590+ walk (child , parent_struct_nid = struct_nid )
1591+ return
1592+
1593+ if value_node and value_node .type in ("enum_declaration" , "union_declaration" ):
1594+ if name_node :
1595+ type_name = _read_text (name_node , source )
1596+ line = node .start_point [0 ] + 1
1597+ type_nid = _make_id (stem , type_name )
1598+ add_node (type_nid , type_name , line )
1599+ add_edge (file_nid , type_nid , "contains" , line )
1600+ return
1601+
1602+ if value_node and value_node .type in ("builtin_function" , "field_expression" ):
1603+ _extract_import (node )
1604+ return
1605+
1606+ for child in node .children :
1607+ walk (child , parent_struct_nid )
1608+
1609+ walk (root )
1610+
1611+ seen_call_pairs : set [tuple [str , str ]] = set ()
1612+
1613+ def walk_calls (node , caller_nid : str ) -> None :
1614+ if node .type == "function_declaration" :
1615+ return
1616+ if node .type == "call_expression" :
1617+ fn = node .child_by_field_name ("function" )
1618+ if fn :
1619+ callee = _read_text (fn , source ).split ("." )[- 1 ]
1620+ tgt_nid = next ((n ["id" ] for n in nodes if n ["label" ] in
1621+ (f"{ callee } ()" , f".{ callee } ()" )), None )
1622+ if tgt_nid and tgt_nid != caller_nid :
1623+ pair = (caller_nid , tgt_nid )
1624+ if pair not in seen_call_pairs :
1625+ seen_call_pairs .add (pair )
1626+ add_edge (caller_nid , tgt_nid , "calls" ,
1627+ node .start_point [0 ] + 1 ,
1628+ confidence = "INFERRED" , weight = 0.8 )
1629+ for child in node .children :
1630+ walk_calls (child , caller_nid )
1631+
1632+ for caller_nid , body_node in function_bodies :
1633+ walk_calls (body_node , caller_nid )
1634+
1635+ clean_edges = [e for e in edges if e ["source" ] in seen_ids and
1636+ (e ["target" ] in seen_ids or e ["relation" ] == "imports_from" )]
1637+ return {"nodes" : nodes , "edges" : clean_edges }
1638+
1639+
1640+ # ── PowerShell ────────────────────────────────────────────────────────────────
1641+
1642+ def extract_powershell (path : Path ) -> dict :
1643+ """Extract functions, classes, methods, and using statements from a .ps1 file."""
1644+ try :
1645+ import tree_sitter_powershell as tsps
1646+ from tree_sitter import Language , Parser
1647+ except ImportError :
1648+ return {"nodes" : [], "edges" : [], "error" : "tree_sitter_powershell not installed" }
1649+
1650+ try :
1651+ language = Language (tsps .language ())
1652+ parser = Parser (language )
1653+ source = path .read_bytes ()
1654+ tree = parser .parse (source )
1655+ root = tree .root_node
1656+ except Exception as e :
1657+ return {"nodes" : [], "edges" : [], "error" : str (e )}
1658+
1659+ stem = path .stem
1660+ str_path = str (path )
1661+ nodes : list [dict ] = []
1662+ edges : list [dict ] = []
1663+ seen_ids : set [str ] = set ()
1664+ function_bodies : list [tuple [str , Any ]] = []
1665+
1666+ def add_node (nid : str , label : str , line : int ) -> None :
1667+ if nid not in seen_ids :
1668+ seen_ids .add (nid )
1669+ nodes .append ({"id" : nid , "label" : label , "file_type" : "code" ,
1670+ "source_file" : str_path , "source_location" : f"L{ line } " })
1671+
1672+ def add_edge (src : str , tgt : str , relation : str , line : int ,
1673+ confidence : str = "EXTRACTED" , weight : float = 1.0 ) -> None :
1674+ edges .append ({"source" : src , "target" : tgt , "relation" : relation ,
1675+ "confidence" : confidence , "source_file" : str_path ,
1676+ "source_location" : f"L{ line } " , "weight" : weight })
1677+
1678+ file_nid = _make_id (stem )
1679+ add_node (file_nid , path .name , 1 )
1680+
1681+ _PS_SKIP = frozenset ({
1682+ "using" , "return" , "if" , "else" , "elseif" , "foreach" , "for" ,
1683+ "while" , "do" , "switch" , "try" , "catch" , "finally" , "throw" ,
1684+ "break" , "continue" , "exit" , "param" , "begin" , "process" , "end" ,
1685+ })
1686+
1687+ def _find_script_block_body (node ):
1688+ for child in node .children :
1689+ if child .type == "script_block" :
1690+ for sc in child .children :
1691+ if sc .type == "script_block_body" :
1692+ return sc
1693+ return child
1694+ return None
1695+
1696+ def walk (node , parent_class_nid : str | None = None ) -> None :
1697+ t = node .type
1698+
1699+ if t == "function_statement" :
1700+ name_node = next ((c for c in node .children if c .type == "function_name" ), None )
1701+ if name_node :
1702+ func_name = _read_text (name_node , source )
1703+ line = node .start_point [0 ] + 1
1704+ func_nid = _make_id (stem , func_name )
1705+ add_node (func_nid , f"{ func_name } ()" , line )
1706+ add_edge (file_nid , func_nid , "contains" , line )
1707+ body = _find_script_block_body (node )
1708+ if body :
1709+ function_bodies .append ((func_nid , body ))
1710+ return
1711+
1712+ if t == "class_statement" :
1713+ name_node = next ((c for c in node .children if c .type == "simple_name" ), None )
1714+ if name_node :
1715+ class_name = _read_text (name_node , source )
1716+ line = node .start_point [0 ] + 1
1717+ class_nid = _make_id (stem , class_name )
1718+ add_node (class_nid , class_name , line )
1719+ add_edge (file_nid , class_nid , "contains" , line )
1720+ for child in node .children :
1721+ walk (child , parent_class_nid = class_nid )
1722+ return
1723+
1724+ if t == "class_method_definition" :
1725+ name_node = next ((c for c in node .children if c .type == "simple_name" ), None )
1726+ if name_node :
1727+ method_name = _read_text (name_node , source )
1728+ line = node .start_point [0 ] + 1
1729+ if parent_class_nid :
1730+ method_nid = _make_id (parent_class_nid , method_name )
1731+ add_node (method_nid , f".{ method_name } ()" , line )
1732+ add_edge (parent_class_nid , method_nid , "method" , line )
1733+ else :
1734+ method_nid = _make_id (stem , method_name )
1735+ add_node (method_nid , f"{ method_name } ()" , line )
1736+ add_edge (file_nid , method_nid , "contains" , line )
1737+ body = _find_script_block_body (node )
1738+ if body :
1739+ function_bodies .append ((method_nid , body ))
1740+ return
1741+
1742+ if t == "command" :
1743+ cmd_name_node = next ((c for c in node .children if c .type == "command_name" ), None )
1744+ if cmd_name_node :
1745+ cmd_text = _read_text (cmd_name_node , source ).lower ()
1746+ if cmd_text == "using" :
1747+ tokens = []
1748+ for child in node .children :
1749+ if child .type == "command_elements" :
1750+ for el in child .children :
1751+ if el .type == "generic_token" :
1752+ tokens .append (_read_text (el , source ))
1753+ module_tokens = [t for t in tokens
1754+ if t .lower () not in ("namespace" , "module" , "assembly" )]
1755+ if module_tokens :
1756+ module_name = module_tokens [- 1 ].split ("." )[- 1 ]
1757+ add_edge (file_nid , _make_id (module_name ), "imports_from" ,
1758+ node .start_point [0 ] + 1 )
1759+ return
1760+
1761+ for child in node .children :
1762+ walk (child , parent_class_nid )
1763+
1764+ walk (root )
1765+
1766+ label_to_nid = {n ["label" ].strip ("()" ).lstrip ("." ).lower (): n ["id" ] for n in nodes }
1767+ seen_call_pairs : set [tuple [str , str ]] = set ()
1768+
1769+ def walk_calls (node , caller_nid : str ) -> None :
1770+ if node .type in ("function_statement" , "class_statement" ):
1771+ return
1772+ if node .type == "command" :
1773+ cmd_name_node = next ((c for c in node .children if c .type == "command_name" ), None )
1774+ if cmd_name_node :
1775+ cmd_text = _read_text (cmd_name_node , source )
1776+ if cmd_text .lower () not in _PS_SKIP :
1777+ tgt_nid = label_to_nid .get (cmd_text .lower ())
1778+ if tgt_nid and tgt_nid != caller_nid :
1779+ pair = (caller_nid , tgt_nid )
1780+ if pair not in seen_call_pairs :
1781+ seen_call_pairs .add (pair )
1782+ add_edge (caller_nid , tgt_nid , "calls" ,
1783+ node .start_point [0 ] + 1 ,
1784+ confidence = "INFERRED" , weight = 0.8 )
1785+ for child in node .children :
1786+ walk_calls (child , caller_nid )
1787+
1788+ for caller_nid , body_node in function_bodies :
1789+ walk_calls (body_node , caller_nid )
1790+
1791+ clean_edges = [e for e in edges if e ["source" ] in seen_ids and
1792+ (e ["target" ] in seen_ids or e ["relation" ] == "imports_from" )]
1793+ return {"nodes" : nodes , "edges" : clean_edges }
1794+
1795+
14851796# ── Cross-file import resolution ──────────────────────────────────────────────
14861797
14871798def _resolve_cross_file_imports (
@@ -1667,6 +1978,8 @@ def extract(paths: list[Path]) -> dict:
16671978 ".swift" : extract_swift ,
16681979 ".lua" : extract_lua ,
16691980 ".toc" : extract_lua ,
1981+ ".zig" : extract_zig ,
1982+ ".ps1" : extract_powershell ,
16701983 }
16711984
16721985 for path in paths :
@@ -1709,7 +2022,7 @@ def collect_files(target: Path) -> list[Path]:
17092022 "*.py" , "*.js" , "*.ts" , "*.tsx" , "*.go" , "*.rs" ,
17102023 "*.java" , "*.c" , "*.h" , "*.cpp" , "*.cc" , "*.cxx" , "*.hpp" ,
17112024 "*.rb" , "*.cs" , "*.kt" , "*.kts" , "*.scala" , "*.php" , "*.swift" ,
1712- "*.lua" , "*.toc" ,
2025+ "*.lua" , "*.toc" , "*.zig" , "*.ps1" ,
17132026 )
17142027 results : list [Path ] = []
17152028 for pattern in _EXTENSIONS :
0 commit comments