aja-codeintel 0.1.0__tar.gz → 0.1.2__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.
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/PKG-INFO +1 -1
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/PKG-INFO +1 -1
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/SOURCES.txt +6 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/__init__.py +3 -2
- aja_codeintel-0.1.2/codeintel_cli/commands/project/endpoints_cmd.py +146 -0
- aja_codeintel-0.1.2/codeintel_cli/endpoints/__init__.py +1 -0
- aja_codeintel-0.1.2/codeintel_cli/endpoints/java_spring.py +153 -0
- aja_codeintel-0.1.2/codeintel_cli/endpoints/models.py +23 -0
- aja_codeintel-0.1.2/codeintel_cli/endpoints/python_web.py +99 -0
- aja_codeintel-0.1.2/codeintel_cli/endpoints/scan.py +102 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/pyproject.toml +1 -1
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/LICENSE +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/README.md +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/dependency_links.txt +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/entry_points.txt +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/requires.txt +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/aja_codeintel.egg-info/top_level.txt +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/__main__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/cli.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/deps_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/related_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/relsymbols_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/reverse_related_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/nav/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/nav/copy_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/nav/open_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/nav/where_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/context_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/folder_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/imports_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/models_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/modeltree_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/new.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/resolve_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/scan_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/servicemap_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/tree_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/version_cmd.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/java_context.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/java_rel.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/java_service.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/python_context.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/python_rel.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/context/python_service.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/fuzzy.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/opener.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/project.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/resolve_folder.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/resolve_model_target.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/resolve_target.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/timing.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/core/where.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/db/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/db/cache.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/db/operations.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/db/schema.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/errors.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/graph/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/graph/builder.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/graph/query.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/graph/traverse.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/java/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/java/engine.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/java/models.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/java/resolve.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/python/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/python/engine.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/python/models.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/lang/router.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/parser/imports.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/parser/resolve.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/parser/symbols.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/scanner/__init__.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/scanner/scanner.py +0 -0
- {aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/setup.cfg +0 -0
|
@@ -23,6 +23,7 @@ codeintel_cli/commands/nav/open_cmd.py
|
|
|
23
23
|
codeintel_cli/commands/nav/where_cmd.py
|
|
24
24
|
codeintel_cli/commands/project/__init__.py
|
|
25
25
|
codeintel_cli/commands/project/context_cmd.py
|
|
26
|
+
codeintel_cli/commands/project/endpoints_cmd.py
|
|
26
27
|
codeintel_cli/commands/project/folder_cmd.py
|
|
27
28
|
codeintel_cli/commands/project/imports_cmd.py
|
|
28
29
|
codeintel_cli/commands/project/models_cmd.py
|
|
@@ -51,6 +52,11 @@ codeintel_cli/db/__init__.py
|
|
|
51
52
|
codeintel_cli/db/cache.py
|
|
52
53
|
codeintel_cli/db/operations.py
|
|
53
54
|
codeintel_cli/db/schema.py
|
|
55
|
+
codeintel_cli/endpoints/__init__.py
|
|
56
|
+
codeintel_cli/endpoints/java_spring.py
|
|
57
|
+
codeintel_cli/endpoints/models.py
|
|
58
|
+
codeintel_cli/endpoints/python_web.py
|
|
59
|
+
codeintel_cli/endpoints/scan.py
|
|
54
60
|
codeintel_cli/graph/__init__.py
|
|
55
61
|
codeintel_cli/graph/builder.py
|
|
56
62
|
codeintel_cli/graph/query.py
|
|
@@ -11,7 +11,7 @@ from .tree_cmd import register_tree
|
|
|
11
11
|
from .modeltree_cmd import register_modeltree
|
|
12
12
|
from .models_cmd import register_models
|
|
13
13
|
from .servicemap_cmd import register_servicemap
|
|
14
|
-
|
|
14
|
+
from .endpoints_cmd import register_endpoints
|
|
15
15
|
|
|
16
16
|
def register_project_commands(app: typer.Typer) -> None:
|
|
17
17
|
register_scan(app)
|
|
@@ -23,4 +23,5 @@ def register_project_commands(app: typer.Typer) -> None:
|
|
|
23
23
|
register_tree(app)
|
|
24
24
|
register_modeltree(app)
|
|
25
25
|
register_models(app)
|
|
26
|
-
register_servicemap(app)
|
|
26
|
+
register_servicemap(app)
|
|
27
|
+
register_endpoints(app)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ...errors import InvalidPathError
|
|
11
|
+
from ...core.project import find_project_root
|
|
12
|
+
from ...endpoints.scan import EndpointScanOptions, iter_supported_source_files
|
|
13
|
+
from ...endpoints.java_spring import extract_spring_endpoints
|
|
14
|
+
from ...endpoints.python_web import extract_python_endpoints
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _validate_folder(path: str) -> Path:
|
|
18
|
+
folder = Path(path).resolve()
|
|
19
|
+
if not folder.exists() or not folder.is_dir():
|
|
20
|
+
raise InvalidPathError(message="Invalid folder", path=folder)
|
|
21
|
+
return folder
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _norm_path(p: str) -> str:
|
|
25
|
+
return p.replace("\\", "/")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _guess_access(path: str) -> str:
|
|
29
|
+
if path.startswith("/admin"):
|
|
30
|
+
return "ADMIN"
|
|
31
|
+
if path.startswith("/me"):
|
|
32
|
+
return "CUSTOMER"
|
|
33
|
+
if path.startswith("/api/auth"):
|
|
34
|
+
return "PUBLIC"
|
|
35
|
+
return "PUBLIC"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def register_endpoints(app: typer.Typer) -> None:
|
|
39
|
+
@app.command()
|
|
40
|
+
def endpoints(
|
|
41
|
+
path: str = typer.Argument(".", help="Folder to scan"),
|
|
42
|
+
lang: str = typer.Option("all", "--lang", help="all | py | java"),
|
|
43
|
+
hidden: bool = typer.Option(False, "--hidden", help="Include hidden dotfiles/folders"),
|
|
44
|
+
no_gitignore: bool = typer.Option(False, "--no-gitignore", help="Do not apply .gitignore rules"),
|
|
45
|
+
fmt: str = typer.Option("clean", "--format", help="clean | text | json"),
|
|
46
|
+
only_main: bool = typer.Option(True, "--only-main/--all-sources", help="Only scan src/main (skip tests)"),
|
|
47
|
+
only_api: bool = typer.Option(False, "--only-api", help="Only show paths starting with /api"),
|
|
48
|
+
dedupe: bool = typer.Option(True, "--dedupe/--no-dedupe", help="Remove duplicates (method+path)"),
|
|
49
|
+
):
|
|
50
|
+
folder = _validate_folder(path)
|
|
51
|
+
project_root = find_project_root(folder)
|
|
52
|
+
|
|
53
|
+
opts = EndpointScanOptions(
|
|
54
|
+
show_hidden=hidden,
|
|
55
|
+
use_gitignore=not no_gitignore,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
lang_norm = lang.strip().lower()
|
|
59
|
+
if lang_norm not in {"all", "py", "java"}:
|
|
60
|
+
raise typer.BadParameter("Invalid --lang. Use: all | py | java")
|
|
61
|
+
|
|
62
|
+
t0 = time.perf_counter()
|
|
63
|
+
all_eps = []
|
|
64
|
+
|
|
65
|
+
files = list(iter_supported_source_files(project_root, opts))
|
|
66
|
+
|
|
67
|
+
for f in files:
|
|
68
|
+
suf = f.suffix.lower()
|
|
69
|
+
if lang_norm == "py" and suf != ".py":
|
|
70
|
+
continue
|
|
71
|
+
if lang_norm == "java" and suf != ".java":
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
src = f.read_text(encoding="utf-8", errors="ignore")
|
|
76
|
+
except Exception:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if suf == ".java":
|
|
80
|
+
all_eps.extend(extract_spring_endpoints(src, str(f)))
|
|
81
|
+
elif suf == ".py":
|
|
82
|
+
all_eps.extend(extract_python_endpoints(src, str(f)))
|
|
83
|
+
|
|
84
|
+
if only_main:
|
|
85
|
+
filtered = []
|
|
86
|
+
for e in all_eps:
|
|
87
|
+
fp = _norm_path(e.file)
|
|
88
|
+
if "/src/test/" in fp:
|
|
89
|
+
continue
|
|
90
|
+
if "/src/main/" in fp or "/main/" in fp:
|
|
91
|
+
filtered.append(e)
|
|
92
|
+
all_eps = filtered
|
|
93
|
+
|
|
94
|
+
if only_api:
|
|
95
|
+
all_eps = [e for e in all_eps if e.path.startswith("/api")]
|
|
96
|
+
|
|
97
|
+
if dedupe:
|
|
98
|
+
seen = set()
|
|
99
|
+
uniq = []
|
|
100
|
+
for e in all_eps:
|
|
101
|
+
key = (e.method, e.path)
|
|
102
|
+
if key in seen:
|
|
103
|
+
continue
|
|
104
|
+
seen.add(key)
|
|
105
|
+
uniq.append(e)
|
|
106
|
+
all_eps = uniq
|
|
107
|
+
|
|
108
|
+
all_eps.sort(key=lambda e: (e.path, e.method, e.file, e.line))
|
|
109
|
+
elapsed = time.perf_counter() - t0
|
|
110
|
+
|
|
111
|
+
fmt_norm = fmt.strip().lower()
|
|
112
|
+
|
|
113
|
+
if fmt_norm == "json":
|
|
114
|
+
typer.echo(json.dumps([e.to_dict() for e in all_eps], indent=2))
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if fmt_norm == "text":
|
|
118
|
+
typer.echo(f"Project: {project_root}")
|
|
119
|
+
typer.echo(f"Found {len(all_eps)} endpoints in {elapsed:.3f}s")
|
|
120
|
+
typer.echo("")
|
|
121
|
+
for e in all_eps:
|
|
122
|
+
roles = set(e.roles or [])
|
|
123
|
+
role_str = f" {{{', '.join(sorted(roles))}}}" if roles else f" {{{_guess_access(e.path)}}}"
|
|
124
|
+
typer.echo(f"{e.method:6} {e.path:40}{role_str} {e.file}:{e.line} {e.handler} <{e.framework}>")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
typer.echo(f"Found {len(all_eps)} endpoints in {elapsed:.3f}s")
|
|
128
|
+
typer.echo("")
|
|
129
|
+
|
|
130
|
+
methods_by_path: dict[str, set[str]] = defaultdict(set)
|
|
131
|
+
roles_by_key: dict[tuple[str, str], set[str]] = defaultdict(set)
|
|
132
|
+
|
|
133
|
+
for e in all_eps:
|
|
134
|
+
methods_by_path[e.path].add(e.method)
|
|
135
|
+
if e.roles:
|
|
136
|
+
roles_by_key[(e.path, e.method)].update(e.roles)
|
|
137
|
+
|
|
138
|
+
paths = sorted(methods_by_path.keys())
|
|
139
|
+
for idx, p in enumerate(paths, start=1):
|
|
140
|
+
methods = sorted(methods_by_path[p])
|
|
141
|
+
parts = []
|
|
142
|
+
for m in methods:
|
|
143
|
+
role_set = roles_by_key.get((p, m), set())
|
|
144
|
+
role_str = f"{{{', '.join(sorted(role_set))}}}" if role_set else f"{{{_guess_access(p)}}}"
|
|
145
|
+
parts.append(f"{m}{role_str}")
|
|
146
|
+
typer.echo(f"{idx:02d}. {p} [{', '.join(parts)}]")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Tuple, Optional, Set
|
|
5
|
+
|
|
6
|
+
from .models import Endpoint
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_MAPPING_ANN = {
|
|
10
|
+
"GetMapping",
|
|
11
|
+
"PostMapping",
|
|
12
|
+
"PutMapping",
|
|
13
|
+
"DeleteMapping",
|
|
14
|
+
"PatchMapping",
|
|
15
|
+
"RequestMapping",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_ANNOT_RE = re.compile(r'@\s*([A-Za-z_]\w*)\s*(\((.*?)\))?', re.DOTALL)
|
|
19
|
+
_CLASS_RE = re.compile(r'\b(class|record)\s+([A-Za-z_]\w*)\b')
|
|
20
|
+
_METHOD_RE = re.compile(r'(public|protected|private)?\s*(static\s+)?[\w<>\[\], ?]+\s+([A-Za-z_]\w*)\s*\(')
|
|
21
|
+
|
|
22
|
+
_PREAUTHORIZE_RE = re.compile(r'@PreAuthorize\s*\(\s*"([^"]+)"\s*\)')
|
|
23
|
+
_SECURED_RE = re.compile(r'@Secured\s*\((.*?)\)')
|
|
24
|
+
_ROLESALLOWED_RE = re.compile(r'@RolesAllowed\s*\((.*?)\)')
|
|
25
|
+
|
|
26
|
+
_HASROLE_RE = re.compile(r"hasRole\s*\(\s*'([^']+)'\s*\)")
|
|
27
|
+
_HASANYROLE_RE = re.compile(r"hasAnyRole\s*\(\s*([^\)]+)\)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_paths(arg_str: str) -> List[str]:
|
|
31
|
+
return re.findall(r'"([^"]+)"', arg_str or "")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_join(base: str, sub: str) -> str:
|
|
35
|
+
if not base:
|
|
36
|
+
return sub or "/"
|
|
37
|
+
if not sub:
|
|
38
|
+
return base
|
|
39
|
+
if base.endswith("/") and sub.startswith("/"):
|
|
40
|
+
return base[:-1] + sub
|
|
41
|
+
if not base.endswith("/") and not sub.startswith("/"):
|
|
42
|
+
return base + "/" + sub
|
|
43
|
+
return base + sub
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _methods_from_annotation(ann: str, args: str) -> List[str]:
|
|
47
|
+
ann = ann.lower()
|
|
48
|
+
if ann == "getmapping":
|
|
49
|
+
return ["GET"]
|
|
50
|
+
if ann == "postmapping":
|
|
51
|
+
return ["POST"]
|
|
52
|
+
if ann == "putmapping":
|
|
53
|
+
return ["PUT"]
|
|
54
|
+
if ann == "deletemapping":
|
|
55
|
+
return ["DELETE"]
|
|
56
|
+
if ann == "patchmapping":
|
|
57
|
+
return ["PATCH"]
|
|
58
|
+
if ann == "requestmapping":
|
|
59
|
+
return re.findall(r'RequestMethod\.([A-Z]+)', args or "")
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _roles_from_expr(expr: str) -> Set[str]:
|
|
64
|
+
roles: Set[str] = set()
|
|
65
|
+
for m in _HASROLE_RE.finditer(expr):
|
|
66
|
+
roles.add(m.group(1))
|
|
67
|
+
for m in _HASANYROLE_RE.finditer(expr):
|
|
68
|
+
for r in re.findall(r"'([^']+)'", m.group(1)):
|
|
69
|
+
roles.add(r)
|
|
70
|
+
return roles
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _roles_from_strings(arg: str) -> Set[str]:
|
|
74
|
+
out: Set[str] = set()
|
|
75
|
+
for s in re.findall(r'"([^"]+)"', arg or ""):
|
|
76
|
+
if s.startswith("ROLE_"):
|
|
77
|
+
s = s[5:]
|
|
78
|
+
out.add(s)
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _extract_roles_block(text: str) -> Set[str]:
|
|
83
|
+
roles: Set[str] = set()
|
|
84
|
+
for m in _PREAUTHORIZE_RE.finditer(text):
|
|
85
|
+
roles |= _roles_from_expr(m.group(1))
|
|
86
|
+
for m in _SECURED_RE.finditer(text):
|
|
87
|
+
roles |= _roles_from_strings(m.group(1))
|
|
88
|
+
for m in _ROLESALLOWED_RE.finditer(text):
|
|
89
|
+
roles |= _roles_from_strings(m.group(1))
|
|
90
|
+
return roles
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def extract_spring_endpoints(java_source: str, file_path: str) -> List[Endpoint]:
|
|
94
|
+
endpoints: List[Endpoint] = []
|
|
95
|
+
lines = java_source.splitlines()
|
|
96
|
+
|
|
97
|
+
mcls = _CLASS_RE.search(java_source)
|
|
98
|
+
if not mcls:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
class_pos = mcls.start()
|
|
102
|
+
controller = mcls.group(2)
|
|
103
|
+
pre = java_source[:class_pos]
|
|
104
|
+
|
|
105
|
+
if "@RestController" not in pre and "@Controller" not in pre:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
base_path = ""
|
|
109
|
+
for m in _ANNOT_RE.finditer(pre):
|
|
110
|
+
if m.group(1) == "RequestMapping":
|
|
111
|
+
ps = _extract_paths(m.group(3) or "")
|
|
112
|
+
if ps:
|
|
113
|
+
base_path = ps[0]
|
|
114
|
+
|
|
115
|
+
class_roles = _extract_roles_block(pre)
|
|
116
|
+
|
|
117
|
+
pending: List[Tuple[str, str, int]] = []
|
|
118
|
+
|
|
119
|
+
for i, line in enumerate(lines, start=1):
|
|
120
|
+
s = line.strip()
|
|
121
|
+
|
|
122
|
+
if s.startswith("@"):
|
|
123
|
+
am = _ANNOT_RE.search(s)
|
|
124
|
+
if am and am.group(1) in _MAPPING_ANN:
|
|
125
|
+
pending.append((am.group(1), am.group(3) or "", i))
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
mm = _METHOD_RE.search(s)
|
|
129
|
+
if mm and pending:
|
|
130
|
+
handler = mm.group(3)
|
|
131
|
+
method_roles = _extract_roles_block("\n".join(lines[max(0, i - 8):i]))
|
|
132
|
+
roles = class_roles | method_roles
|
|
133
|
+
|
|
134
|
+
for ann, args, ln in pending:
|
|
135
|
+
for meth in _methods_from_annotation(ann, args):
|
|
136
|
+
paths = _extract_paths(args) or [""]
|
|
137
|
+
for p in paths:
|
|
138
|
+
full = _normalize_join(base_path, p)
|
|
139
|
+
endpoints.append(
|
|
140
|
+
Endpoint(
|
|
141
|
+
method=meth,
|
|
142
|
+
path=full,
|
|
143
|
+
file=file_path,
|
|
144
|
+
line=ln,
|
|
145
|
+
handler=handler,
|
|
146
|
+
framework="spring",
|
|
147
|
+
controller=controller,
|
|
148
|
+
roles=roles or None,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
pending = []
|
|
152
|
+
|
|
153
|
+
return endpoints
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, asdict
|
|
4
|
+
from typing import Optional, Dict, Any, Set
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Endpoint:
|
|
9
|
+
method: str
|
|
10
|
+
path: str
|
|
11
|
+
file: str
|
|
12
|
+
line: int
|
|
13
|
+
handler: str
|
|
14
|
+
framework: str
|
|
15
|
+
controller: Optional[str] = None
|
|
16
|
+
roles: Optional[Set[str]] = None # e.g. {"ADMIN","CUSTOMER"}
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
19
|
+
d = asdict(self)
|
|
20
|
+
|
|
21
|
+
if self.roles is not None:
|
|
22
|
+
d["roles"] = sorted(self.roles)
|
|
23
|
+
return d
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from .models import Endpoint
|
|
7
|
+
|
|
8
|
+
_FASTAPI_METHODS = {"get", "post", "put", "delete", "patch", "options", "head"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_str(node: ast.AST) -> Optional[str]:
|
|
12
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
13
|
+
return node.value
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _call_name(expr: ast.AST) -> Tuple[Optional[str], Optional[str]]:
|
|
18
|
+
if isinstance(expr, ast.Call):
|
|
19
|
+
func = expr.func
|
|
20
|
+
if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
|
|
21
|
+
return func.value.id, func.attr
|
|
22
|
+
return None, None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _extract_flask_methods(dec_call: ast.Call) -> List[str]:
|
|
26
|
+
for kw in dec_call.keywords:
|
|
27
|
+
if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
|
|
28
|
+
ms: List[str] = []
|
|
29
|
+
for el in kw.value.elts:
|
|
30
|
+
s = _get_str(el)
|
|
31
|
+
if s:
|
|
32
|
+
ms.append(s.upper())
|
|
33
|
+
return sorted(set(ms)) if ms else ["GET"]
|
|
34
|
+
return ["GET"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_python_endpoints(py_source: str, file_path: str) -> List[Endpoint]:
|
|
38
|
+
try:
|
|
39
|
+
tree = ast.parse(py_source)
|
|
40
|
+
except SyntaxError:
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
endpoints: List[Endpoint] = []
|
|
44
|
+
|
|
45
|
+
for node in ast.walk(tree):
|
|
46
|
+
if not isinstance(node, ast.FunctionDef):
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
if not node.decorator_list:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
handler = node.name
|
|
53
|
+
|
|
54
|
+
for dec in node.decorator_list:
|
|
55
|
+
if not isinstance(dec, ast.Call):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
obj, meth = _call_name(dec)
|
|
59
|
+
if not obj or not meth:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# path is usually first arg
|
|
63
|
+
pnode = dec.args[0] if dec.args else None
|
|
64
|
+
path = _get_str(pnode) if pnode else None
|
|
65
|
+
if not path or not path.startswith("/"):
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
low = meth.lower()
|
|
69
|
+
|
|
70
|
+
# FastAPI: @app.get("/x") / @router.post("/x")
|
|
71
|
+
if low in _FASTAPI_METHODS:
|
|
72
|
+
endpoints.append(
|
|
73
|
+
Endpoint(
|
|
74
|
+
method=low.upper(),
|
|
75
|
+
path=path,
|
|
76
|
+
file=file_path,
|
|
77
|
+
line=getattr(dec, "lineno", getattr(node, "lineno", 1)),
|
|
78
|
+
handler=handler,
|
|
79
|
+
framework="fastapi",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Flask: @app.route("/x", methods=[...])
|
|
85
|
+
if low == "route":
|
|
86
|
+
ms = _extract_flask_methods(dec)
|
|
87
|
+
for m in ms:
|
|
88
|
+
endpoints.append(
|
|
89
|
+
Endpoint(
|
|
90
|
+
method=m,
|
|
91
|
+
path=path,
|
|
92
|
+
file=file_path,
|
|
93
|
+
line=getattr(dec, "lineno", getattr(node, "lineno", 1)),
|
|
94
|
+
handler=handler,
|
|
95
|
+
framework="flask",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return endpoints
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Iterable, Optional
|
|
6
|
+
|
|
7
|
+
# Optional: pip install pathspec
|
|
8
|
+
try:
|
|
9
|
+
import pathspec # type: ignore
|
|
10
|
+
except Exception: # pragma: no cover
|
|
11
|
+
pathspec = None
|
|
12
|
+
|
|
13
|
+
from ..core.project import find_project_root, SKIP_DIRS
|
|
14
|
+
from ..commands.project.tree_cmd import DEFAULT_IGNORE_NAMES # reuse your ignore set
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class EndpointScanOptions:
|
|
19
|
+
show_hidden: bool
|
|
20
|
+
use_gitignore: bool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_hidden_name(name: str) -> bool:
|
|
24
|
+
return name.startswith(".")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_gitignore_spec(root: Path):
|
|
28
|
+
if pathspec is None:
|
|
29
|
+
return None
|
|
30
|
+
gi = root / ".gitignore"
|
|
31
|
+
if not gi.exists():
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
lines: list[str] = []
|
|
35
|
+
for line in gi.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
36
|
+
s = line.strip()
|
|
37
|
+
if not s or s.startswith("#"):
|
|
38
|
+
continue
|
|
39
|
+
lines.append(s)
|
|
40
|
+
|
|
41
|
+
if not lines:
|
|
42
|
+
return None
|
|
43
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", lines)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _gitignore_matches(spec, rel_posix: str, is_dir: bool) -> bool:
|
|
47
|
+
if spec is None:
|
|
48
|
+
return False
|
|
49
|
+
if spec.match_file(rel_posix):
|
|
50
|
+
return True
|
|
51
|
+
if is_dir and spec.match_file(rel_posix.rstrip("/") + "/"):
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_skipped(p: Path, root: Path, spec, opts: EndpointScanOptions) -> bool:
|
|
57
|
+
name = p.name
|
|
58
|
+
|
|
59
|
+
# reuse your directory skip policy + tree ignores
|
|
60
|
+
if any(part in SKIP_DIRS for part in p.parts):
|
|
61
|
+
return True
|
|
62
|
+
if name in DEFAULT_IGNORE_NAMES:
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
if _is_hidden_name(name) and not opts.show_hidden:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
if opts.use_gitignore and spec is not None:
|
|
69
|
+
try:
|
|
70
|
+
rel = p.relative_to(root).as_posix()
|
|
71
|
+
except Exception:
|
|
72
|
+
rel = p.as_posix()
|
|
73
|
+
if _gitignore_matches(spec, rel, p.is_dir()):
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def iter_supported_source_files(start: Path, opts: EndpointScanOptions) -> Iterable[Path]:
|
|
80
|
+
start = start.resolve()
|
|
81
|
+
project_root = find_project_root(start)
|
|
82
|
+
spec = _load_gitignore_spec(project_root) if opts.use_gitignore else None
|
|
83
|
+
|
|
84
|
+
# manual walk so we can prune dirs early
|
|
85
|
+
stack = [project_root]
|
|
86
|
+
while stack:
|
|
87
|
+
cur = stack.pop()
|
|
88
|
+
if _is_skipped(cur, project_root, spec, opts):
|
|
89
|
+
continue
|
|
90
|
+
try:
|
|
91
|
+
entries = list(cur.iterdir())
|
|
92
|
+
except Exception:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
for e in entries:
|
|
96
|
+
if _is_skipped(e, project_root, spec, opts):
|
|
97
|
+
continue
|
|
98
|
+
if e.is_dir():
|
|
99
|
+
stack.append(e)
|
|
100
|
+
elif e.is_file():
|
|
101
|
+
if e.suffix.lower() in {".java", ".py"}:
|
|
102
|
+
yield e.resolve()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/graph/reverse_related_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aja_codeintel-0.1.0 → aja_codeintel-0.1.2}/codeintel_cli/commands/project/servicemap_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|