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.
- uml_gen-1.0.0/LICENSE.txt +44 -0
- uml_gen-1.0.0/PKG-INFO +11 -0
- uml_gen-1.0.0/pyproject.toml +32 -0
- uml_gen-1.0.0/setup.cfg +4 -0
- uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/PKG-INFO +11 -0
- uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/SOURCES.txt +29 -0
- uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/dependency_links.txt +1 -0
- uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/entry_points.txt +5 -0
- uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/requires.txt +3 -0
- uml_gen-1.0.0/uml/.gen/uml_gen.egg-info/top_level.txt +1 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/__init__.py +1 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/frontends/__init__.py +1 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/frontends/base.py +32 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/frontends/registry.py +44 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/ir_models.py +69 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/java_sequence_index.py +346 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/java_tree_sitter_frontend.py +167 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/java_tree_sitter_ir.py +258 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/java_tree_sitter_support.py +141 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/mark.txt +11 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlc.py +578 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlc_gen.py +782 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_cli.py +24 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_file_header.py +83 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_legend.py +88 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_matched.py +47 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_rule_match.py +116 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umlgen_yaml.py +607 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umls.py +617 -0
- uml_gen-1.0.0/uml/.gen/umlgen_pkg/umls_gen.py +581 -0
- 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"]
|
uml_gen-1.0.0/setup.cfg
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,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 @@
|
|
|
1
|
+
|
|
@@ -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
|
+
)
|