Skip to content

Commit b9e318c

Browse files
committed
Add TypeScript/Rust Uses/UsedBy extraction and source link features
- Added extract_ts_function_calls and extract_rust_function_calls for cross-references - Added source_file/source_line fields to all dataclasses - Updated TypeScript parser with function call extraction and source info - Updated Rust parser with workspace_root and source tracking - Fixed fallback dataclasses in rust_parser.py
1 parent 097f2e5 commit b9e318c

File tree

2 files changed

+171
-8
lines changed

2 files changed

+171
-8
lines changed

praisonai_tools/docs_generator/generator.py

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class ModuleInfo:
9494
constants: List[Tuple[str, str]] = field(default_factory=list)
9595
is_init: bool = False
9696
package: str = "python"
97+
source_file: str = "" # For GitHub source links
9798

9899
@property
99100
def display_name(self) -> str:
@@ -812,6 +813,89 @@ def extract_function_calls(node) -> List[str]:
812813
return visitor.get_unique_calls()
813814

814815

816+
def extract_ts_function_calls(source_code: str) -> List[str]:
817+
"""Extract function calls from TypeScript source code using regex.
818+
819+
Args:
820+
source_code: TypeScript function body source code
821+
822+
Returns:
823+
List of unique function names called
824+
"""
825+
# Match function calls: functionName( or obj.method(
826+
call_pattern = r'\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\('
827+
method_pattern = r'\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\('
828+
829+
calls = re.findall(call_pattern, source_code)
830+
methods = re.findall(method_pattern, source_code)
831+
832+
# Combine and filter
833+
excluded = {
834+
'if', 'for', 'while', 'switch', 'catch', 'function', 'class', 'new',
835+
'return', 'throw', 'typeof', 'instanceof', 'constructor', 'async', 'await',
836+
'console', 'log', 'error', 'warn', 'info', 'debug',
837+
'push', 'pop', 'shift', 'unshift', 'slice', 'splice', 'map', 'filter',
838+
'reduce', 'forEach', 'find', 'some', 'every', 'includes', 'indexOf',
839+
'toString', 'valueOf', 'hasOwnProperty', 'length', 'split', 'join',
840+
'trim', 'toLowerCase', 'toUpperCase', 'replace', 'match', 'test',
841+
'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'String', 'Number', 'Boolean',
842+
'Array', 'Object', 'Promise', 'Date', 'Math', 'JSON', 'Error',
843+
}
844+
845+
seen = set()
846+
result = []
847+
for call in calls + methods:
848+
if call not in excluded and not call.startswith('_') and call not in seen:
849+
seen.add(call)
850+
result.append(call)
851+
852+
return result[:10]
853+
854+
855+
def extract_rust_function_calls(source_code: str) -> List[str]:
856+
"""Extract function calls from Rust source code using regex.
857+
858+
Args:
859+
source_code: Rust function body source code
860+
861+
Returns:
862+
List of unique function names called
863+
"""
864+
# Match function calls: function_name( or Type::method(
865+
call_pattern = r'\b([a-z_][a-z0-9_]*)\s*\('
866+
method_pattern = r'\.([a-z_][a-z0-9_]*)\s*\('
867+
type_method_pattern = r'([A-Z][a-zA-Z0-9_]*)::([a-z_][a-z0-9_]*)\s*\('
868+
869+
calls = re.findall(call_pattern, source_code)
870+
methods = re.findall(method_pattern, source_code)
871+
type_methods = re.findall(type_method_pattern, source_code)
872+
873+
# Combine type methods as Type::method
874+
type_method_calls = [f"{t}::{m}" for t, m in type_methods]
875+
876+
# Filter out common Rust keywords and built-ins
877+
excluded = {
878+
'if', 'for', 'while', 'match', 'loop', 'fn', 'let', 'mut', 'const',
879+
'return', 'break', 'continue', 'impl', 'struct', 'enum', 'trait',
880+
'pub', 'mod', 'use', 'crate', 'self', 'super', 'async', 'await',
881+
'println', 'print', 'eprintln', 'eprint', 'format', 'panic', 'assert',
882+
'vec', 'Box', 'Rc', 'Arc', 'RefCell', 'Cell', 'Option', 'Result',
883+
'Ok', 'Err', 'Some', 'None', 'unwrap', 'expect', 'map', 'and_then',
884+
'or_else', 'clone', 'into', 'from', 'default', 'new', 'push', 'pop',
885+
'len', 'is_empty', 'iter', 'collect', 'to_string', 'as_str',
886+
}
887+
888+
seen = set()
889+
result = []
890+
for call in calls + methods + type_method_calls:
891+
call_name = call.split("::")[-1] if "::" in call else call
892+
if call_name not in excluded and not call_name.startswith('_') and call not in seen:
893+
seen.add(call)
894+
result.append(call)
895+
896+
return result[:10]
897+
898+
815899
# =============================================================================
816900
# CONFIGURATION
817901
# =============================================================================
@@ -1325,14 +1409,17 @@ def parse_module(self, module_name: str) -> Optional[ModuleInfo]:
13251409
try:
13261410
content = index_file.read_text()
13271411
module_short_name = module_name
1412+
# Get relative source path for GitHub links
1413+
source_file = str(index_file.relative_to(self.source_path.parent))
13281414
info = ModuleInfo(
13291415
name=f"praisonai.{module_name}",
13301416
short_name=module_short_name,
13311417
docstring=self._extract_module_doc(content),
13321418
package="typescript",
1419+
source_file=source_file,
13331420
)
1334-
info.classes = self._parse_classes(content)
1335-
info.functions = self._parse_functions(content)
1421+
info.classes = self._parse_classes(content, source_file)
1422+
info.functions = self._parse_functions(content, source_file)
13361423
return info
13371424
except Exception:
13381425
return None
@@ -1346,33 +1433,66 @@ def _extract_module_doc(self, content: str) -> str:
13461433
return doc.strip()
13471434
return ""
13481435

1349-
def _parse_classes(self, content: str) -> List[ClassInfo]:
1436+
def _parse_classes(self, content: str, source_file: str = "") -> List[ClassInfo]:
13501437
classes = []
13511438
pattern = r'export\s+(?:class|interface)\s+(\w+)(?:\s+extends\s+(\w+))?'
13521439
for match in re.finditer(pattern, content):
13531440
name = match.group(1)
13541441
base = match.group(2)
1442+
# Find line number
1443+
line_no = content[:match.start()].count('\n') + 1
1444+
# Extract class body for function calls
1445+
class_body = self._extract_block(content, match.end())
13551446
classes.append(ClassInfo(
13561447
name=name,
13571448
bases=[base] if base else [],
13581449
docstring=f"TypeScript {name} class",
1450+
source_file=source_file,
1451+
source_line=line_no,
13591452
))
13601453
return classes
13611454

1362-
def _parse_functions(self, content: str) -> List[FunctionInfo]:
1455+
def _parse_functions(self, content: str, source_file: str = "") -> List[FunctionInfo]:
13631456
functions = []
13641457
pattern = r'export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{]+))?'
13651458
for match in re.finditer(pattern, content):
13661459
name = match.group(1)
13671460
params_str = match.group(2)
13681461
return_type = match.group(3) or "void"
1462+
# Find line number
1463+
line_no = content[:match.start()].count('\n') + 1
1464+
# Extract function body for calls extraction
1465+
func_body = self._extract_block(content, match.end())
1466+
calls = extract_ts_function_calls(func_body) if func_body else []
13691467
functions.append(FunctionInfo(
13701468
name=name,
13711469
signature=params_str.strip(),
13721470
return_type=return_type.strip(),
13731471
is_async='async' in match.group(0),
1472+
source_file=source_file,
1473+
source_line=line_no,
1474+
calls=calls,
13741475
))
13751476
return functions
1477+
1478+
def _extract_block(self, content: str, start_pos: int) -> str:
1479+
"""Extract a code block (between { and }) from start position."""
1480+
# Find opening brace
1481+
brace_start = content.find('{', start_pos)
1482+
if brace_start == -1:
1483+
return ""
1484+
1485+
# Count braces to find matching close
1486+
depth = 1
1487+
pos = brace_start + 1
1488+
while pos < len(content) and depth > 0:
1489+
if content[pos] == '{':
1490+
depth += 1
1491+
elif content[pos] == '}':
1492+
depth -= 1
1493+
pos += 1
1494+
1495+
return content[brace_start:pos] if depth == 0 else ""
13761496

13771497

13781498
# =============================================================================

praisonai_tools/docs_generator/rust_parser.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
ModuleInfo,
2323
SKIP_MODULES,
2424
SKIP_METHODS,
25+
extract_rust_function_calls,
2526
)
2627
except ImportError:
2728
# Define minimal fallback classes when run standalone
@@ -57,6 +58,8 @@ class ClassInfo:
5758
properties: List[ParamInfo] = field(default_factory=list)
5859
init_params: List[ParamInfo] = field(default_factory=list)
5960
examples: List[str] = field(default_factory=list)
61+
source_file: str = ""
62+
source_line: int = 0
6063

6164
@dataclass
6265
class FunctionInfo:
@@ -67,6 +70,9 @@ class FunctionInfo:
6770
params: List[ParamInfo] = field(default_factory=list)
6871
is_async: bool = False
6972
examples: List[str] = field(default_factory=list)
73+
source_file: str = ""
74+
source_line: int = 0
75+
calls: List[str] = field(default_factory=list)
7076

7177
@dataclass
7278
class ModuleInfo:
@@ -79,6 +85,11 @@ class ModuleInfo:
7985
classes: List[ClassInfo] = field(default_factory=list)
8086
functions: List[FunctionInfo] = field(default_factory=list)
8187
constants: List[Tuple[str, str]] = field(default_factory=list)
88+
source_file: str = ""
89+
90+
def extract_rust_function_calls(source_code: str) -> List[str]:
91+
"""Fallback when run standalone - return empty list."""
92+
return []
8293

8394
SKIP_MODULES = {"tests", "__pycache__", "conftest"}
8495
SKIP_METHODS = {"__init__", "__repr__", "__str__"}
@@ -103,6 +114,8 @@ class TraitInfo:
103114
docstring: str = ""
104115
methods: List[MethodInfo] = field(default_factory=list)
105116
supertraits: List[str] = field(default_factory=list)
117+
source_file: str = ""
118+
source_line: int = 0
106119

107120

108121
@dataclass
@@ -111,6 +124,8 @@ class EnumInfo:
111124
name: str
112125
docstring: str = ""
113126
variants: List[Tuple[str, str]] = field(default_factory=list) # (name, docstring)
127+
source_file: str = ""
128+
source_line: int = 0
114129

115130

116131
@dataclass
@@ -151,6 +166,7 @@ def __init__(self, crate_path: Path, crate_name: str = "praisonai"):
151166
self.crate_path = crate_path
152167
self.crate_name = crate_name
153168
self.src_path = crate_path / "src"
169+
self.workspace_root = crate_path.parent # Parent of crate for relative paths
154170

155171
# Cache for parsed files
156172
self._file_cache: Dict[Path, str] = {}
@@ -217,16 +233,23 @@ def parse_module(self, module_path: str) -> Optional[ModuleInfo]:
217233
parts = module_path.split(".")
218234
short_name = parts[-1] if len(parts) > 1 else module_path
219235

236+
# Get relative source path for GitHub links
237+
try:
238+
source_file = str(file_path.relative_to(self.workspace_root))
239+
except ValueError:
240+
source_file = str(file_path)
241+
220242
info = ModuleInfo(
221243
name=module_path,
222244
short_name=short_name,
223245
docstring=module_doc,
224246
is_init=file_path.name in ("lib.rs", "mod.rs"),
225247
package="rust",
248+
source_file=source_file,
226249
)
227250

228251
# Parse structs as classes
229-
info.classes = self._parse_structs(content)
252+
info.classes = self._parse_structs(content, source_file)
230253

231254
# Parse traits (also as classes with special handling)
232255
traits = self._parse_traits(content)
@@ -236,6 +259,7 @@ def parse_module(self, module_path: str) -> Optional[ModuleInfo]:
236259
docstring=trait.docstring,
237260
bases=trait.supertraits,
238261
methods=trait.methods,
262+
source_file=source_file,
239263
))
240264

241265
# Parse enums
@@ -248,10 +272,11 @@ def parse_module(self, module_path: str) -> Optional[ModuleInfo]:
248272
ParamInfo(name=v[0], description=v[1], type="variant")
249273
for v in enum.variants
250274
],
275+
source_file=source_file,
251276
))
252277

253278
# Parse standalone functions
254-
info.functions = self._parse_functions(content)
279+
info.functions = self._parse_functions(content, source_file)
255280

256281
# Parse impl blocks and attach methods to structs
257282
self._parse_impl_blocks(content, info)
@@ -353,7 +378,7 @@ def _extract_doc_comments(self, content: str, start_pos: int) -> str:
353378

354379
return "\n".join(lines)
355380

356-
def _parse_structs(self, content: str) -> List[ClassInfo]:
381+
def _parse_structs(self, content: str, source_file: str = "") -> List[ClassInfo]:
357382
"""Parse struct definitions."""
358383
structs = []
359384

@@ -364,6 +389,9 @@ def _parse_structs(self, content: str) -> List[ClassInfo]:
364389
name = match.group(1)
365390
fields_str = match.group(2) or ""
366391

392+
# Calculate line number
393+
line_no = content[:match.start()].count('\n') + 1
394+
367395
# Extract doc comments
368396
doc = self._extract_doc_comments(content, match.start())
369397

@@ -392,6 +420,8 @@ def _parse_structs(self, content: str) -> List[ClassInfo]:
392420
docstring=self._clean_docstring(doc),
393421
properties=properties,
394422
examples=examples,
423+
source_file=source_file,
424+
source_line=line_no,
395425
))
396426

397427
return structs
@@ -489,7 +519,7 @@ def _parse_enums(self, content: str) -> List[EnumInfo]:
489519

490520
return enums
491521

492-
def _parse_functions(self, content: str) -> List[FunctionInfo]:
522+
def _parse_functions(self, content: str, source_file: str = "") -> List[FunctionInfo]:
493523
"""Parse standalone public functions."""
494524
functions = []
495525

@@ -510,6 +540,9 @@ def _parse_functions(self, content: str) -> List[FunctionInfo]:
510540
if impl_count > close_brace_count:
511541
continue
512542

543+
# Calculate line number
544+
line_no = content[:match.start()].count('\n') + 1
545+
513546
# Extract doc comments
514547
doc = self._extract_doc_comments(content, match.start())
515548

@@ -519,6 +552,13 @@ def _parse_functions(self, content: str) -> List[FunctionInfo]:
519552
# Check if async
520553
is_async = "async" in match.group(0)
521554

555+
# Extract function body for calls
556+
func_body = self._extract_brace_content(content, match.end() - 1)
557+
try:
558+
calls = extract_rust_function_calls(func_body) if func_body else []
559+
except:
560+
calls = []
561+
522562
functions.append(FunctionInfo(
523563
name=name,
524564
signature=params_str,
@@ -527,6 +567,9 @@ def _parse_functions(self, content: str) -> List[FunctionInfo]:
527567
params=params,
528568
is_async=is_async,
529569
examples=self._extract_examples(doc),
570+
source_file=source_file,
571+
source_line=line_no,
572+
calls=calls,
530573
))
531574

532575
return functions

0 commit comments

Comments
 (0)