-
Notifications
You must be signed in to change notification settings - Fork 46
Add Kotlin language support #592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
6442af8
Add Kotlin language support
gkorland 20d093b
fix: address review feedback for Kotlin analyzer
gkorland 692fd06
fix: address review — fix return types, indentation, path normalization
gkorland e5eda92
fix(analyzers): restore C# support, fix return types, improve delegat…
gkorland File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| from pathlib import Path | ||
| from ...entities.entity import Entity | ||
| from ...entities.file import File | ||
| from typing import Optional | ||
| from ..analyzer import AbstractAnalyzer | ||
|
|
||
| from multilspy import SyncLanguageServer | ||
|
|
||
| import tree_sitter_kotlin as tskotlin | ||
| from tree_sitter import Language, Node | ||
|
|
||
| import logging | ||
| logger = logging.getLogger('code_graph') | ||
|
|
||
| class KotlinAnalyzer(AbstractAnalyzer): | ||
| def __init__(self) -> None: | ||
| super().__init__(Language(tskotlin.language())) | ||
|
|
||
| def add_dependencies(self, path: Path, files: list[Path]): | ||
| # For now, we skip dependency resolution for Kotlin | ||
| # In the future, this could parse build.gradle or pom.xml for Kotlin projects | ||
| pass | ||
|
|
||
| def get_entity_label(self, node: Node) -> str: | ||
| if node.type == 'class_declaration': | ||
| # Check if it's an interface by looking for interface keyword | ||
| for child in node.children: | ||
| if child.type == 'interface': | ||
| return "Interface" | ||
| return "Class" | ||
| elif node.type == 'object_declaration': | ||
| return "Object" | ||
| elif node.type == 'function_declaration': | ||
| # Check if this is a method (inside a class) or a top-level function | ||
| parent = node.parent | ||
| if parent and parent.type == 'class_body': | ||
| return "Method" | ||
| return "Function" | ||
| raise ValueError(f"Unknown entity type: {node.type}") | ||
|
|
||
| def get_entity_name(self, node: Node) -> str: | ||
| if node.type in ['class_declaration', 'object_declaration', 'function_declaration']: | ||
| for child in node.children: | ||
| if child.type == 'identifier': | ||
| return child.text.decode('utf-8') | ||
| raise ValueError(f"Cannot extract name from entity type: {node.type}") | ||
|
|
||
| def get_entity_docstring(self, node: Node) -> Optional[str]: | ||
| if node.type in ['class_declaration', 'object_declaration', 'function_declaration']: | ||
| # Check for KDoc comment (/** ... */) before the node | ||
| if node.prev_sibling and node.prev_sibling.type == "multiline_comment": | ||
| comment_text = node.prev_sibling.text.decode('utf-8') | ||
| # Only return if it's a KDoc comment (starts with /**) | ||
| if comment_text.startswith('/**'): | ||
| return comment_text | ||
| return None | ||
| raise ValueError(f"Unknown entity type: {node.type}") | ||
|
|
||
| def get_entity_types(self) -> list[str]: | ||
| return ['class_declaration', 'object_declaration', 'function_declaration'] | ||
|
|
||
| def _get_delegation_types(self, entity: Entity) -> list[tuple]: | ||
| """Extract type identifiers from delegation specifiers in order. | ||
|
|
||
| Returns list of (node, is_constructor_invocation) tuples. | ||
| constructor_invocation indicates a superclass; plain user_type indicates an interface. | ||
| """ | ||
| types = [] | ||
| for child in entity.node.children: | ||
| if child.type == 'delegation_specifiers': | ||
| for spec in child.children: | ||
| if spec.type == 'delegation_specifier': | ||
| for sub in spec.children: | ||
| if sub.type == 'constructor_invocation': | ||
| for s in sub.children: | ||
| if s.type == 'user_type': | ||
| for id_node in s.children: | ||
| if id_node.type == 'identifier': | ||
| types.append((id_node, True)) | ||
| elif sub.type == 'user_type': | ||
| for id_node in sub.children: | ||
| if id_node.type == 'identifier': | ||
| types.append((id_node, False)) | ||
| return types | ||
|
|
||
| def add_symbols(self, entity: Entity) -> None: | ||
| if entity.node.type == 'class_declaration': | ||
| types = self._get_delegation_types(entity) | ||
| for node, is_class in types: | ||
| if is_class: | ||
| entity.add_symbol("base_class", node) | ||
| else: | ||
| entity.add_symbol("implement_interface", node) | ||
|
|
||
| elif entity.node.type == 'object_declaration': | ||
| types = self._get_delegation_types(entity) | ||
| for node, _ in types: | ||
| entity.add_symbol("implement_interface", node) | ||
|
|
||
| elif entity.node.type == 'function_declaration': | ||
| # Find function calls | ||
| captures = self._captures("(call_expression) @reference.call", entity.node) | ||
| if 'reference.call' in captures: | ||
| for caller in captures['reference.call']: | ||
| entity.add_symbol("call", caller) | ||
|
|
||
| # Find parameters with types | ||
| captures = self._captures("(parameter (user_type (identifier) @parameter))", entity.node) | ||
| if 'parameter' in captures: | ||
| for parameter in captures['parameter']: | ||
| entity.add_symbol("parameters", parameter) | ||
|
|
||
| # Find return type | ||
| captures = self._captures("(function_declaration (user_type (identifier) @return_type))", entity.node) | ||
| if 'return_type' in captures: | ||
| for return_type in captures['return_type']: | ||
| entity.add_symbol("return_type", return_type) | ||
|
|
||
| def is_dependency(self, file_path: str) -> bool: | ||
| # Check if file is in a dependency directory (e.g., build, .gradle cache) | ||
| return "build/" in file_path or ".gradle/" in file_path or "/cache/" in file_path | ||
|
|
||
| def resolve_path(self, file_path: str, path: Path) -> str: | ||
| # For Kotlin, just return the file path as-is for now | ||
| return file_path | ||
|
|
||
| def resolve_type(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]: | ||
| res = [] | ||
| for file, resolved_node in self.resolve(files, lsp, file_path, path, node): | ||
| type_dec = self.find_parent(resolved_node, ['class_declaration', 'object_declaration']) | ||
| if type_dec in file.entities: | ||
| res.append(file.entities[type_dec]) | ||
| return res | ||
|
|
||
| def resolve_method(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]: | ||
| res = [] | ||
| # For call expressions, we need to extract the function name | ||
| if node.type == 'call_expression': | ||
| # Find the identifier being called | ||
| for child in node.children: | ||
| if child.type in ['identifier', 'navigation_expression']: | ||
| for file, resolved_node in self.resolve(files, lsp, file_path, path, child): | ||
| method_dec = self.find_parent(resolved_node, ['function_declaration', 'class_declaration', 'object_declaration']) | ||
| if method_dec and method_dec.type in ['class_declaration', 'object_declaration']: | ||
| continue | ||
| if method_dec in file.entities: | ||
| res.append(file.entities[method_dec]) | ||
| break | ||
| return res | ||
|
|
||
| def resolve_symbol(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, key: str, symbol: Node) -> list[Entity]: | ||
| if key in ["implement_interface", "base_class", "parameters", "return_type"]: | ||
| return self.resolve_type(files, lsp, file_path, path, symbol) | ||
| elif key in ["call"]: | ||
| return self.resolve_method(files, lsp, file_path, path, symbol) | ||
| else: | ||
| raise ValueError(f"Unknown key {key}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,31 @@ | ||
| from typing import Callable, Self | ||
| from tree_sitter import Node | ||
|
|
||
| class Symbol: | ||
| def __init__(self, symbol: Node): | ||
| self.symbol = symbol | ||
| self.resolved_symbol = set() | ||
|
|
||
| def add_resolve_symbol(self, resolved_symbol): | ||
| self.resolved_symbol.add(resolved_symbol) | ||
|
|
||
| class Entity: | ||
| def __init__(self, node: Node): | ||
| self.node = node | ||
| self.symbols: dict[str, list[Symbol]] = {} | ||
| self.symbols: dict[str, list[Node]] = {} | ||
| self.resolved_symbols: dict[str, set[Self]] = {} | ||
| self.children: dict[Node, Self] = {} | ||
|
|
||
| def add_symbol(self, key: str, symbol: Node): | ||
| if key not in self.symbols: | ||
| self.symbols[key] = [] | ||
| self.symbols[key].append(Symbol(symbol)) | ||
| self.symbols[key].append(symbol) | ||
|
|
||
| def add_resolved_symbol(self, key: str, symbol: Self): | ||
| if key not in self.resolved_symbols: | ||
| self.resolved_symbols[key] = set() | ||
| self.resolved_symbols[key].add(symbol) | ||
|
|
||
| def add_child(self, child: Self): | ||
| child.parent = self | ||
| self.children[child.node] = child | ||
|
|
||
| def resolved_symbol(self, f: Callable[[str, Node], list[Self]]): | ||
| for key, symbols in self.symbols.items(): | ||
| self.resolved_symbols[key] = set() | ||
| for symbol in symbols: | ||
| for resolved_symbol in f(key, symbol.symbol): | ||
| symbol.add_resolve_symbol(resolved_symbol) | ||
| for resolved_symbol in f(key, symbol): | ||
| self.resolved_symbols[key].add(resolved_symbol) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * A base interface for logging | ||
| */ | ||
| interface Logger { | ||
| fun log(message: String) | ||
| } | ||
|
|
||
| /** | ||
| * Base class for shapes | ||
| */ | ||
| open class Shape(val name: String) { | ||
| open fun area(): Double = 0.0 | ||
| } | ||
|
|
||
| class Circle(val radius: Double) : Shape("circle"), Logger { | ||
| override fun area(): Double { | ||
| return Math.PI * radius * radius | ||
| } | ||
|
|
||
| override fun log(message: String) { | ||
| println(message) | ||
| } | ||
| } | ||
|
|
||
| fun calculateTotal(shapes: List<Shape>): Double { | ||
| var total = 0.0 | ||
| for (shape in shapes) { | ||
| total += shape.area() | ||
| } | ||
| return total | ||
| } | ||
|
|
||
| object AppConfig : Logger { | ||
| val version = "1.0" | ||
|
|
||
| override fun log(message: String) { | ||
| println("[$version] $message") | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CALLSrelationships no longer include call-site properties (previouslylineandtextwere stored). With the newresolved_symbolsset-based structure, this also loses per-call-site granularity when the same callee is invoked multiple times. Consider preserving a mapping from each call node to its resolved entity(ies) so you can keep call-site metadata on edges.