aja-codeintel 0.1.0__py3-none-any.whl
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.
- aja_codeintel-0.1.0.dist-info/METADATA +436 -0
- aja_codeintel-0.1.0.dist-info/RECORD +68 -0
- aja_codeintel-0.1.0.dist-info/WHEEL +5 -0
- aja_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
- aja_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
- aja_codeintel-0.1.0.dist-info/top_level.txt +1 -0
- codeintel_cli/__init__.py +1 -0
- codeintel_cli/__main__.py +4 -0
- codeintel_cli/cli.py +41 -0
- codeintel_cli/commands/__init__.py +1 -0
- codeintel_cli/commands/graph/__init__.py +18 -0
- codeintel_cli/commands/graph/deps_cmd.py +35 -0
- codeintel_cli/commands/graph/related_cmd.py +121 -0
- codeintel_cli/commands/graph/relsymbols_cmd.py +347 -0
- codeintel_cli/commands/graph/reverse_related_cmd.py +54 -0
- codeintel_cli/commands/nav/__init__.py +12 -0
- codeintel_cli/commands/nav/copy_cmd.py +101 -0
- codeintel_cli/commands/nav/open_cmd.py +18 -0
- codeintel_cli/commands/nav/where_cmd.py +21 -0
- codeintel_cli/commands/project/__init__.py +26 -0
- codeintel_cli/commands/project/context_cmd.py +326 -0
- codeintel_cli/commands/project/folder_cmd.py +51 -0
- codeintel_cli/commands/project/imports_cmd.py +90 -0
- codeintel_cli/commands/project/models_cmd.py +98 -0
- codeintel_cli/commands/project/modeltree_cmd.py +476 -0
- codeintel_cli/commands/project/new.py +0 -0
- codeintel_cli/commands/project/resolve_cmd.py +29 -0
- codeintel_cli/commands/project/scan_cmd.py +51 -0
- codeintel_cli/commands/project/servicemap_cmd.py +180 -0
- codeintel_cli/commands/project/tree_cmd.py +203 -0
- codeintel_cli/commands/project/version_cmd.py +14 -0
- codeintel_cli/context/java_context.py +180 -0
- codeintel_cli/context/java_rel.py +299 -0
- codeintel_cli/context/java_service.py +291 -0
- codeintel_cli/context/python_context.py +91 -0
- codeintel_cli/context/python_rel.py +251 -0
- codeintel_cli/context/python_service.py +205 -0
- codeintel_cli/core/fuzzy.py +72 -0
- codeintel_cli/core/opener.py +37 -0
- codeintel_cli/core/project.py +34 -0
- codeintel_cli/core/resolve_folder.py +68 -0
- codeintel_cli/core/resolve_model_target.py +92 -0
- codeintel_cli/core/resolve_target.py +53 -0
- codeintel_cli/core/timing.py +13 -0
- codeintel_cli/core/where.py +77 -0
- codeintel_cli/db/__init__.py +7 -0
- codeintel_cli/db/cache.py +224 -0
- codeintel_cli/db/operations.py +333 -0
- codeintel_cli/db/schema.py +102 -0
- codeintel_cli/errors.py +78 -0
- codeintel_cli/graph/__init__.py +1 -0
- codeintel_cli/graph/builder.py +149 -0
- codeintel_cli/graph/query.py +30 -0
- codeintel_cli/graph/traverse.py +49 -0
- codeintel_cli/lang/__init__.py +0 -0
- codeintel_cli/lang/java/__init__.py +0 -0
- codeintel_cli/lang/java/engine.py +18 -0
- codeintel_cli/lang/java/models.py +105 -0
- codeintel_cli/lang/java/resolve.py +49 -0
- codeintel_cli/lang/python/__init__.py +0 -0
- codeintel_cli/lang/python/engine.py +8 -0
- codeintel_cli/lang/python/models.py +86 -0
- codeintel_cli/lang/router.py +24 -0
- codeintel_cli/parser/imports.py +26 -0
- codeintel_cli/parser/resolve.py +49 -0
- codeintel_cli/parser/symbols.py +92 -0
- codeintel_cli/scanner/__init__.py +0 -0
- codeintel_cli/scanner/scanner.py +41 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from collections import deque
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_reverse_graph(graph: dict[Path, set[Path]]) -> dict[Path, set[Path]]:
|
|
8
|
+
rev: dict[Path, set[Path]] = {}
|
|
9
|
+
|
|
10
|
+
for src, targets in graph.items():
|
|
11
|
+
rev.setdefault(src, set())
|
|
12
|
+
for t in targets:
|
|
13
|
+
rev.setdefault(t, set()).add(src)
|
|
14
|
+
|
|
15
|
+
return rev
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bfs_related(
|
|
19
|
+
graph: dict[Path, set[Path]],
|
|
20
|
+
start: Path,
|
|
21
|
+
max_depth: int,
|
|
22
|
+
skip: set[Path] | None = None,
|
|
23
|
+
) -> set[Path]:
|
|
24
|
+
if max_depth <= 0:
|
|
25
|
+
return set()
|
|
26
|
+
|
|
27
|
+
skip = skip or set()
|
|
28
|
+
seen: set[Path] = set()
|
|
29
|
+
q = deque([(start, 0)])
|
|
30
|
+
|
|
31
|
+
while q:
|
|
32
|
+
node, depth = q.popleft()
|
|
33
|
+
|
|
34
|
+
expand = node not in skip
|
|
35
|
+
|
|
36
|
+
for nxt in graph.get(node, set()):
|
|
37
|
+
if nxt == start or nxt in seen:
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
next_depth = depth + 1
|
|
41
|
+
if next_depth > max_depth:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
seen.add(nxt)
|
|
45
|
+
|
|
46
|
+
if expand:
|
|
47
|
+
q.append((nxt, next_depth))
|
|
48
|
+
|
|
49
|
+
return seen
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
IMP = re.compile(r"^\s*import\s+([\w\.]+)\s*;")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def java_extract_imports(path: Path):
|
|
10
|
+
out: list[str] = []
|
|
11
|
+
try:
|
|
12
|
+
for line in path.read_text(errors="ignore").splitlines():
|
|
13
|
+
m = IMP.match(line)
|
|
14
|
+
if m:
|
|
15
|
+
out.append(m.group(1))
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
return out
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ModelField:
|
|
10
|
+
name: str
|
|
11
|
+
type: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ModelDef:
|
|
16
|
+
name: str
|
|
17
|
+
file: str
|
|
18
|
+
kind: str
|
|
19
|
+
bases: list[str]
|
|
20
|
+
fields: list[ModelField]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TYPE_RE = re.compile(r"\b(class|interface|enum|record)\s+([A-Za-z_][A-Za-z0-9_]*)\b")
|
|
24
|
+
RECORD_RE = re.compile(r"\brecord\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)")
|
|
25
|
+
FIELD_RE = re.compile(
|
|
26
|
+
r"^\s*(?:@\w+(?:\([^)]*\))?\s*)*"
|
|
27
|
+
r"(?:(?:public|protected|private)\s+)?"
|
|
28
|
+
r"(?:(?:static|final)\s+)*"
|
|
29
|
+
r"([A-Za-z0-9_<>\[\].,?]+)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:=.*)?;"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_DENY_TYPES = {"return", "throw", "new", "super", "this", "break", "continue"}
|
|
33
|
+
_DENY_NAMES = {"return", "throw", "new", "super", "this", "true", "false", "null"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _split_record_params(sig: str) -> list[tuple[str, str]]:
|
|
37
|
+
s = sig.strip()
|
|
38
|
+
if not s:
|
|
39
|
+
return []
|
|
40
|
+
parts: list[str] = []
|
|
41
|
+
cur = []
|
|
42
|
+
depth = 0
|
|
43
|
+
for ch in s:
|
|
44
|
+
if ch == "<":
|
|
45
|
+
depth += 1
|
|
46
|
+
elif ch == ">":
|
|
47
|
+
depth = max(0, depth - 1)
|
|
48
|
+
elif ch == "," and depth == 0:
|
|
49
|
+
parts.append("".join(cur).strip())
|
|
50
|
+
cur = []
|
|
51
|
+
continue
|
|
52
|
+
cur.append(ch)
|
|
53
|
+
if cur:
|
|
54
|
+
parts.append("".join(cur).strip())
|
|
55
|
+
|
|
56
|
+
out: list[tuple[str, str]] = []
|
|
57
|
+
for p in parts:
|
|
58
|
+
toks = p.split()
|
|
59
|
+
if len(toks) >= 2:
|
|
60
|
+
t = " ".join(toks[:-1]).strip()
|
|
61
|
+
n = toks[-1].strip()
|
|
62
|
+
if t and n:
|
|
63
|
+
out.append((n, t))
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def extract_java_models(path: Path, project_root: Path) -> list[ModelDef]:
|
|
68
|
+
try:
|
|
69
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
70
|
+
except Exception:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
rel = str(path.relative_to(project_root))
|
|
74
|
+
|
|
75
|
+
mrec = RECORD_RE.search(text)
|
|
76
|
+
if mrec:
|
|
77
|
+
name = mrec.group(1)
|
|
78
|
+
params = mrec.group(2)
|
|
79
|
+
fields = [ModelField(n, t) for (n, t) in _split_record_params(params)]
|
|
80
|
+
return [ModelDef(name, rel, "java", [], fields)]
|
|
81
|
+
|
|
82
|
+
mtype = TYPE_RE.search(text)
|
|
83
|
+
if not mtype:
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
name = mtype.group(2)
|
|
87
|
+
|
|
88
|
+
fields: list[ModelField] = []
|
|
89
|
+
for line in text.splitlines():
|
|
90
|
+
s = line.strip()
|
|
91
|
+
if not s or s.startswith("return ") or s.startswith("throw "):
|
|
92
|
+
continue
|
|
93
|
+
fm = FIELD_RE.match(line)
|
|
94
|
+
if fm:
|
|
95
|
+
ftype = fm.group(1).strip()
|
|
96
|
+
fname = fm.group(2).strip()
|
|
97
|
+
if not ftype or not fname:
|
|
98
|
+
continue
|
|
99
|
+
if ftype.lower() in _DENY_TYPES:
|
|
100
|
+
continue
|
|
101
|
+
if fname.lower() in _DENY_NAMES:
|
|
102
|
+
continue
|
|
103
|
+
fields.append(ModelField(fname, ftype))
|
|
104
|
+
|
|
105
|
+
return [ModelDef(name, rel, "java", [], fields)]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _possible_java_source_roots(project_root: Path) -> list[Path]:
|
|
7
|
+
out: list[Path] = []
|
|
8
|
+
seen: set[Path] = set()
|
|
9
|
+
|
|
10
|
+
def add(p: Path) -> None:
|
|
11
|
+
try:
|
|
12
|
+
pr = p.resolve()
|
|
13
|
+
except Exception:
|
|
14
|
+
return
|
|
15
|
+
if pr in seen:
|
|
16
|
+
return
|
|
17
|
+
if pr.exists() and pr.is_dir():
|
|
18
|
+
seen.add(pr)
|
|
19
|
+
out.append(pr)
|
|
20
|
+
|
|
21
|
+
add(project_root / "src" / "main" / "java")
|
|
22
|
+
add(project_root / "src" / "test" / "java")
|
|
23
|
+
|
|
24
|
+
for p in project_root.rglob("src"):
|
|
25
|
+
try:
|
|
26
|
+
if not p.is_dir():
|
|
27
|
+
continue
|
|
28
|
+
except Exception:
|
|
29
|
+
continue
|
|
30
|
+
add(p / "main" / "java")
|
|
31
|
+
add(p / "test" / "java")
|
|
32
|
+
|
|
33
|
+
add(project_root)
|
|
34
|
+
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_java_import_to_file(module: str, project_root: Path) -> Path | None:
|
|
39
|
+
if not module:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
rel = Path(*module.split("."))
|
|
43
|
+
|
|
44
|
+
for base in _possible_java_source_roots(project_root):
|
|
45
|
+
cand = (base / rel).with_suffix(".java")
|
|
46
|
+
if cand.exists() and cand.is_file():
|
|
47
|
+
return cand.resolve()
|
|
48
|
+
|
|
49
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import ast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ModelField:
|
|
10
|
+
name: str
|
|
11
|
+
type: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ModelDef:
|
|
16
|
+
name: str
|
|
17
|
+
file: str
|
|
18
|
+
kind: str
|
|
19
|
+
bases: list[str]
|
|
20
|
+
fields: list[ModelField]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _unparse(node: ast.AST | None) -> str:
|
|
24
|
+
if node is None:
|
|
25
|
+
return "Any"
|
|
26
|
+
try:
|
|
27
|
+
return ast.unparse(node)
|
|
28
|
+
except Exception:
|
|
29
|
+
return node.__class__.__name__
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _base_names(cls: ast.ClassDef) -> list[str]:
|
|
33
|
+
return [_unparse(b) for b in cls.bases]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _has_decorator(cls: ast.ClassDef, name: str) -> bool:
|
|
37
|
+
for d in cls.decorator_list:
|
|
38
|
+
if isinstance(d, ast.Name) and d.id == name:
|
|
39
|
+
return True
|
|
40
|
+
if isinstance(d, ast.Attribute) and d.attr == name:
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _looks_like_model(cls: ast.ClassDef) -> bool:
|
|
46
|
+
bases = _base_names(cls)
|
|
47
|
+
if _has_decorator(cls, "dataclass"):
|
|
48
|
+
return True
|
|
49
|
+
if any("BaseModel" in b for b in bases):
|
|
50
|
+
return True
|
|
51
|
+
if any(b.endswith("Base") for b in bases):
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def extract_python_models(path: Path, project_root: Path) -> list[ModelDef]:
|
|
57
|
+
try:
|
|
58
|
+
tree = ast.parse(path.read_text(encoding="utf-8", errors="ignore"))
|
|
59
|
+
except Exception:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
rel = str(path.relative_to(project_root))
|
|
63
|
+
out: list[ModelDef] = []
|
|
64
|
+
|
|
65
|
+
for node in tree.body:
|
|
66
|
+
if not isinstance(node, ast.ClassDef):
|
|
67
|
+
continue
|
|
68
|
+
if not _looks_like_model(node):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
fields: list[ModelField] = []
|
|
72
|
+
for st in node.body:
|
|
73
|
+
if isinstance(st, ast.AnnAssign) and isinstance(st.target, ast.Name):
|
|
74
|
+
fields.append(ModelField(st.target.id, _unparse(st.annotation)))
|
|
75
|
+
|
|
76
|
+
out.append(
|
|
77
|
+
ModelDef(
|
|
78
|
+
name=node.name,
|
|
79
|
+
file=rel,
|
|
80
|
+
kind="python",
|
|
81
|
+
bases=_base_names(node),
|
|
82
|
+
fields=fields,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return out
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .python.engine import py_extract_imports
|
|
6
|
+
from .java.engine import java_extract_imports
|
|
7
|
+
|
|
8
|
+
def extract_imports(path: Path, lang: str):
|
|
9
|
+
path = path.resolve()
|
|
10
|
+
if lang == "java" or path.suffix.lower() == ".java":
|
|
11
|
+
return java_extract_imports(path)
|
|
12
|
+
return py_extract_imports(path)
|
|
13
|
+
|
|
14
|
+
def extract_models_for_file(path: Path, project_root: Path):
|
|
15
|
+
path = path.resolve()
|
|
16
|
+
if path.suffix.lower() == ".py":
|
|
17
|
+
from .python.models import extract_python_models
|
|
18
|
+
return extract_python_models(path, project_root)
|
|
19
|
+
|
|
20
|
+
if path.suffix.lower() == ".java":
|
|
21
|
+
from .java.models import extract_java_models
|
|
22
|
+
return extract_java_models(path, project_root)
|
|
23
|
+
|
|
24
|
+
return []
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def extract_imports_from_code(code: str) -> list[tuple[str, int]]:
|
|
8
|
+
code = code.lstrip("\ufeff")
|
|
9
|
+
tree = ast.parse(code)
|
|
10
|
+
|
|
11
|
+
out: list[tuple[str, int]] = []
|
|
12
|
+
|
|
13
|
+
for node in ast.walk(tree):
|
|
14
|
+
if isinstance(node, ast.Import):
|
|
15
|
+
for n in node.names:
|
|
16
|
+
if n.name:
|
|
17
|
+
out.append((n.name, 0))
|
|
18
|
+
elif isinstance(node, ast.ImportFrom):
|
|
19
|
+
out.append((node.module or "", node.level))
|
|
20
|
+
|
|
21
|
+
return out
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def extract_imports_from_file(path: Path) -> list[tuple[str, int]]:
|
|
25
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
26
|
+
return extract_imports_from_code(text)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def resolve_module_to_file(module: str, root: Path) -> Path | None:
|
|
6
|
+
"""
|
|
7
|
+
Absolute import resolution (backward compatible).
|
|
8
|
+
Example: 'codeintel_cli.graph.builder' -> root/codeintel_cli/graph/builder.py
|
|
9
|
+
"""
|
|
10
|
+
if not module:
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
cand = root / (module.replace(".", "/") + ".py")
|
|
14
|
+
if cand.exists():
|
|
15
|
+
return cand.resolve()
|
|
16
|
+
|
|
17
|
+
init = root / module.replace(".", "/") / "__init__.py"
|
|
18
|
+
if init.exists():
|
|
19
|
+
return init.resolve()
|
|
20
|
+
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_import_to_file(module: str, level: int, current_file: Path, root: Path) -> Path | None:
|
|
25
|
+
"""
|
|
26
|
+
Supports absolute + relative imports.
|
|
27
|
+
- level=0: absolute import (uses resolve_module_to_file)
|
|
28
|
+
- level>0: relative import (from . / .. / ...)
|
|
29
|
+
"""
|
|
30
|
+
# absolute
|
|
31
|
+
if level == 0:
|
|
32
|
+
return resolve_module_to_file(module, root)
|
|
33
|
+
|
|
34
|
+
# relative
|
|
35
|
+
base = current_file.parent
|
|
36
|
+
for _ in range(level - 1):
|
|
37
|
+
base = base.parent
|
|
38
|
+
|
|
39
|
+
target_base = base if module == "" else base / module.replace(".", "/")
|
|
40
|
+
|
|
41
|
+
py = target_base.with_suffix(".py")
|
|
42
|
+
if py.exists():
|
|
43
|
+
return py.resolve()
|
|
44
|
+
|
|
45
|
+
init = target_base / "__init__.py"
|
|
46
|
+
if init.exists():
|
|
47
|
+
return init.resolve()
|
|
48
|
+
|
|
49
|
+
return None
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import ast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ClassInfo:
|
|
10
|
+
name: str
|
|
11
|
+
lineno: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class FuncInfo:
|
|
16
|
+
name: str
|
|
17
|
+
lineno: int
|
|
18
|
+
signature: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _format_args(a: ast.arguments) -> str:
|
|
22
|
+
parts: list[str] = []
|
|
23
|
+
|
|
24
|
+
if getattr(a, "posonlyargs", None):
|
|
25
|
+
for x in a.posonlyargs:
|
|
26
|
+
parts.append(x.arg)
|
|
27
|
+
parts.append("/")
|
|
28
|
+
|
|
29
|
+
for x in a.args:
|
|
30
|
+
parts.append(x.arg)
|
|
31
|
+
|
|
32
|
+
if a.vararg:
|
|
33
|
+
parts.append(f"*{a.vararg.arg}")
|
|
34
|
+
elif a.kwonlyargs:
|
|
35
|
+
parts.append("*")
|
|
36
|
+
|
|
37
|
+
for x in a.kwonlyargs:
|
|
38
|
+
parts.append(x.arg)
|
|
39
|
+
|
|
40
|
+
if a.kwarg:
|
|
41
|
+
parts.append(f"**{a.kwarg.arg}")
|
|
42
|
+
|
|
43
|
+
return ", ".join(parts)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_source_text(path: Path) -> str:
|
|
47
|
+
try:
|
|
48
|
+
raw = path.read_bytes()
|
|
49
|
+
except Exception:
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
text = raw.decode("utf-8-sig", errors="ignore")
|
|
53
|
+
|
|
54
|
+
if "\x00" in text:
|
|
55
|
+
text = text.replace("\x00", "")
|
|
56
|
+
if text.startswith("\ufeff"):
|
|
57
|
+
text = text.lstrip("\ufeff")
|
|
58
|
+
|
|
59
|
+
return text
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract_classes_from_file(path: Path) -> list[ClassInfo]:
|
|
63
|
+
try:
|
|
64
|
+
text = _read_source_text(path)
|
|
65
|
+
if not text.strip():
|
|
66
|
+
return []
|
|
67
|
+
tree = ast.parse(text)
|
|
68
|
+
except Exception:
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
out: list[ClassInfo] = []
|
|
72
|
+
for node in tree.body:
|
|
73
|
+
if isinstance(node, ast.ClassDef):
|
|
74
|
+
out.append(ClassInfo(node.name, getattr(node, "lineno", 1)))
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def extract_funcs_from_file(path: Path) -> list[FuncInfo]:
|
|
79
|
+
try:
|
|
80
|
+
text = _read_source_text(path)
|
|
81
|
+
if not text.strip():
|
|
82
|
+
return []
|
|
83
|
+
tree = ast.parse(text)
|
|
84
|
+
except Exception:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
out: list[FuncInfo] = []
|
|
88
|
+
for node in tree.body:
|
|
89
|
+
if isinstance(node, ast.FunctionDef):
|
|
90
|
+
sig = f"{node.name}({_format_args(node.args)})"
|
|
91
|
+
out.append(FuncInfo(node.name, getattr(node, "lineno", 1), sig))
|
|
92
|
+
return out
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..core.project import is_skipped_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_source_files(root: Path, exts: tuple[str, ...]) -> list[Path]:
|
|
9
|
+
root = root.resolve()
|
|
10
|
+
results: list[Path] = []
|
|
11
|
+
|
|
12
|
+
for ext in exts:
|
|
13
|
+
for p in root.rglob(f"*{ext}"):
|
|
14
|
+
if not p.is_file():
|
|
15
|
+
continue
|
|
16
|
+
if is_skipped_path(p):
|
|
17
|
+
continue
|
|
18
|
+
results.append(p.resolve())
|
|
19
|
+
|
|
20
|
+
return sorted(set(results))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_python_files(root: Path) -> list[Path]:
|
|
24
|
+
return find_source_files(root, (".py",))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_java_files(root: Path) -> list[Path]:
|
|
28
|
+
return find_source_files(root, (".java",))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_all_supported_files(root: Path) -> list[Path]:
|
|
32
|
+
root = root.resolve()
|
|
33
|
+
results: list[Path] = []
|
|
34
|
+
for p in root.rglob("*"):
|
|
35
|
+
if not p.is_file():
|
|
36
|
+
continue
|
|
37
|
+
if is_skipped_path(p):
|
|
38
|
+
continue
|
|
39
|
+
if p.suffix.lower() in {".py", ".java"}:
|
|
40
|
+
results.append(p.resolve())
|
|
41
|
+
return sorted(set(results))
|