diff --git a/README.md b/README.md index ce047407..951812bb 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ codewiki generate --github-pages --create-branch ## What is CodeWiki? -CodeWiki is an open-source framework for **automated repository-level documentation** across seven programming languages. It generates holistic, architecture-aware documentation that captures not only individual functions but also their cross-file, cross-module, and system-level interactions. +CodeWiki is an open-source framework for **automated repository-level documentation** across eight programming languages. It generates holistic, architecture-aware documentation that captures not only individual functions but also their cross-file, cross-module, and system-level interactions. ### Key Innovations @@ -88,7 +88,7 @@ CodeWiki is an open-source framework for **automated repository-level documentat ### Supported Languages -**🐍 Python** • **☕ Java** • **🟨 JavaScript** • **🔷 TypeScript** • **⚙️ C** • **🔧 C++** • **🪟 C#** +**🐍 Python** • **☕ Java** • **🟨 JavaScript** • **🔷 TypeScript** • **⚙️ C** • **🔧 C++** • **🪟 C#** • **🎯 Kotlin** --- diff --git a/codewiki/cli/utils/repo_validator.py b/codewiki/cli/utils/repo_validator.py index 3e17d031..608e6037 100644 --- a/codewiki/cli/utils/repo_validator.py +++ b/codewiki/cli/utils/repo_validator.py @@ -30,6 +30,8 @@ '.php', # PHP '.phtml', # PHP templates '.inc', # PHP includes + '.kt', # Kotlin + '.kts', # Kotlin Scripts } diff --git a/codewiki/cli/utils/validation.py b/codewiki/cli/utils/validation.py index 12cb5454..9711ba33 100644 --- a/codewiki/cli/utils/validation.py +++ b/codewiki/cli/utils/validation.py @@ -172,6 +172,7 @@ def detect_supported_languages(directory: Path) -> List[Tuple[str, int]]: 'C++': ['.cpp', '.hpp', '.cc', '.hh', '.cxx', '.hxx'], 'C#': ['.cs'], 'PHP': ['.php', '.phtml', '.inc'], + 'Kotlin': ['.kt', '.kts'], } # Directories to exclude from counting diff --git a/codewiki/src/be/dependency_analyzer/analysis/analysis_service.py b/codewiki/src/be/dependency_analyzer/analysis/analysis_service.py index aa3ba471..c9cf5bb6 100644 --- a/codewiki/src/be/dependency_analyzer/analysis/analysis_service.py +++ b/codewiki/src/be/dependency_analyzer/analysis/analysis_service.py @@ -310,6 +310,7 @@ def _filter_supported_languages(self, code_files: List[Dict]) -> List[Dict]: "php", "go", "rust", + "kotlin", } return [ @@ -320,7 +321,7 @@ def _filter_supported_languages(self, code_files: List[Dict]) -> List[Dict]: def _get_supported_languages(self) -> List[str]: """Get list of currently supported languages for analysis.""" - return ["python", "javascript", "typescript", "java", "csharp", "c", "cpp", "php"] + return ["python", "javascript", "typescript", "java", "csharp", "c", "cpp", "php", "kotlin"] def _cleanup_repository(self, temp_dir: str): """Clean up cloned repository.""" diff --git a/codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py b/codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py index 7175cd9b..da825fd4 100644 --- a/codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py +++ b/codewiki/src/be/dependency_analyzer/analysis/call_graph_analyzer.py @@ -126,6 +126,8 @@ def _analyze_code_file(self, repo_dir: str, file_info: Dict): self._analyze_typescript_file(file_path, content, repo_dir) elif language == "java": self._analyze_java_file(file_path, content, repo_dir) + elif language == "kotlin": + self._analyze_kotlin_file(file_path, content, repo_dir) elif language == "csharp": self._analyze_csharp_file(file_path, content, repo_dir) elif language == "c": @@ -280,6 +282,27 @@ def _analyze_java_file(self, file_path: str, content: str, repo_dir: str): except Exception as e: logger.error(f"Failed to analyze Java file {file_path}: {e}", exc_info=True) + def _analyze_kotlin_file(self, file_path: str, content: str, repo_dir: str): + """ + Analyze Kotlin file using tree-sitter based analyzer. + + Args: + file_path: Relative path to the Kotlin file + content: File content string + repo_dir: Repository base directory + """ + from codewiki.src.be.dependency_analyzer.analyzers.kotlin import analyze_kotlin_file + + try: + functions, relationships = analyze_kotlin_file(file_path, content, repo_path=repo_dir) + for func in functions: + func_id = func.id if func.id else f"{file_path}:{func.name}" + self.functions[func_id] = func + + self.call_relationships.extend(relationships) + except Exception as e: + logger.error(f"Failed to analyze Kotlin file {file_path}: {e}", exc_info=True) + def _analyze_csharp_file(self, file_path: str, content: str, repo_dir: str): """ Analyze C# file using tree-sitter based analyzer. @@ -408,6 +431,8 @@ def _generate_visualization_data(self) -> Dict: node_classes.append("lang-c") elif file_ext in [".cpp", ".cc", ".cxx", ".hpp", ".hxx"]: node_classes.append("lang-cpp") + elif file_ext in [".kt", ".kts"]: + node_classes.append("lang-kotlin") elif file_ext in [".php", ".phtml", ".inc"]: node_classes.append("lang-php") diff --git a/codewiki/src/be/dependency_analyzer/analyzers/kotlin.py b/codewiki/src/be/dependency_analyzer/analyzers/kotlin.py new file mode 100644 index 00000000..d56f220c --- /dev/null +++ b/codewiki/src/be/dependency_analyzer/analyzers/kotlin.py @@ -0,0 +1,505 @@ +import logging +from typing import List, Optional, Tuple +from pathlib import Path +import sys +import os + +from tree_sitter import Parser, Language +import tree_sitter_kotlin +from codewiki.src.be.dependency_analyzer.models.core import Node, CallRelationship + +logger = logging.getLogger(__name__) + +class TreeSitterKotlinAnalyzer: + def __init__(self, file_path: str, content: str, repo_path: Optional[str] = None): + self.file_path = Path(file_path) + self.content = content + self.repo_path = repo_path or "" + self.nodes: List[Node] = [] + self.call_relationships: List[CallRelationship] = [] + self._analyze() + + def _get_module_path(self) -> str: + if self.repo_path: + try: + rel_path = os.path.relpath(str(self.file_path), self.repo_path) + except ValueError: + rel_path = str(self.file_path) + else: + rel_path = str(self.file_path) + + for ext in ['.kt', '.kts']: + if rel_path.endswith(ext): + rel_path = rel_path[:-len(ext)] + break + return rel_path.replace('/', '.').replace('\\', '.') + + def _get_relative_path(self) -> str: + """Get relative path from repo root.""" + if self.repo_path: + try: + return os.path.relpath(str(self.file_path), self.repo_path) + except ValueError: + return str(self.file_path) + else: + return str(self.file_path) + + def _get_component_id(self, name: str, parent_class: Optional[str] = None) -> str: + module_path = self._get_module_path() + if parent_class: + return f"{module_path}.{parent_class}.{name}" + else: + return f"{module_path}.{name}" + + def _analyze(self): + try: + language_capsule = tree_sitter_kotlin.language() + kotlin_language = Language(language_capsule) + parser = Parser(kotlin_language) + tree = parser.parse(bytes(self.content, "utf8")) + root = tree.root_node + lines = self.content.splitlines() + + top_level_nodes = {} + + self._extract_nodes(root, top_level_nodes, lines) + self._extract_relationships(root, top_level_nodes) + except Exception as e: + logger.error(f"Error parsing Kotlin file {self.file_path}: {e}") + + def _extract_nodes(self, node, top_level_nodes, lines): + node_type = None + node_name = None + + if node.type == "class_declaration": + is_interface = any(c.type == "interface" for c in node.children) + + if is_interface: + node_type = "interface" + else: + modifiers = self._get_class_modifiers(node) + if "abstract" in modifiers: + node_type = "abstract class" + elif "data" in modifiers: + node_type = "data class" + elif "enum" in modifiers: + node_type = "enum class" + elif "annotation" in modifiers: + node_type = "annotation class" + else: + node_type = "class" + + name_node = next((c for c in node.children if c.type == "identifier"), None) + node_name = name_node.text.decode() if name_node else None + + elif node.type == "object_declaration": + node_type = "object" + name_node = next((c for c in node.children if c.type == "identifier"), None) + node_name = name_node.text.decode() if name_node else None + + elif node.type == "function_declaration": + name_node = next((c for c in node.children if c.type == "identifier"), None) + if name_node: + method_name = name_node.text.decode() + containing_class = self._find_containing_class_name(node) + if containing_class: + node_type = "method" + node_name = f"{containing_class}.{method_name}" + else: + node_type = "function" + node_name = method_name + + if node_type and node_name: + component_id = self._get_component_id(node_name) + relative_path = self._get_relative_path() + + # Extract docstring if present + docstring = "" + if node.prev_sibling and hasattr(node.prev_sibling, "type"): + if node.prev_sibling.type in ("line_comment", "block_comment"): + docstring = node.prev_sibling.text.decode().strip() + + # Safely extract code lines + start_line_idx = node.start_point[0] + end_line_idx = node.end_point[0] + 1 + code_snippet = "\n".join(lines[start_line_idx:end_line_idx]) if start_line_idx < len(lines) else "" + + node_obj = Node( + id=component_id, + name=node_name, + component_type=node_type, + file_path=str(self.file_path), + relative_path=relative_path, + source_code=code_snippet, + start_line=node.start_point[0]+1, + end_line=node.end_point[0]+1, + has_docstring=bool(docstring), + docstring=docstring, + parameters=None, + node_type=node_type, + base_classes=None, + class_name=None, + display_name=f"{node_type} {node_name}", + component_id=component_id + ) + self.nodes.append(node_obj) + top_level_nodes[node_name] = node_obj + + for child in node.children: + self._extract_nodes(child, top_level_nodes, lines) + + def _get_class_modifiers(self, class_node) -> set: + """Extract class modifiers (abstract, data, enum, annotation, etc.).""" + modifiers = set() + modifiers_node = next((c for c in class_node.children if c.type == "modifiers"), None) + if modifiers_node: + for mod in modifiers_node.children: + if mod.type in ("class_modifier", "inheritance_modifier", "visibility_modifier"): + for inner in mod.children: + modifiers.add(inner.type) + return modifiers + + def _extract_relationships(self, node, top_level_nodes): + # 1. Inheritance and Interface Implementation via delegation_specifiers + if node.type == "class_declaration": + class_name = self._get_identifier_name(node) + delegation_specifiers = next( + (c for c in node.children if c.type == "delegation_specifiers"), None + ) + if delegation_specifiers and class_name: + for spec in delegation_specifiers.children: + if spec.type == "delegation_specifier": + for child in spec.children: + type_name = None + if child.type == "constructor_invocation": + user_type = next( + (c for c in child.children if c.type == "user_type"), None + ) + if user_type: + type_name = self._get_type_name(user_type) + elif child.type == "user_type": + type_name = self._get_type_name(child) + + if type_name and not self._is_primitive_type(type_name): + caller_id = self._get_component_id(class_name) + callee_id = self._get_component_id(type_name) + self.call_relationships.append(CallRelationship( + caller=caller_id, + callee=callee_id, + call_line=node.start_point[0]+1, + is_resolved=False + )) + + # 2. Property Type Use (field types) + if node.type == "property_declaration": + containing_class = self._find_containing_class(node, top_level_nodes) + var_decl = next((c for c in node.children if c.type == "variable_declaration"), None) + if containing_class and var_decl: + type_node = next( + (c for c in var_decl.children if c.type == "user_type"), None + ) + if type_node: + prop_type_name = self._get_type_name(type_node) + if prop_type_name and not self._is_primitive_type(prop_type_name): + self.call_relationships.append(CallRelationship( + caller=containing_class, + callee=prop_type_name, + call_line=node.start_point[0]+1, + is_resolved=False + )) + + # 3. Constructor parameter type use + if node.type == "class_parameter": + containing_class_node = node.parent + while containing_class_node and containing_class_node.type != "class_declaration": + containing_class_node = containing_class_node.parent + if containing_class_node: + class_name = self._get_identifier_name(containing_class_node) + if class_name and class_name in top_level_nodes: + type_node = next( + (c for c in node.children if c.type == "user_type"), None + ) + if type_node: + param_type = self._get_type_name(type_node) + if param_type and not self._is_primitive_type(param_type): + caller_id = self._get_component_id(class_name) + self.call_relationships.append(CallRelationship( + caller=caller_id, + callee=param_type, + call_line=node.start_point[0]+1, + is_resolved=False + )) + + # 4. Method Calls / Function invocations + if node.type == "call_expression": + caller_id = self._find_containing_method(node) or self._find_containing_class(node, top_level_nodes) + + target_expr = next( + (c for c in node.children if c.type in ["identifier", "navigation_expression"]), None + ) + + if target_expr and caller_id: + if target_expr.type == "identifier": + callee_name = target_expr.text.decode() + if callee_name and callee_name[0].isupper() and not self._is_primitive_type(callee_name): + callee_id = self._get_component_id(callee_name) + self.call_relationships.append(CallRelationship( + caller=caller_id, + callee=callee_id, + call_line=node.start_point[0]+1, + is_resolved=False + )) + elif callee_name and not self._is_primitive_type(callee_name): + self.call_relationships.append(CallRelationship( + caller=caller_id, + callee=callee_name, + call_line=node.start_point[0]+1, + is_resolved=False + )) + + elif target_expr.type == "navigation_expression": + children = list(target_expr.children) + object_node = next( + (c for c in children if c.type == "identifier"), None + ) + method_node = None + identifiers = [c for c in children if c.type == "identifier"] + if len(identifiers) >= 2: + object_node = identifiers[0] + method_node = identifiers[-1] + elif len(identifiers) == 1: + method_node = identifiers[0] + nav_child = next( + (c for c in children if c.type == "navigation_expression"), None + ) + if nav_child: + object_node = self._get_root_identifier(nav_child) + else: + object_node = None + + if object_node and method_node: + object_name = object_node.text.decode() if hasattr(object_node, 'text') else str(object_node) + method_name = method_node.text.decode() + + target_type = None + if object_name in top_level_nodes: + target_type = object_name + else: + target_type = self._find_variable_type(node, object_name, top_level_nodes) + + if target_type and not self._is_primitive_type(target_type): + callee_id = self._get_component_id(target_type) + self.call_relationships.append(CallRelationship( + caller=caller_id, + callee=callee_id, + call_line=node.start_point[0]+1, + is_resolved=False + )) + elif method_node and not object_node: + callee_name = method_node.text.decode() + self.call_relationships.append(CallRelationship( + caller=caller_id, + callee=callee_name, + call_line=node.start_point[0]+1, + is_resolved=False + )) + + for child in node.children: + self._extract_relationships(child, top_level_nodes) + + def _is_primitive_type(self, type_name: str) -> bool: + """Check if type is a Kotlin primitive or common built-in type.""" + primitives = { + "Boolean", "Byte", "Char", "Double", "Float", "Int", "Long", "Short", + "String", "Unit", "Nothing", "Any", + "List", "Set", "Map", "Collection", "Iterable", "Sequence", + "MutableList", "MutableSet", "MutableMap", "MutableCollection", + "Array", "IntArray", "LongArray", "FloatArray", "DoubleArray", + "BooleanArray", "ByteArray", "CharArray", "ShortArray", + "Pair", "Triple", + } + return type_name in primitives + + def _get_identifier_name(self, node): + """Get identifier name from a node.""" + name_node = next((c for c in node.children if c.type == "identifier"), None) + return name_node.text.decode() if name_node else None + + def _get_type_name(self, node) -> Optional[str]: + """Get the primary type name from a type node, stripping generics.""" + if node.type == "user_type": + id_node = next((c for c in node.children if c.type == "identifier"), None) + return id_node.text.decode() if id_node else None + elif node.type == "nullable_type": + inner = next((c for c in node.children if c.type == "user_type"), None) + if inner: + return self._get_type_name(inner) + elif node.type == "identifier": + return node.text.decode() + return None + + def _get_root_identifier(self, nav_node): + """Get the root identifier from a chain of navigation_expressions.""" + first_child = nav_node.children[0] if nav_node.children else None + if first_child: + if first_child.type == "identifier": + return first_child + elif first_child.type == "navigation_expression": + return self._get_root_identifier(first_child) + return None + + def _find_containing_class_name(self, node): + """Walk up to find the containing class/object/interface name.""" + current = node.parent + while current: + if current.type in ("class_declaration", "object_declaration"): + name_node = next((c for c in current.children if c.type == "identifier"), None) + if name_node: + return name_node.text.decode() + current = current.parent + return None + + def _find_containing_class(self, node, top_level_nodes): + """Find the component ID of the containing class.""" + class_name = self._find_containing_class_name(node) + if class_name and class_name in top_level_nodes: + return self._get_component_id(class_name) + return None + + def _find_containing_method(self, node): + """Find the component ID of the containing function/method.""" + current = node.parent + while current: + if current.type == "function_declaration": + method_name = self._get_identifier_name(current) + class_name = self._find_containing_class_name(current) + if method_name: + if class_name: + return self._get_component_id(f"{class_name}.{method_name}") + return self._get_component_id(method_name) + current = current.parent + return None + + def _find_variable_type(self, node, variable_name: str, top_level_nodes) -> Optional[str]: + """ + Try to resolve the type of a variable by searching local declarations, + function parameters, constructor parameters, and class properties. + """ + func_node = node.parent + while func_node and func_node.type != "function_declaration": + func_node = func_node.parent + + if func_node: + params_node = next( + (c for c in func_node.children if c.type == "function_value_parameters"), None + ) + if params_node: + for param in params_node.children: + if param.type == "parameter": + param_name_node = next( + (c for c in param.children if c.type == "identifier"), None + ) + if param_name_node and param_name_node.text.decode() == variable_name: + type_node = next( + (c for c in param.children if c.type in ("user_type", "nullable_type")), None + ) + if type_node: + return self._get_type_name(type_node) + + body_node = next( + (c for c in func_node.children if c.type == "function_body"), None + ) + if body_node: + block = next((c for c in body_node.children if c.type == "block"), None) + if block: + result = self._search_variable_declaration(block, variable_name) + if result: + return result + + class_node = node.parent + while class_node and class_node.type not in ("class_declaration", "object_declaration"): + class_node = class_node.parent + + if class_node: + primary_ctor = next( + (c for c in class_node.children if c.type == "primary_constructor"), None + ) + if primary_ctor: + class_params = next( + (c for c in primary_ctor.children if c.type == "class_parameters"), None + ) + if class_params: + for param in class_params.children: + if param.type == "class_parameter": + param_name = next( + (c for c in param.children if c.type == "identifier"), None + ) + if param_name and param_name.text.decode() == variable_name: + type_node = next( + (c for c in param.children if c.type in ("user_type", "nullable_type")), None + ) + if type_node: + return self._get_type_name(type_node) + + class_body = next( + (c for c in class_node.children if c.type in ("class_body", "enum_class_body")), None + ) + if class_body: + for body_child in class_body.children: + if body_child.type == "property_declaration": + var_decl = next( + (c for c in body_child.children if c.type == "variable_declaration"), None + ) + if var_decl: + prop_name = next( + (c for c in var_decl.children if c.type == "identifier"), None + ) + if prop_name and prop_name.text.decode() == variable_name: + type_node = next( + (c for c in var_decl.children if c.type in ("user_type", "nullable_type")), None + ) + if type_node: + return self._get_type_name(type_node) + + return None + + def _search_variable_declaration(self, block_node, variable_name: str) -> Optional[str]: + """Search for a local variable declaration with explicit type annotation in a block.""" + for child in block_node.children: + if child.type == "property_declaration": + var_decl = next( + (c for c in child.children if c.type == "variable_declaration"), None + ) + if var_decl: + name_node = next( + (c for c in var_decl.children if c.type == "identifier"), None + ) + if name_node and name_node.text.decode() == variable_name: + type_node = next( + (c for c in var_decl.children if c.type in ("user_type", "nullable_type")), None + ) + if type_node: + return self._get_type_name(type_node) + + init_expr = next( + (c for c in child.children if c.type == "call_expression"), None + ) + if init_expr: + call_id = next( + (c for c in init_expr.children if c.type == "identifier"), None + ) + if call_id: + inferred = call_id.text.decode() + if inferred and inferred[0].isupper(): + return inferred + + elif child.type == "block": + result = self._search_variable_declaration(child, variable_name) + if result: + return result + + return None + +def analyze_kotlin_file(file_path: str, content: str, repo_path: Optional[str] = None) -> Tuple[List[Node], List[CallRelationship]]: + analyzer = TreeSitterKotlinAnalyzer(file_path, content, repo_path) + return analyzer.nodes, analyzer.call_relationships diff --git a/codewiki/src/be/dependency_analyzer/ast_parser.py b/codewiki/src/be/dependency_analyzer/ast_parser.py index 3323ed7a..81ac0bdc 100644 --- a/codewiki/src/be/dependency_analyzer/ast_parser.py +++ b/codewiki/src/be/dependency_analyzer/ast_parser.py @@ -135,7 +135,7 @@ def _determine_component_type(self, func_dict: Dict) -> str: def _file_to_module_path(self, file_path: str) -> str: path = file_path - extensions = ['.py', '.js', '.ts', '.java', '.cs', '.cpp', '.hpp', '.h', '.c', '.tsx', '.jsx', '.cc', '.mjs', '.cxx', '.cc', '.cjs'] + extensions = ['.py', '.js', '.ts', '.java', '.cs', '.cpp', '.hpp', '.h', '.c', '.tsx', '.jsx', '.cc', '.mjs', '.cxx', '.cc', '.cjs', '.kt', '.kts'] for ext in extensions: if path.endswith(ext): path = path[:-len(ext)] diff --git a/codewiki/src/be/dependency_analyzer/utils/patterns.py b/codewiki/src/be/dependency_analyzer/utils/patterns.py index 9fb003f7..1680ed4f 100644 --- a/codewiki/src/be/dependency_analyzer/utils/patterns.py +++ b/codewiki/src/be/dependency_analyzer/utils/patterns.py @@ -5,7 +5,7 @@ and function definitions across multiple programming languages. """ -from typing import List, Dict +from typing import List, Dict, Optional DEFAULT_IGNORE_PATTERNS = { ".github", @@ -156,6 +156,7 @@ "*.rb", "*.swift", "*.kt", + "*.kts", "*.scala", "*.clj", "*.hs", @@ -407,6 +408,7 @@ "c": ["void {name}", "int {name}", "{name}("], "cpp": ["void {name}", "int {name}", "{name}("], "php": ["function {name}", "public function {name}", "private function {name}", "protected function {name}"], + "kotlin": ["fun {name}", "private fun {name}", "public fun {name}", "internal fun {name}", "protected fun {name}"], "general": ["{name}("], # Fallback pattern } @@ -533,7 +535,7 @@ def has_high_connectivity_potential(filename: str, filepath: str) -> bool: return False -def is_critical_function(func_name: str, code_snippet: str = None) -> bool: +def is_critical_function(func_name: str, code_snippet: Optional[str] = None) -> bool: """ Check if a function is critical based on name and code patterns. diff --git a/codewiki/src/be/prompt_template.py b/codewiki/src/be/prompt_template.py index d37d5b61..f6da5f8b 100644 --- a/codewiki/src/be/prompt_template.py +++ b/codewiki/src/be/prompt_template.py @@ -235,6 +235,8 @@ ".cjs": "javascript", ".jsx": "javascript", ".cs": "csharp", + ".kt": "kotlin", + ".kts": "kotlin", ".php": "php", ".phtml": "php", ".inc": "php" diff --git a/pyproject.toml b/pyproject.toml index 00c3e01d..8360f6e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "tree-sitter-cpp>=0.23.4", "tree-sitter-c-sharp>=0.23.1", "tree-sitter-php>=0.23.0", + "tree-sitter-kotlin>=1.1.0", "openai>=1.107.0", "litellm>=1.77.0", "pydantic>=2.11.7", diff --git a/requirements.txt b/requirements.txt index bed6e91a..e2dce481 100644 --- a/requirements.txt +++ b/requirements.txt @@ -147,6 +147,7 @@ tree-sitter-cpp==0.23.4 tree-sitter-embedded-template==0.23.2 tree-sitter-java==0.23.5 tree-sitter-javascript==0.21.4 +tree-sitter-kotlin==1.1.0 tree-sitter-language-pack==0.8.0 tree-sitter-python==0.23.6 tree-sitter-typescript==0.21.2