uml-gen 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. uml_gen-1.0.0/LICENSE.txt +44 -0
  2. uml_gen-1.0.0/PKG-INFO +11 -0
  3. uml_gen-1.0.0/pyproject.toml +32 -0
  4. uml_gen-1.0.0/setup.cfg +4 -0
  5. uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/PKG-INFO +11 -0
  6. uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/SOURCES.txt +29 -0
  7. uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/dependency_links.txt +1 -0
  8. uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/entry_points.txt +5 -0
  9. uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/requires.txt +3 -0
  10. uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/top_level.txt +1 -0
  11. uml_gen-1.0.0/uml/.gen/umlgen_pkg/__init__.py +1 -0
  12. uml_gen-1.0.0/uml/.gen/umlgen_pkg/frontends/__init__.py +1 -0
  13. uml_gen-1.0.0/uml/.gen/umlgen_pkg/frontends/base.py +32 -0
  14. uml_gen-1.0.0/uml/.gen/umlgen_pkg/frontends/registry.py +44 -0
  15. uml_gen-1.0.0/uml/.gen/umlgen_pkg/ir_models.py +69 -0
  16. uml_gen-1.0.0/uml/.gen/umlgen_pkg/java_sequence_index.py +346 -0
  17. uml_gen-1.0.0/uml/.gen/umlgen_pkg/java_tree_sitter_frontend.py +167 -0
  18. uml_gen-1.0.0/uml/.gen/umlgen_pkg/java_tree_sitter_ir.py +258 -0
  19. uml_gen-1.0.0/uml/.gen/umlgen_pkg/java_tree_sitter_support.py +141 -0
  20. uml_gen-1.0.0/uml/.gen/umlgen_pkg/mark.txt +11 -0
  21. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlc.py +578 -0
  22. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlc_gen.py +782 -0
  23. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_cli.py +24 -0
  24. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_file_header.py +83 -0
  25. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_legend.py +88 -0
  26. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_matched.py +47 -0
  27. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_rule_match.py +116 -0
  28. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_yaml.py +607 -0
  29. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umls.py +617 -0
  30. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umls_gen.py +581 -0
  31. uml_gen-1.0.0/uml/.gen/umlgen_pkg/umls_hierarchy.py +306 -0
@@ -0,0 +1,44 @@
1
+ UML Tool License Agreement (Non-Commercial Use)
2
+
3
+ Copyright (c) 2013 petercai
4
+
5
+ 1. Grant of License
6
+
7
+ Permission is hereby granted to any individual or organization to use, copy,
8
+ modify, and distribute this software for personal, educational, or other
9
+ non-commercial purposes, free of charge.
10
+
11
+ 2. Commercial Use Restriction
12
+
13
+ Commercial use of this software is strictly prohibited without a valid
14
+ commercial license obtained from the author.
15
+
16
+ Commercial use includes, but is not limited to:
17
+
18
+ - Use within a for-profit organization
19
+ - Use in a production environment
20
+ - Offering the software as a service (SaaS)
21
+ - Integration into a paid product or service
22
+ - Internal business usage
23
+
24
+ 3. Commercial License
25
+
26
+ Organizations or individuals wishing to use this software for commercial
27
+ purposes must obtain a separate commercial license.
28
+
29
+ For commercial licensing inquiries, please contact:
30
+ petercaica@hotmail.com
31
+
32
+ 4. Redistribution
33
+
34
+ Redistribution of the software is permitted for non-commercial purposes only,
35
+ provided that this license is included.
36
+
37
+ 5. No Warranty
38
+
39
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40
+ IMPLIED.
41
+
42
+ 6. Termination
43
+
44
+ Violation of this license will result in automatic termination of rights.
uml_gen-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: uml-gen
3
+ Version: 1.0.0
4
+ Summary: Generate UML diagrams from source code.
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE.txt
8
+ Requires-Dist: pyyaml>=6.0.3
9
+ Requires-Dist: tree-sitter>=0.25.2
10
+ Requires-Dist: tree-sitter-java>=0.23.5
11
+ Dynamic: license-file
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "uml-gen"
3
+ version = "1.0.0"
4
+ description = "Generate UML diagrams from source code."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "pyyaml>=6.0.3",
9
+ "tree-sitter>=0.25.2",
10
+ "tree-sitter-java>=0.23.5",
11
+ ]
12
+
13
+ [project.scripts]
14
+ umlc = "umlgen_pkg.umlc:main"
15
+ umls = "umlgen_pkg.umls:main"
16
+ umlc-gen = "umlgen_pkg.umlc_gen:main"
17
+ umls-gen = "umlgen_pkg.umls_gen:main"
18
+
19
+ [build-system]
20
+ requires = ["setuptools>=64"]
21
+ build-backend = "setuptools.build_meta"
22
+
23
+ # Map package roots to uml/.gen.
24
+ [tool.setuptools.package-dir]
25
+ "" = "uml/.gen"
26
+
27
+ # Auto-discover packages (umlgen_pkg and umlgen_pkg.frontends).
28
+ [tool.setuptools.packages.find]
29
+ where = ["uml/.gen"]
30
+
31
+ [tool.setuptools.package-data]
32
+ umlgen_pkg = ["mark.txt"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: uml-gen
3
+ Version: 1.0.0
4
+ Summary: Generate UML diagrams from source code.
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE.txt
8
+ Requires-Dist: pyyaml>=6.0.3
9
+ Requires-Dist: tree-sitter>=0.25.2
10
+ Requires-Dist: tree-sitter-java>=0.23.5
11
+ Dynamic: license-file
@@ -0,0 +1,29 @@
1
+ LICENSE.txt
2
+ pyproject.toml
3
+ uml/.gen/uml_gen.egg-info/PKG-INFO
4
+ uml/.gen/uml_gen.egg-info/SOURCES.txt
5
+ uml/.gen/uml_gen.egg-info/dependency_links.txt
6
+ uml/.gen/uml_gen.egg-info/entry_points.txt
7
+ uml/.gen/uml_gen.egg-info/requires.txt
8
+ uml/.gen/uml_gen.egg-info/top_level.txt
9
+ uml/.gen/umlgen_pkg/__init__.py
10
+ uml/.gen/umlgen_pkg/ir_models.py
11
+ uml/.gen/umlgen_pkg/java_sequence_index.py
12
+ uml/.gen/umlgen_pkg/java_tree_sitter_frontend.py
13
+ uml/.gen/umlgen_pkg/java_tree_sitter_ir.py
14
+ uml/.gen/umlgen_pkg/java_tree_sitter_support.py
15
+ uml/.gen/umlgen_pkg/mark.txt
16
+ uml/.gen/umlgen_pkg/umlc.py
17
+ uml/.gen/umlgen_pkg/umlc_gen.py
18
+ uml/.gen/umlgen_pkg/umlgen_cli.py
19
+ uml/.gen/umlgen_pkg/umlgen_file_header.py
20
+ uml/.gen/umlgen_pkg/umlgen_legend.py
21
+ uml/.gen/umlgen_pkg/umlgen_matched.py
22
+ uml/.gen/umlgen_pkg/umlgen_rule_match.py
23
+ uml/.gen/umlgen_pkg/umlgen_yaml.py
24
+ uml/.gen/umlgen_pkg/umls.py
25
+ uml/.gen/umlgen_pkg/umls_gen.py
26
+ uml/.gen/umlgen_pkg/umls_hierarchy.py
27
+ uml/.gen/umlgen_pkg/frontends/__init__.py
28
+ uml/.gen/umlgen_pkg/frontends/base.py
29
+ uml/.gen/umlgen_pkg/frontends/registry.py
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ umlc = umlgen_pkg.umlc:main
3
+ umlc-gen = umlgen_pkg.umlc_gen:main
4
+ umls = umlgen_pkg.umls:main
5
+ umls-gen = umlgen_pkg.umls_gen:main
@@ -0,0 +1,3 @@
1
+ pyyaml>=6.0.3
2
+ tree-sitter>=0.25.2
3
+ tree-sitter-java>=0.23.5
@@ -0,0 +1 @@
1
+ umlgen_pkg
@@ -0,0 +1 @@
1
+ """uml-gen package."""
@@ -0,0 +1 @@
1
+ """Frontend contracts and registry for umlgen language parsers."""
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ """Plugin contracts for umlgen frontends."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any, Protocol, runtime_checkable
9
+
10
+
11
+ @runtime_checkable
12
+ class JavaClassIndexer(Protocol):
13
+ """Contract for Java class-diagram source indexers."""
14
+
15
+ def __call__(self, workspace: Path, src_root: Path) -> tuple[dict[str, Any], dict[str, list[Any]]]:
16
+ """Return (by_fqcn, by_simple_name) indices."""
17
+
18
+
19
+ @runtime_checkable
20
+ class JavaSequenceIndexer(Protocol):
21
+ """Contract for Java sequence-diagram source indexers."""
22
+
23
+ def __call__(self, workspace: Path, src_root: Path) -> Any:
24
+ """Return a sequence index object compatible with umls_gen consumers."""
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class FrontendSelection:
29
+ """Selected frontend pair for class/sequence generation."""
30
+
31
+ class_indexer: JavaClassIndexer | None
32
+ sequence_indexer: JavaSequenceIndexer | None
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ """Frontend registry for parser selection and reserved parser placeholders."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from umlgen_pkg.frontends.base import FrontendSelection
7
+
8
+ try:
9
+ from umlgen_pkg.java_tree_sitter_frontend import (
10
+ index_source_tree as tree_sitter_sequence_indexer,
11
+ index_workspace_types as tree_sitter_class_indexer,
12
+ )
13
+ except ImportError: # pragma: no cover - optional dependency at runtime
14
+ tree_sitter_sequence_indexer = None
15
+ tree_sitter_class_indexer = None
16
+
17
+
18
+ def _reserved_error(parser_name: str) -> ValueError:
19
+ return ValueError(
20
+ f"config.runtime.parser={parser_name} is reserved for a future bridge frontend and is not implemented yet"
21
+ )
22
+
23
+
24
+ def resolve_java_frontend(parser_name: str) -> FrontendSelection:
25
+ parser = (parser_name or "legacy").strip()
26
+
27
+ if parser == "legacy":
28
+ return FrontendSelection(
29
+ class_indexer=None,
30
+ sequence_indexer=None,
31
+ )
32
+
33
+ if parser == "tree-sitter":
34
+ if tree_sitter_class_indexer is None or tree_sitter_sequence_indexer is None:
35
+ raise ValueError("tree-sitter parser selected but dependencies are not installed")
36
+ return FrontendSelection(
37
+ class_indexer=tree_sitter_class_indexer,
38
+ sequence_indexer=tree_sitter_sequence_indexer,
39
+ )
40
+
41
+ if parser in {"spoon", "ts-morph"}:
42
+ raise _reserved_error(parser)
43
+
44
+ raise ValueError(f"Unsupported parser: {parser}")
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """Shared intermediate representation models for umlgen frontends."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class SourceRef:
11
+ """Stable source location reference."""
12
+
13
+ relpath: str
14
+ line: int
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class MethodCallIR:
19
+ """Call-site level IR for sequence generation."""
20
+
21
+ method_name: str
22
+ qualifier: str | None
23
+ source: SourceRef
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FieldIR:
28
+ """Field/member IR."""
29
+
30
+ name: str
31
+ visibility: str
32
+ source: SourceRef
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class MethodIR:
37
+ """Method/constructor IR."""
38
+
39
+ name: str
40
+ visibility: str
41
+ source: SourceRef
42
+ return_type_names: tuple[str, ...] = ()
43
+ parameter_type_names: tuple[str, ...] = ()
44
+ calls: tuple[MethodCallIR, ...] = ()
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class TypeIR:
49
+ """Type-level IR shared by class and sequence frontends."""
50
+
51
+ language: str
52
+ kind: str
53
+ name: str
54
+ package: str
55
+ fqcn: str
56
+ source: SourceRef
57
+ extends_types: tuple[str, ...] = ()
58
+ implements_types: tuple[str, ...] = ()
59
+ dependency_types: tuple[str, ...] = ()
60
+ fields: tuple[FieldIR, ...] = ()
61
+ methods: tuple[MethodIR, ...] = ()
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class SourceIndexIR:
66
+ """Workspace-scoped IR index returned by language frontends."""
67
+
68
+ language: str
69
+ types: tuple[TypeIR, ...] = ()
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env python3
2
+ """Minimal Java source index for sequence call-chain extraction."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ import re
8
+ from pathlib import Path
9
+
10
+ PACKAGE_RE = re.compile(r"^\s*package\s+([A-Za-z_$][\w$.]*)\s*;")
11
+ TYPE_RE = re.compile(
12
+ r"^\s*(?:public|protected|private)?\s*(?:abstract\s+|final\s+)?"
13
+ r"(class|interface|enum|record)\s+([A-Za-z_$][\w$]*)\b"
14
+ )
15
+ METHOD_RE = re.compile(
16
+ r"^\s*(?:public|protected|private)?\s*(?:static\s+|final\s+|abstract\s+|synchronized\s+|default\s+)*"
17
+ r"(?:[A-Za-z_$][\w$<>\[\],.?\s]+\s+)?([A-Za-z_$][\w$]*)\s*\(([^)]*)\)"
18
+ )
19
+ FIELD_RE = re.compile(
20
+ r"^\s*(?:public|protected|private)?\s*(?:static\s+)?(?:final\s+)?"
21
+ r"([A-Za-z_$][\w$<>\[\],.?]*)\s+([a-zA-Z_$][\w$]*)\s*(?:=|;|,)"
22
+ )
23
+ LOCAL_VAR_RE = re.compile(
24
+ r"\b([A-Za-z_$][\w$<>\[\],.?]*)\s+([a-zA-Z_$][\w$]*)\s*(?:=|;|,)"
25
+ )
26
+ QUALIFIED_CALL_RE = re.compile(r"\b([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(([^)]*)\)")
27
+ SIMPLE_CALL_RE = re.compile(r"(?<!\.)\b([A-Za-z_$][\w$]*)\s*\(([^)]*)\)")
28
+
29
+ KEYWORDS = {
30
+ "if",
31
+ "for",
32
+ "while",
33
+ "switch",
34
+ "catch",
35
+ "return",
36
+ "throw",
37
+ "new",
38
+ "super",
39
+ "this",
40
+ "try",
41
+ }
42
+
43
+
44
+ @dataclass
45
+ class MethodCall:
46
+ method_name: str
47
+ qualifier: str | None
48
+ call_line: int
49
+
50
+
51
+ @dataclass
52
+ class MethodDef:
53
+ type_fqcn: str
54
+ type_short_name: str
55
+ relpath: str
56
+ method_name: str
57
+ decl_line: int
58
+ calls: list[MethodCall] = field(default_factory=list)
59
+
60
+
61
+ @dataclass
62
+ class TypeDef:
63
+ fqcn: str
64
+ short_name: str
65
+ relpath: str
66
+ decl_line: int
67
+ extends_types: tuple[str, ...] = ()
68
+ implements_types: tuple[str, ...] = ()
69
+
70
+
71
+ @dataclass
72
+ class SequenceIndex:
73
+ types_by_fqcn: dict[str, TypeDef]
74
+ methods_by_key: dict[tuple[str, str], MethodDef]
75
+ methods_by_short_type: dict[str, list[MethodDef]]
76
+
77
+
78
+ def _short_type(type_expr: str) -> str:
79
+ text = re.sub(r"<[^<>]*>", "", type_expr or "").replace("[]", "").strip()
80
+ if not text:
81
+ return ""
82
+ return text.split(".")[-1]
83
+
84
+
85
+ def _find_package(lines: list[str]) -> str:
86
+ for line in lines:
87
+ matched = PACKAGE_RE.match(line)
88
+ if matched:
89
+ return matched.group(1)
90
+ return ""
91
+
92
+
93
+ def _normalize_declared_type_name(raw: str) -> str:
94
+ text = re.sub(r"<[^<>]*>", "", (raw or "")).replace("[]", "").strip()
95
+ if not text:
96
+ return ""
97
+ return text.split(".")[-1]
98
+
99
+
100
+ def _parse_declared_types(raw: str) -> tuple[str, ...]:
101
+ if not raw:
102
+ return ()
103
+ values: list[str] = []
104
+ for part in raw.split(","):
105
+ name = _normalize_declared_type_name(part)
106
+ if name and name not in values:
107
+ values.append(name)
108
+ return tuple(values)
109
+
110
+
111
+ def _extract_parent_types_from_signature(signature: str) -> tuple[tuple[str, ...], tuple[str, ...]]:
112
+ text = re.sub(r"\s+", " ", signature)
113
+ extends_types: tuple[str, ...] = ()
114
+ implements_types: tuple[str, ...] = ()
115
+
116
+ # interface Foo extends A, B {
117
+ interface_match = re.search(r"\binterface\b\s+[A-Za-z_$][\w$]*\s+extends\s+([^\{]+)", text)
118
+ if interface_match:
119
+ extends_types = _parse_declared_types(interface_match.group(1))
120
+
121
+ # class Foo extends A implements B, C {
122
+ extends_match = re.search(r"\bclass\b\s+[A-Za-z_$][\w$]*\s+extends\s+([^\{\s]+(?:\s*<[^{}>]*>)?)", text)
123
+ if extends_match:
124
+ extends_types = _parse_declared_types(extends_match.group(1))
125
+
126
+ implements_match = re.search(r"\b(?:class|record)\b\s+[A-Za-z_$][\w$]*(?:\s+extends\s+[^\{]+?)?\s+implements\s+([^\{]+)", text)
127
+ if implements_match:
128
+ implements_types = _parse_declared_types(implements_match.group(1))
129
+
130
+ return extends_types, implements_types
131
+
132
+
133
+ def _type_block_end(lines: list[str], start_line_index: int) -> int:
134
+ depth = 0
135
+ opened = False
136
+ for i in range(start_line_index, len(lines)):
137
+ for ch in lines[i]:
138
+ if ch == "{":
139
+ depth += 1
140
+ opened = True
141
+ elif ch == "}" and opened:
142
+ depth -= 1
143
+ if opened and depth == 0:
144
+ return i
145
+ return len(lines) - 1
146
+
147
+
148
+ def _collect_fields(lines: list[str], start: int, end: int) -> dict[str, str]:
149
+ depth = 0
150
+ fields: dict[str, str] = {}
151
+
152
+ for i in range(start, end + 1):
153
+ line = lines[i]
154
+ current_depth = depth
155
+ if current_depth == 1:
156
+ match = FIELD_RE.match(line.strip())
157
+ if match:
158
+ fields[match.group(2)] = _short_type(match.group(1))
159
+
160
+ for ch in line:
161
+ if ch == "{":
162
+ depth += 1
163
+ elif ch == "}" and depth > 0:
164
+ depth -= 1
165
+
166
+ return fields
167
+
168
+
169
+ def _parse_method_calls(
170
+ *,
171
+ lines: list[str],
172
+ method_start: int,
173
+ method_end: int,
174
+ method_name: str,
175
+ fields: dict[str, str],
176
+ ) -> list[MethodCall]:
177
+ calls: list[MethodCall] = []
178
+ local_types: dict[str, str] = {}
179
+
180
+ for i in range(method_start, method_end + 1):
181
+ raw = lines[i]
182
+ text = raw.strip()
183
+ if text.startswith("//"):
184
+ continue
185
+
186
+ local_decl = LOCAL_VAR_RE.search(text)
187
+ if local_decl:
188
+ local_types[local_decl.group(2)] = _short_type(local_decl.group(1))
189
+
190
+ for match in QUALIFIED_CALL_RE.finditer(text):
191
+ qualifier = match.group(1)
192
+ called = match.group(2)
193
+ if called in KEYWORDS:
194
+ continue
195
+ if called == method_name:
196
+ # recursive call is still useful
197
+ pass
198
+
199
+ resolved_qualifier = qualifier
200
+ if qualifier in local_types:
201
+ resolved_qualifier = local_types[qualifier]
202
+ elif qualifier in fields:
203
+ resolved_qualifier = fields[qualifier]
204
+
205
+ calls.append(MethodCall(method_name=called, qualifier=resolved_qualifier, call_line=i + 1))
206
+
207
+ for match in SIMPLE_CALL_RE.finditer(text):
208
+ called = match.group(1)
209
+ if called in KEYWORDS:
210
+ continue
211
+ if f".{called}(" in text:
212
+ continue
213
+ calls.append(MethodCall(method_name=called, qualifier=None, call_line=i + 1))
214
+
215
+ return calls
216
+
217
+
218
+ def _parse_methods(
219
+ *,
220
+ lines: list[str],
221
+ type_fqcn: str,
222
+ type_short_name: str,
223
+ relpath: str,
224
+ start: int,
225
+ end: int,
226
+ fields: dict[str, str],
227
+ ) -> list[MethodDef]:
228
+ methods: list[MethodDef] = []
229
+ depth = 0
230
+ i = start
231
+
232
+ while i <= end:
233
+ line = lines[i]
234
+ current_depth = depth
235
+ text = line.strip()
236
+
237
+ if current_depth == 1 and "(" in text and not text.startswith(("if ", "for ", "while ", "switch ", "catch ")):
238
+ signature = text
239
+ sig_end = i
240
+ while ")" not in signature and sig_end + 1 <= end:
241
+ sig_end += 1
242
+ signature += " " + lines[sig_end].strip()
243
+
244
+ method_match = METHOD_RE.match(signature)
245
+ if method_match:
246
+ method_name = method_match.group(1)
247
+ body_start = sig_end
248
+ while body_start <= end and "{" not in lines[body_start]:
249
+ body_start += 1
250
+ if body_start <= end:
251
+ body_depth = 0
252
+ body_end = body_start
253
+ for j in range(body_start, end + 1):
254
+ for ch in lines[j]:
255
+ if ch == "{":
256
+ body_depth += 1
257
+ elif ch == "}" and body_depth > 0:
258
+ body_depth -= 1
259
+ if body_depth == 0 and j > body_start:
260
+ body_end = j
261
+ break
262
+
263
+ methods.append(
264
+ MethodDef(
265
+ type_fqcn=type_fqcn,
266
+ type_short_name=type_short_name,
267
+ relpath=relpath,
268
+ method_name=method_name,
269
+ decl_line=i + 1,
270
+ calls=_parse_method_calls(
271
+ lines=lines,
272
+ method_start=body_start,
273
+ method_end=body_end,
274
+ method_name=method_name,
275
+ fields=fields,
276
+ ),
277
+ )
278
+ )
279
+ i = body_end
280
+
281
+ for ch in line:
282
+ if ch == "{":
283
+ depth += 1
284
+ elif ch == "}" and depth > 0:
285
+ depth -= 1
286
+
287
+ i += 1
288
+
289
+ return methods
290
+
291
+
292
+ def index_source_tree(workspace: Path, src_root: Path) -> SequenceIndex:
293
+ java_files = sorted(p for p in src_root.rglob("*.java") if p.is_file())
294
+
295
+ types_by_fqcn: dict[str, TypeDef] = {}
296
+ methods_by_key: dict[tuple[str, str], MethodDef] = {}
297
+ methods_by_short_type: dict[str, list[MethodDef]] = {}
298
+
299
+ for java_file in java_files:
300
+ lines = java_file.read_text(encoding="utf-8", errors="ignore").splitlines()
301
+ package = _find_package(lines)
302
+ relpath = java_file.relative_to(workspace).as_posix()
303
+
304
+ for i, line in enumerate(lines):
305
+ type_match = TYPE_RE.match(line)
306
+ if not type_match:
307
+ continue
308
+
309
+ signature = line.strip()
310
+ sig_end = i
311
+ while "{" not in signature and sig_end + 1 < len(lines):
312
+ sig_end += 1
313
+ signature += " " + lines[sig_end].strip()
314
+ extends_types, implements_types = _extract_parent_types_from_signature(signature)
315
+
316
+ type_short_name = type_match.group(2)
317
+ type_fqcn = f"{package}.{type_short_name}" if package else type_short_name
318
+ type_end = _type_block_end(lines, i)
319
+ types_by_fqcn[type_fqcn] = TypeDef(
320
+ fqcn=type_fqcn,
321
+ short_name=type_short_name,
322
+ relpath=relpath,
323
+ decl_line=i + 1,
324
+ extends_types=extends_types,
325
+ implements_types=implements_types,
326
+ )
327
+
328
+ fields = _collect_fields(lines, i, type_end)
329
+ methods = _parse_methods(
330
+ lines=lines,
331
+ type_fqcn=type_fqcn,
332
+ type_short_name=type_short_name,
333
+ relpath=relpath,
334
+ start=i,
335
+ end=type_end,
336
+ fields=fields,
337
+ )
338
+ for method in methods:
339
+ methods_by_key[(method.type_fqcn, method.method_name)] = method
340
+ methods_by_short_type.setdefault(method.type_short_name, []).append(method)
341
+
342
+ return SequenceIndex(
343
+ types_by_fqcn=types_by_fqcn,
344
+ methods_by_key=methods_by_key,
345
+ methods_by_short_type=methods_by_short_type,
346
+ )