Skip to content

Commit cabf335

Browse files
committed
rebase
1 parent 2e6b1ca commit cabf335

7 files changed

Lines changed: 285 additions & 49 deletions

File tree

osa_tool/operations/codebase/docstring_generation/adapters/python_adapter.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
import tree_sitter_python as tspython
24
from tree_sitter import Parser, Language
35
from osa_tool.operations.codebase.docstring_generation.adapters.base import LanguageAdapter
@@ -68,7 +70,75 @@ def get_parameters(self, node, sv):
6870
return params
6971

7072
def extract_imports(self, root, sv, cwd):
71-
return {}
73+
import_map = {}
74+
for node in root.children:
75+
if node.type in ("import_statement", "import_from_statement"):
76+
import_map.update(self._resolve_import_path(sv.text(node), cwd))
77+
78+
return import_map
79+
80+
@staticmethod
81+
def _resolve_import_path(import_text, cwd):
82+
import_mapping = {}
83+
text = import_text.strip()
84+
85+
if text.startswith("from"):
86+
try:
87+
from_part, import_part = text.split("import", 1)
88+
except ValueError:
89+
return import_mapping
90+
91+
module_name = from_part.replace("from", "").strip()
92+
module_path = os.path.join(cwd, *module_name.split(".")) + ".py"
93+
if not os.path.exists(module_path):
94+
return import_mapping
95+
96+
for entity in (e.strip() for e in import_part.split(",")):
97+
if " as " in entity:
98+
imported_name, alias_name = (e.strip() for e in entity.split(" as ", 1))
99+
else:
100+
imported_name = alias_name = entity
101+
import_mapping[alias_name] = {
102+
"module": module_name,
103+
"class": imported_name,
104+
"path": module_path,
105+
}
106+
107+
elif text.startswith("import"):
108+
parts = text.replace("import", "").strip().split()
109+
if not parts:
110+
return import_mapping
111+
if "as" in parts:
112+
module_name = parts[0]
113+
alias_name = parts[parts.index("as") + 1]
114+
else:
115+
module_name = alias_name = parts[0]
116+
117+
module_path = os.path.join(cwd, *module_name.split(".")) + ".py"
118+
if os.path.exists(module_path):
119+
import_mapping[alias_name] = {"module": module_name, "path": module_path}
120+
121+
return import_mapping
72122

73123
def resolve_method_calls(self, node, sv):
74-
return []
124+
block = next((c for c in node.children if c.type == "block"), None)
125+
if not block:
126+
return []
127+
128+
calls = set()
129+
130+
def walk(n):
131+
if n.type == "function_definition" and n is not node:
132+
return
133+
if n.type == "call":
134+
target = n.child_by_field_name("function")
135+
if target:
136+
text = sv.text(target).strip()
137+
if text:
138+
calls.add(text)
139+
for c in n.children:
140+
walk(c)
141+
142+
walk(block)
143+
144+
return sorted(calls)

osa_tool/operations/codebase/docstring_generation/adapters/typescript_adapter.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class TypeScriptAdapter(LanguageAdapter):
99

10-
EXTENSIONS = (".ts", ".tsx")
10+
EXTENSIONS = (".ts",)
1111

1212
def build_parser(self):
1313
return Parser(Language(tstypescript.language_typescript()))
@@ -25,8 +25,18 @@ def is_function(self, node):
2525

2626
def get_name(self, node, sv):
2727
n = node.child_by_field_name("name")
28+
if n:
29+
return sv.text(n)
2830

29-
return sv.text(n) if n else "anonymous"
31+
# arrow functions / function expressions have no name field;
32+
# the name usually sits on the enclosing declarator (const foo = () => ...)
33+
parent = node.parent
34+
if parent and parent.type == "variable_declarator":
35+
declared = parent.child_by_field_name("name")
36+
if declared:
37+
return sv.text(declared)
38+
39+
return "anonymous"
3040

3141
def _get_doc_owner(self, node):
3242
parent = node.parent
@@ -100,6 +110,10 @@ def get_parameters(self, node, sv):
100110
pnode = node.child_by_field_name("parameters")
101111

102112
if not pnode:
113+
# single paren-less arrow parameter, e.g. `x => x * 2`
114+
single = node.child_by_field_name("parameter")
115+
if single:
116+
params.append(sv.text(single))
103117
return params
104118

105119
for c in pnode.children:
@@ -117,3 +131,11 @@ def extract_imports(self, root, sv, cwd):
117131

118132
def resolve_method_calls(self, node, sv):
119133
return []
134+
135+
136+
class TSXAdapter(TypeScriptAdapter):
137+
138+
EXTENSIONS = (".tsx",)
139+
140+
def build_parser(self):
141+
return Parser(Language(tstypescript.language_tsx()))

osa_tool/operations/codebase/docstring_generation/core/osa_parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from osa_tool.operations.codebase.docstring_generation.adapters.python_adapter import PythonAdapter
77
from osa_tool.operations.codebase.docstring_generation.adapters.javascript_adapter import JavaScriptAdapter
8-
from osa_tool.operations.codebase.docstring_generation.adapters.typescript_adapter import TypeScriptAdapter
8+
from osa_tool.operations.codebase.docstring_generation.adapters.typescript_adapter import TypeScriptAdapter, TSXAdapter
99

1010

1111
class OSA_TreeSitter:
@@ -14,6 +14,7 @@ class OSA_TreeSitter:
1414
PythonAdapter(),
1515
JavaScriptAdapter(),
1616
TypeScriptAdapter(),
17+
TSXAdapter(),
1718
]
1819

1920
def __init__(self, scripts_path: str, ignore_list: list[str] = None, target_files: list[str] = None):

osa_tool/operations/codebase/docstring_generation/docgen.py

Lines changed: 96 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -174,26 +174,49 @@ def count_tokens(self, prompt: str) -> int:
174174
tokens = enc.encode(prompt)
175175
return len(tokens)
176176

177-
async def generate_class_documentation(self, class_details: list, semaphore: asyncio.Semaphore) -> str:
177+
@staticmethod
178+
def _lang_of(file_path: str) -> str:
179+
"""Detect the documentation language from a file extension."""
180+
p = str(file_path)
181+
if p.endswith((".ts", ".tsx")):
182+
return "typescript"
183+
if p.endswith((".js", ".jsx")):
184+
return "javascript"
185+
return "python"
186+
187+
async def generate_class_documentation(
188+
self, class_details: list, semaphore: asyncio.Semaphore, language: str = "python"
189+
) -> str:
178190
"""
179191
Generate documentation for a class.
180192
181193
Args:
182194
class_details: A list of dictionaries containing method names and their docstrings.
183195
semaphore: synchronous primitive that implements limitation of concurrency degree to avoid overloading api.
196+
language: Source language of the class ("python", "javascript" or "typescript").
184197
Returns:
185198
The generated class docstring.
186199
"""
187200
# Construct a structured prompt
188-
prompt = (
189-
f"""Generate a single Python docstring for the following class {class_details[0]}. The docstring should follow Google-style format and include:\n"""
190-
"- Respond strictly in English.\n"
191-
"- A short summary of what the class does.\n"
192-
"- A list of its methods without details if class has them otherwise do not mention a list of methods.\n"
193-
"- A list of its attributes that explicitly mentioned at the constructor method's docstring (can be adressed as attributes, properties, class fields, etc.), without types if class or constructor method has them otherwise do not mention a list of attributes.\n"
194-
"- A brief summary of what its methods and attributes do if one has them for.\n\n"
195-
"Return only docstring without any quotation."
196-
)
201+
if language in ("javascript", "typescript"):
202+
prompt = (
203+
f"""Generate a JSDoc comment for the following JavaScript/TypeScript class {class_details[0]}. Include:\n"""
204+
"- Respond strictly in English.\n"
205+
"- A short summary of what the class does.\n"
206+
"- Keep it concise; do NOT list methods/attributes as separate sections.\n"
207+
"- Do NOT use Python conventions: no `self`, no `__init__`, no `Args:`/`Returns:`/`Attributes:` sections.\n\n"
208+
"Return only the comment text without any quotation."
209+
)
210+
else:
211+
prompt = (
212+
f"""Generate a single Python docstring for the following class {class_details[0]}. The docstring should follow Google-style format and include:\n"""
213+
"- Respond strictly in English.\n"
214+
"- A short summary of what the class does.\n"
215+
"- A list of its methods without details if class has them otherwise do not mention a list of methods.\n"
216+
"- A list of its attributes that explicitly mentioned at the constructor method's docstring (can be adressed as attributes, properties, class fields, etc.), without types if class or constructor method has them otherwise do not mention a list of attributes.\n"
217+
"- A brief summary of what its methods and attributes do if one has them for.\n\n"
218+
"Return only docstring without any quotation."
219+
)
197220

198221
if len(class_details[1]) > 0:
199222
prompt += f"\nClass Attributes:\n"
@@ -209,7 +232,9 @@ async def generate_class_documentation(self, class_details: list, semaphore: asy
209232
docstring = await self.model_handler.async_request(prompt)
210233
return docstring.strip('"""')
211234

212-
async def update_class_documentation(self, class_details: list, semaphore: asyncio.Semaphore) -> str:
235+
async def update_class_documentation(
236+
self, class_details: list, semaphore: asyncio.Semaphore, language: str = "python"
237+
) -> str:
213238
"""
214239
Generate documentation for a class.
215240
@@ -227,8 +252,9 @@ async def update_class_documentation(self, class_details: list, semaphore: async
227252
return class_details[-1].strip().strip('"').strip("'")
228253

229254
old_desc = desc.strip('"\n ')
255+
lang_word = "JavaScript/TypeScript" if language in ("javascript", "typescript") else "Python"
230256
prompt = (
231-
f"""Update the provided description for the following Python class {class_details[0]} using provided main idea of the project.\n"""
257+
f"""Update the provided description for the following {lang_word} class {class_details[0]} using provided main idea of the project.\n"""
232258
"""Do not pay too much attention to the provided main idea - try not to mention it explicitly.\n"""
233259
f"""The main idea: {self.main_idea}\n"""
234260
f"""Old docstring description part: {old_desc}\n\n"""
@@ -246,21 +272,34 @@ async def generate_method_documentation(
246272
method_details: dict,
247273
semaphore: asyncio.Semaphore,
248274
context_code: str = None,
275+
language: str = "python",
249276
) -> str:
250277
"""
251278
Generate documentation for a single method.
252279
"""
253280
arguments = [a for a in method_details["arguments"] if a not in ("self", "cls")]
281+
if language in ("javascript", "typescript"):
282+
intro = (
283+
"Generate a JSDoc comment for the following JavaScript/TypeScript function. Use JSDoc tags and include:\n"
284+
"- Respond strictly in English.\n"
285+
"- A short summary of what the function does.\n"
286+
"- A `@param {type} name` line for each parameter (infer the type from the code; use `*` if unknown).\n"
287+
"- A `@returns {type}` line describing the return value (omit it if the function returns nothing).\n"
288+
"- Do NOT use Python conventions: no `self`, no `__init__`, no `Args:`/`Returns:` sections.\n\n"
289+
)
290+
else:
291+
intro = (
292+
"Generate a Python docstring for the following method. The docstring should follow Google-style format and include:\n"
293+
"- Respond strictly in English.\n"
294+
"- A short summary of what the method does.\n"
295+
"- A description of its parameters without types.\n"
296+
"- If the method is a class constructor, explicitly list all class fields (object properties) that are initialized, "
297+
"including their names and purposes. These fields should match the attributes assigned within the constructor "
298+
"(e.g., this.field = ..., self.field = ...). This information will be used to generate the class-level documentation.\n"
299+
"- The return type and description (omit Returns section if the method does not return a value).\n\n"
300+
)
254301
prompt = (
255-
"Generate a Python docstring for the following method. The docstring should follow Google-style format and include:\n"
256-
"- A short summary of what the method does.\n"
257-
"- A description of its parameters without types.\n"
258-
"- Respond strictly in English.\n"
259-
"- If the method is a class constructor, explicitly list all class fields (object properties) that are initialized, "
260-
"including their names and purposes. These fields should match the attributes assigned within the constructor "
261-
"(e.g., this.field = ..., self.field = ...). This information will be used to generate the class-level documentation.\n"
262-
"- The return type and description (omit Returns section if the method does not return a value).\n\n"
263-
f"- Method Name: {method_details['method_name']}\n\n"
302+
intro + f"- Method Name: {method_details['method_name']}\n\n"
264303
"Method source code: You are given only the body of a single method, without its signature. "
265304
"All visible code, including any inner functions or nested logic, belongs to this single method. "
266305
"Do NOT write separate docstrings for inner functions — they are part of the main method's logic.\n"
@@ -304,23 +343,38 @@ async def update_method_documentation(
304343
semaphore: asyncio.Semaphore,
305344
context_code: str = None,
306345
class_name: str = None,
346+
language: str = "python",
307347
) -> str:
308348
"""
309349
Update documentation for a single method.
310350
"""
311351
docstring = method_details["docstring"]
312352

353+
if language in ("javascript", "typescript"):
354+
guidelines = (
355+
"Update the provided JSDoc comment for the following JavaScript/TypeScript function.\n"
356+
"Preserve correct existing information and add missing details based on the source code.\n\n"
357+
"Guidelines:\n"
358+
"- Improve clarity and completeness without rewriting everything from scratch.\n"
359+
"- Use JSDoc tags: `@param {type} name` for each parameter, `@returns {type}` for the return value (omit if nothing is returned).\n"
360+
"- Do NOT use Python conventions: no `self`, no `__init__`, no `Args:`/`Returns:` sections.\n"
361+
"- Do NOT invent parameters or behavior.\n\n"
362+
)
363+
else:
364+
guidelines = (
365+
"Update the provided docstring for the following Python method.\n"
366+
"Preserve correct existing information and add missing details based on the source code.\n\n"
367+
"Guidelines:\n"
368+
"- Improve clarity and completeness without rewriting everything from scratch.\n"
369+
"- Be specific and clear about the method's purpose and possible usages in a system based on a field-specific main idea if it can be vague for non-participant compliances.\n"
370+
"- If the original docstring contains only a description, add Args and Returns sections if needed.\n"
371+
"- Describe parameters without types.\n"
372+
"- Omit Returns section if the method does not return a value.\n"
373+
"- Do NOT invent parameters or behavior.\n\n"
374+
)
375+
313376
prompt = (
314-
"Update the provided docstring for the following Python method.\n"
315-
"Preserve correct existing information and add missing details based on the source code.\n\n"
316-
"Guidelines:\n"
317-
"- Improve clarity and completeness without rewriting everything from scratch.\n"
318-
"- Be specific and clear about the method's purpose and possible usages in a system based on a field-specific main idea if it can be vague for non-participant compliances.\n"
319-
"- If the original docstring contains only a description, add Args and Returns sections if needed.\n"
320-
"- Describe parameters without types.\n"
321-
"- Omit Returns section if the method does not return a value.\n"
322-
"- Do NOT invent parameters or behavior.\n\n"
323-
f"Original docstring:\n{docstring}\n\n"
377+
guidelines + f"Original docstring:\n{docstring}\n\n"
324378
"Method Details:\n"
325379
f"- Method Name: {method_details['method_name']}"
326380
f"{f' (located inside {class_name} class)' if class_name else ''}\n"
@@ -895,16 +949,19 @@ async def _generate_node(
895949
)
896950

897951
context = self.context_extractor(metadata, parsed_structure, function_index, generated_docstrings)
952+
language = self._lang_of(file_path)
898953

899954
try:
900955
if self.main_idea:
901956
if node_type == "method":
902957
class_name = node_info.get("class", "")
903-
docstring = await self.update_method_documentation(metadata, semaphore, context, class_name)
958+
docstring = await self.update_method_documentation(
959+
metadata, semaphore, context, class_name, language
960+
)
904961
else:
905-
docstring = await self.update_method_documentation(metadata, semaphore, context)
962+
docstring = await self.update_method_documentation(metadata, semaphore, context, language=language)
906963
else:
907-
docstring = await self.generate_method_documentation(metadata, semaphore, context)
964+
docstring = await self.generate_method_documentation(metadata, semaphore, context, language=language)
908965

909966
return (node_id, node_type, file_path, docstring, metadata) if docstring else None
910967

@@ -1097,10 +1154,11 @@ async def _fetch_docstrings_for_class(
10971154
f"""{progress_label} Requesting for docstrings {"update" if self.main_idea else "generation"} for the class: {item["name"]} at {file}"""
10981155
)
10991156

1157+
class_language = self._lang_of(file)
11001158
request_coroutine = (
1101-
self.generate_class_documentation(class_metadata, semaphore)
1159+
self.generate_class_documentation(class_metadata, semaphore, class_language)
11021160
if not self.main_idea
1103-
else self.update_class_documentation(class_metadata, semaphore)
1161+
else self.update_class_documentation(class_metadata, semaphore, class_language)
11041162
)
11051163

11061164
# just add new coroutine and class name to a task list
@@ -1148,14 +1206,12 @@ async def generate_the_main_idea(self, parsed_structure: dict, top_n: int = 5) -
11481206
else:
11491207
docstring = component["details"]["docstring"] if component["details"]["docstring"] else ""
11501208

1151-
prompt_structure.append(
1152-
f"""
1209+
prompt_structure.append(f"""
11531210
{_type.capitalize()} name: {component["name"] if _type == "class" else component["details"]["method_name"]}
11541211
Component description: {docstring}
11551212
Component place in hierarchy: {file}
11561213
Component importance score: {score}
1157-
"""
1158-
)
1214+
""")
11591215

11601216
logger.info(f"Generating the main idea of the project...")
11611217

osa_tool/operations/codebase/docstring_generation/insert/factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def create(file_path: str):
99
if file_path.endswith(".py"):
1010
return PythonAugmentor()
1111

12-
if file_path.endswith((".ts", ".js")):
12+
if file_path.endswith((".ts", ".tsx", ".js", ".jsx")):
1313
return TSJSAugmentor()
1414

1515
raise ValueError(f"Unsupported file type: {file_path}")

0 commit comments

Comments
 (0)