Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions api/analyzers/source_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .java.analyzer import JavaAnalyzer
from .python.analyzer import PythonAnalyzer
from .csharp.analyzer import CSharpAnalyzer
from .typescript.analyzer import TypeScriptAnalyzer

from multilspy import SyncLanguageServer
from multilspy.multilspy_config import MultilspyConfig
Expand All @@ -26,7 +27,9 @@
# '.h': CAnalyzer(),
'.py': PythonAnalyzer(),
'.java': JavaAnalyzer(),
'.cs': CSharpAnalyzer()}
'.cs': CSharpAnalyzer(),
'.ts': TypeScriptAnalyzer(),
'.tsx': TypeScriptAnalyzer()}

class NullLanguageServer:
def start_server(self):
Expand Down Expand Up @@ -143,7 +146,15 @@ def second_pass(self, graph: Graph, files: list[Path], path: Path) -> None:
lsps[".cs"] = SyncLanguageServer.create(config, logger, str(path))
else:
lsps[".cs"] = NullLanguageServer()
with lsps[".java"].start_server(), lsps[".py"].start_server(), lsps[".cs"].start_server():
if any(path.rglob('*.ts')) or any(path.rglob('*.tsx')):
config = MultilspyConfig.from_dict({"code_language": "typescript"})
ts_lsp = SyncLanguageServer.create(config, logger, str(path))
lsps[".ts"] = ts_lsp
lsps[".tsx"] = ts_lsp
else:
lsps[".ts"] = NullLanguageServer()
lsps[".tsx"] = NullLanguageServer()
with lsps[".java"].start_server(), lsps[".py"].start_server(), lsps[".cs"].start_server(), lsps[".ts"].start_server():
files_len = len(self.files)
for i, file_path in enumerate(files):
file = self.files[file_path]
Expand Down Expand Up @@ -174,7 +185,7 @@ def analyze_files(self, files: list[Path], path: Path, graph: Graph) -> None:

def analyze_sources(self, path: Path, ignore: list[str], graph: Graph) -> None:
path = path.resolve()
files = list(path.rglob("*.java")) + list(path.rglob("*.py")) + list(path.rglob("*.cs"))
files = list(path.rglob("*.java")) + list(path.rglob("*.py")) + list(path.rglob("*.cs")) + list(path.rglob("*.ts")) + list(path.rglob("*.tsx"))
# First pass analysis of the source code
self.first_pass(path, files, ignore, graph)

Expand Down
Empty file.
119 changes: 119 additions & 0 deletions api/analyzers/typescript/analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import subprocess
from pathlib import Path

from multilspy import SyncLanguageServer
from ...entities import *
from typing import Optional
from ..analyzer import AbstractAnalyzer

import tree_sitter_typescript as tstypescript
from tree_sitter import Language, Node

import logging
logger = logging.getLogger('code_graph')

class TypeScriptAnalyzer(AbstractAnalyzer):
def __init__(self) -> None:
super().__init__(Language(tstypescript.language_typescript()))

def add_dependencies(self, path: Path, files: list[Path]):
if Path(f"{path}/node_modules").is_dir():
return
if Path(f"{path}/package.json").is_file():
subprocess.run(["npm", "install"], cwd=str(path))
Comment on lines +19 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for npm install and consider security implications.

  1. The files parameter is unused (Ruff ARG002).
  2. subprocess.run without check=True silently ignores failures.
  3. Using partial executable path "npm" (Ruff S607) could be a security concern in certain environments.
🛡️ Proposed fix with error handling
-    def add_dependencies(self, path: Path, files: list[Path]):
+    def add_dependencies(self, path: Path, files: list[Path]):  # noqa: ARG002
         if Path(f"{path}/node_modules").is_dir():
             return
         if Path(f"{path}/package.json").is_file():
-            subprocess.run(["npm", "install"], cwd=str(path))
+            try:
+                subprocess.run(["npm", "install"], cwd=str(path), check=True)
+            except subprocess.CalledProcessError as e:
+                logger.warning(f"npm install failed: {e}")
🧰 Tools
🪛 Ruff (0.15.5)

[warning] 19-19: Unused method argument: files

(ARG002)


[error] 23-23: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/analyzers/typescript/analyzer.py` around lines 19 - 23, The
add_dependencies function currently ignores the files parameter, calls npm
without check=True so failures are silent, and uses the bare "npm" executable
which is a security risk; update add_dependencies to (1) rename or use the files
parameter (or change it to _files) to satisfy linting, (2) resolve the full npm
path via shutil.which and raise/log a clear error if not found, and (3) call
subprocess.run with check=True (and optionally capture_output=True) inside a
try/except to handle and surface installation failures (log or re-raise with
context), referencing the add_dependencies method and the path argument to
locate where to implement these changes.


def get_entity_label(self, node: Node) -> str:
if node.type == 'class_declaration':
return "Class"
elif node.type == 'interface_declaration':
return "Interface"
elif node.type == 'function_declaration':
return "Function"
elif node.type == 'method_definition':
return "Method"
raise ValueError(f"Unknown entity type: {node.type}")

def get_entity_name(self, node: Node) -> str:
if node.type in ['class_declaration', 'interface_declaration', 'function_declaration', 'method_definition']:
name_node = node.child_by_field_name('name')
if name_node:
return name_node.text.decode('utf-8')
return ''
raise ValueError(f"Unknown entity type: {node.type}")

def get_entity_docstring(self, node: Node) -> Optional[str]:
if node.type in ['class_declaration', 'interface_declaration', 'function_declaration', 'method_definition']:
sibling = node.prev_sibling
if sibling and sibling.type == 'comment':
return sibling.text.decode('utf-8')
return None
raise ValueError(f"Unknown entity type: {node.type}")

def get_entity_types(self) -> list[str]:
return ['class_declaration', 'interface_declaration', 'function_declaration', 'method_definition']

def add_symbols(self, entity: Entity) -> None:
if entity.node.type == 'class_declaration':
heritage = self._captures("(class_heritage (extends_clause value: (_) @base_class))", entity.node)
if 'base_class' in heritage:
for base in heritage['base_class']:
entity.add_symbol("base_class", base)
implements = self._captures("(class_heritage (implements_clause (type_list (_) @interface)))", entity.node)
if 'interface' in implements:
for iface in implements['interface']:
entity.add_symbol("implement_interface", iface)
elif entity.node.type == 'interface_declaration':
extends = self._captures("(interface_declaration (extends_clause (type_list (_) @type)))?", entity.node)
if 'type' in extends:
for t in extends['type']:
entity.add_symbol("extend_interface", t)
elif entity.node.type in ['function_declaration', 'method_definition']:
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)
params = self._captures("(formal_parameters (required_parameter type: (_) @parameter))", entity.node)
if 'parameter' in params:
for param in params['parameter']:
entity.add_symbol("parameters", param)
return_type = entity.node.child_by_field_name('return_type')
if return_type:
entity.add_symbol("return_type", return_type)

def is_dependency(self, file_path: str) -> bool:
return "node_modules" in file_path

def resolve_path(self, file_path: str, path: Path) -> str:
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', 'interface_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 = []
if node.type == 'call_expression':
func_node = node.child_by_field_name('function')
if func_node and func_node.type == 'member_expression':
func_node = func_node.child_by_field_name('property')
if func_node:
node = func_node
for file, resolved_node in self.resolve(files, lsp, file_path, path, node):
method_dec = self.find_parent(resolved_node, ['function_declaration', 'method_definition', 'class_declaration', 'interface_declaration'])
if method_dec and method_dec.type in ['class_declaration', 'interface_declaration']:
continue
if method_dec in file.entities:
res.append(file.entities[method_dec])
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", "extend_interface", "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}")
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies = [
"tree-sitter-python>=0.25.0,<0.26.0",
"tree-sitter-java>=0.23.5,<0.24.0",
"tree-sitter-c-sharp>=0.23.1,<0.24.0",
"tree-sitter-typescript>=0.23.2,<0.24.0",
"flask>=3.1.0,<4.0.0",
"python-dotenv>=1.0.1,<2.0.0",
"multilspy @ git+https://github.com/AviAvni/multilspy.git@python-init-params",
Expand Down