CoolFace
Apppublic

salim0986/graph-bug-ai

sourceHugging Facemitupdated 4mo agoView on Hugging Face
0likes
chunker.py216 linesDownload Raw Back to src
1import tiktoken2from typing import List, Dict, Any, Optional3from tree_sitter import Node4import os5 6class SemanticChunker:7    """8    Splits source code into semantic chunks respecting AST boundaries.9    Uses tiktoken for fast token estimation.10    """11    def __init__(self, max_tokens: int = 512, overlap_tokens: int = 64):12        self.max_tokens = max_tokens13        self.overlap_tokens = overlap_tokens14        # Use cl100k_base as a fast generic tokenizer approximation15        self.encoder = tiktoken.get_encoding("cl100k_base")16        17        # AST node types that represent structural boundaries18        self.function_types = {19            "function_definition", "function_declaration", "function_item",20            "method_definition", "method_declaration",21            "arrow_function", "generator_function_declaration"22        }23        self.class_types = {24            "class_definition", "class_declaration",25            "interface_declaration", "impl_item"26        }27 28    def _get_tokens(self, text: str) -> List[int]:29        return self.encoder.encode(text, disallowed_special=())30        31    def _decode_tokens(self, tokens: List[int]) -> str:32        return self.encoder.decode(tokens)33 34    def _split_large_text(self, text: str, start_line: int, parent_func: Optional[str], parent_class: Optional[str]) -> List[Dict[str, Any]]:35        """Split a large text block into overlapping chunks."""36        tokens = self._get_tokens(text)37        chunks = []38 39        if len(tokens) <= self.max_tokens:40            return [{41                "text": text,42                "start_line": start_line,43                "end_line": start_line + text.count("\n"),44                "parent_function": parent_func,45                "parent_class": parent_class46            }]47 48        step = self.max_tokens - self.overlap_tokens49        if step <= 0:50            step = self.max_tokens  # Fallback: no overlap51 52        # Pre-compute cumulative newline counts at each token boundary.53        # This turns line-number estimation from O(N²) into O(N) total.54        # We walk the text once, mapping token offset → newline count.55        token_newlines: List[int] = [0] * (len(tokens) + 1)56        decoded_so_far = ""57        for idx in range(len(tokens)):58            decoded_so_far = self._decode_tokens(tokens[:idx + 1])59            token_newlines[idx + 1] = decoded_so_far.count("\n")60 61        for i in range(0, len(tokens), step):62            chunk_tokens = tokens[i:i + self.max_tokens]63            chunk_text = self._decode_tokens(chunk_tokens)64            chunk_start_line = start_line + token_newlines[i]65            chunk_end_line = start_line + token_newlines[min(i + self.max_tokens, len(tokens))]66 67            chunks.append({68                "text": chunk_text,69                "start_line": chunk_start_line,70                "end_line": chunk_end_line,71                "parent_function": parent_func,72                "parent_class": parent_class73            })74 75        return chunks76 77    def _extract_name(self, node: Node, code_bytes: bytes) -> Optional[str]:78        """Extract the name of a function or class node."""79        name_node = node.child_by_field_name("name")80        if name_node:81            return code_bytes[name_node.start_byte:name_node.end_byte].decode("utf8", errors="ignore")82        83        # Fallback: find the first identifier child84        for child in node.children:85            if child.type == "identifier" or child.type == "type_identifier":86                return code_bytes[child.start_byte:child.end_byte].decode("utf8", errors="ignore")87        return None88 89    def _process_node(90        self, 91        node: Node, 92        code_bytes: bytes, 93        current_class: Optional[str] = None94    ) -> List[Dict[str, Any]]:95        """Recursively process nodes, extracting semantic chunks."""96        chunks = []97        node_type = node.type98        99        if node_type in self.class_types:100            # We found a class, update current_class and process children101            class_name = self._extract_name(node, code_bytes) or current_class102            node_text = code_bytes[node.start_byte:node.end_byte].decode("utf8", errors="ignore")103            tokens = self._get_tokens(node_text)104            if len(tokens) <= self.max_tokens:105                chunks.append({106                    "text": node_text,107                    "start_line": node.start_point[0],108                    "end_line": node.end_point[0],109                    "parent_function": None,110                    "parent_class": class_name111                })112            else:113                for child in node.children:114                    chunks.extend(self._process_node(child, code_bytes, class_name))115            return chunks116 117        elif node_type in self.function_types:118            # We found a function/method. Treat it as a single unit (split if too large)119            func_name = self._extract_name(node, code_bytes)120            node_text = code_bytes[node.start_byte:node.end_byte].decode("utf8", errors="ignore")121            chunks.extend(self._split_large_text(122                text=node_text,123                start_line=node.start_point[0],124                parent_func=func_name,125                parent_class=current_class126            ))127            return chunks128 129        # If it's a root node, process its children130        if node.parent is None:131            for child in node.children:132                chunks.extend(self._process_node(child, code_bytes, current_class))133            return chunks134            135        # Top-level statements (e.g. imports) or class-level statements (e.g. fields)136        # We can identify them if their parent is the root or if their parent is a class/block137        # To avoid leaf nodes, we only chunk statements (nodes with children or specific types)138        # A simpler way: if we are here and we didn't recurse yet, we should recurse to find functions, 139        # but also capture non-function statements.140        # Actually, let's just recurse. For simplicity, we might lose some non-function statements if they are 141        # nested and we don't capture them. Let's capture them if they are direct children of root or class body.142        143        is_top_level = node.parent and node.parent.parent is None144        is_class_level = current_class and node.parent and node.parent.type in ("block", "declaration_list")145        146        if is_top_level or is_class_level:147            node_text = code_bytes[node.start_byte:node.end_byte].decode("utf8", errors="ignore")148            if node_text.strip() and not any(child.type in self.function_types for child in node.children):149                chunks.extend(self._split_large_text(150                    text=node_text,151                    start_line=node.start_point[0],152                    parent_func=None,153                    parent_class=current_class154                ))155                return chunks156                157        # Otherwise, keep recursing to find nested functions/classes158        for child in node.children:159            chunks.extend(self._process_node(child, code_bytes, current_class))160                161        return chunks162 163    def chunk_ast(self, tree: Any, code_bytes: bytes, file_path: str, language: str) -> List[Dict[str, Any]]:164        """165        Takes a tree-sitter AST and code, returns list of chunks.166        Each chunk: {file, start_line, end_line, parent_function, parent_class, language, raw_code}167        """168        raw_chunks = self._process_node(tree.root_node, code_bytes)169        170        # Post-process to add file and language metadata, and group tiny adjacent chunks?171        # Grouping tiny adjacent chunks (like sequential imports) makes retrieval better.172        grouped_chunks = []173        current_group = None174        current_tokens = 0175        176        for c in raw_chunks:177            c_tokens = len(self._get_tokens(c["text"]))178            179            # If the chunk has a function or class context, keep it separate from global blocks180            has_context = c["parent_function"] is not None or c["parent_class"] is not None181            182            if not has_context and current_group is not None and (current_tokens + c_tokens) <= self.max_tokens:183                # Append to current group184                current_group["text"] += "\n" + c["text"]185                current_group["end_line"] = c["end_line"]186                current_tokens += c_tokens187            else:188                if current_group is not None:189                    grouped_chunks.append(current_group)190                    191                if has_context or c_tokens > 20: # Only start new group if it's substantial or has context192                    current_group = c.copy()193                    current_tokens = c_tokens194                elif not has_context:195                    # Start a new group anyway for small global statements196                    current_group = c.copy()197                    current_tokens = c_tokens198                    199        if current_group is not None:200            grouped_chunks.append(current_group)201            202        # Finalize format203        final_chunks = []204        for c in grouped_chunks:205            final_chunks.append({206                "file": file_path,207                "language": language,208                "start_line": c["start_line"],209                "end_line": c["end_line"],210                "parent_function": c.get("parent_function"),211                "parent_class": c.get("parent_class"),212                "raw_code": c["text"].strip()213            })214            215        return final_chunks216