graphlens-php 0.7.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.
- graphlens_php/__init__.py +9 -0
- graphlens_php/_adapter.py +427 -0
- graphlens_php/_deps.py +164 -0
- graphlens_php/_module_resolver.py +109 -0
- graphlens_php/_project_detector.py +76 -0
- graphlens_php/_resolver.py +596 -0
- graphlens_php/_visitor.py +809 -0
- graphlens_php-0.7.0.dist-info/METADATA +8 -0
- graphlens_php-0.7.0.dist-info/RECORD +11 -0
- graphlens_php-0.7.0.dist-info/WHEEL +4 -0
- graphlens_php-0.7.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Namespace resolution and PSR-4 source-root detection for PHP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_psr4_map(project_root: Path) -> dict[str, list[Path]]:
|
|
13
|
+
r"""
|
|
14
|
+
Return the project's PSR-4 namespace → directories map.
|
|
15
|
+
|
|
16
|
+
Reads both ``autoload`` and ``autoload-dev`` so that test namespaces are
|
|
17
|
+
treated as internal. Namespace keys are returned without their trailing
|
|
18
|
+
backslash (``"App\\"`` → ``"App"``). Directories are resolved relative to
|
|
19
|
+
``project_root``. Returns ``{}`` on any error (never raises).
|
|
20
|
+
"""
|
|
21
|
+
composer = project_root / "composer.json"
|
|
22
|
+
if not composer.exists():
|
|
23
|
+
return {}
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(composer.read_text(encoding="utf-8"))
|
|
26
|
+
except (OSError, json.JSONDecodeError):
|
|
27
|
+
return {}
|
|
28
|
+
if not isinstance(data, dict):
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
out: dict[str, list[Path]] = {}
|
|
32
|
+
for section in ("autoload", "autoload-dev"):
|
|
33
|
+
block = data.get(section)
|
|
34
|
+
if not isinstance(block, dict):
|
|
35
|
+
continue
|
|
36
|
+
psr4 = block.get("psr-4")
|
|
37
|
+
if not isinstance(psr4, dict):
|
|
38
|
+
continue
|
|
39
|
+
for prefix, dirs in psr4.items():
|
|
40
|
+
if not isinstance(prefix, str): # pragma: no cover
|
|
41
|
+
continue
|
|
42
|
+
namespace = prefix.rstrip("\\")
|
|
43
|
+
dir_list = [dirs] if isinstance(dirs, str) else dirs
|
|
44
|
+
if not isinstance(dir_list, list):
|
|
45
|
+
continue
|
|
46
|
+
resolved = [
|
|
47
|
+
(project_root / d) for d in dir_list if isinstance(d, str)
|
|
48
|
+
]
|
|
49
|
+
out.setdefault(namespace, []).extend(resolved)
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def internal_namespace_tops(project_root: Path) -> set[str]:
|
|
54
|
+
"""Return the top-level segment of every PSR-4 namespace prefix."""
|
|
55
|
+
tops: set[str] = set()
|
|
56
|
+
for namespace in load_psr4_map(project_root):
|
|
57
|
+
if namespace:
|
|
58
|
+
tops.add(namespace.split("\\", maxsplit=1)[0])
|
|
59
|
+
return tops
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def find_source_roots(
|
|
63
|
+
project_root: Path,
|
|
64
|
+
files: list[Path], # noqa: ARG001
|
|
65
|
+
) -> list[Path]:
|
|
66
|
+
"""
|
|
67
|
+
Detect PHP source roots from the PSR-4 autoload directories.
|
|
68
|
+
|
|
69
|
+
Every directory referenced by a PSR-4 prefix becomes a source root, with
|
|
70
|
+
``project_root`` appended last as a catch-all so files outside any
|
|
71
|
+
declared PSR-4 tree are still attributable.
|
|
72
|
+
"""
|
|
73
|
+
roots: list[Path] = []
|
|
74
|
+
for dirs in load_psr4_map(project_root).values():
|
|
75
|
+
for directory in dirs:
|
|
76
|
+
if directory.is_dir() and directory not in roots:
|
|
77
|
+
roots.append(directory)
|
|
78
|
+
if project_root not in roots:
|
|
79
|
+
roots.append(project_root)
|
|
80
|
+
return roots
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def path_to_namespace(file_path: Path, project_root: Path) -> str:
|
|
84
|
+
r"""
|
|
85
|
+
Map a file path to its PSR-4 namespace (the file's containing namespace).
|
|
86
|
+
|
|
87
|
+
This is a *fallback* used only when a file declares no ``namespace``
|
|
88
|
+
statement of its own — the in-source declaration is always authoritative
|
|
89
|
+
when present. For ``psr-4 {"App\\": "src/"}`` the file
|
|
90
|
+
``src/Service/UserService.php`` maps to namespace ``App\\Service``.
|
|
91
|
+
|
|
92
|
+
Returns ``""`` (the global namespace) when no PSR-4 prefix matches.
|
|
93
|
+
"""
|
|
94
|
+
psr4 = load_psr4_map(project_root)
|
|
95
|
+
best_namespace = ""
|
|
96
|
+
best_len = -1
|
|
97
|
+
for namespace, dirs in psr4.items():
|
|
98
|
+
for directory in dirs:
|
|
99
|
+
try:
|
|
100
|
+
relative = file_path.parent.relative_to(directory)
|
|
101
|
+
except ValueError:
|
|
102
|
+
continue
|
|
103
|
+
depth = len(directory.parts)
|
|
104
|
+
if depth <= best_len:
|
|
105
|
+
continue
|
|
106
|
+
sub = [p for p in relative.parts if p not in (".", "")]
|
|
107
|
+
best_namespace = "\\".join([namespace, *sub]) if sub else namespace
|
|
108
|
+
best_len = depth
|
|
109
|
+
return best_namespace
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""PHP project detection: marker files and project name extraction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from graphlens.utils import collect_marker_roots
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
PHP_MARKERS: tuple[str, ...] = (
|
|
14
|
+
"composer.json",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
EXCLUDED_DIRS: frozenset[str] = frozenset({
|
|
18
|
+
"vendor", "node_modules", ".git", "var", "cache",
|
|
19
|
+
"build", "dist", ".phpunit.cache",
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_php_project(project_root: Path) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Return True if the directory contains a PHP project.
|
|
26
|
+
|
|
27
|
+
Detection order:
|
|
28
|
+
1. PHP-specific marker files (composer.json)
|
|
29
|
+
2. Fallback: any ``.php`` file exists anywhere under project_root
|
|
30
|
+
|
|
31
|
+
The fallback handles multi-language monorepos and plain PHP projects
|
|
32
|
+
that ship no composer manifest.
|
|
33
|
+
"""
|
|
34
|
+
if (project_root / "composer.json").exists():
|
|
35
|
+
return True
|
|
36
|
+
return any(project_root.rglob("*.php"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def find_php_roots(search_root: Path) -> list[Path]:
|
|
40
|
+
"""
|
|
41
|
+
Find the actual PHP project roots within search_root.
|
|
42
|
+
|
|
43
|
+
Walks for ``composer.json`` markers and returns their parent
|
|
44
|
+
directories — one per distinct PHP sub-project. A marker at
|
|
45
|
+
``search_root`` does not hide nested marker roots, so a monorepo that is
|
|
46
|
+
itself a project and also contains PHP sub-packages yields every root.
|
|
47
|
+
|
|
48
|
+
Falls back to ``[search_root]`` when no markers are found anywhere (a
|
|
49
|
+
directory that contains only bare ``.php`` scripts with no manifest).
|
|
50
|
+
"""
|
|
51
|
+
return collect_marker_roots(
|
|
52
|
+
search_root,
|
|
53
|
+
PHP_MARKERS,
|
|
54
|
+
excluded_dirs=EXCLUDED_DIRS,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def detect_project_name(project_root: Path) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Extract the project name.
|
|
61
|
+
|
|
62
|
+
Resolution order:
|
|
63
|
+
1. composer.json ``name`` (e.g. ``vendor/package``)
|
|
64
|
+
2. project_root directory name
|
|
65
|
+
"""
|
|
66
|
+
composer = project_root / "composer.json"
|
|
67
|
+
if composer.exists():
|
|
68
|
+
try:
|
|
69
|
+
data = json.loads(composer.read_text(encoding="utf-8"))
|
|
70
|
+
except (OSError, json.JSONDecodeError):
|
|
71
|
+
data = {}
|
|
72
|
+
name = data.get("name") if isinstance(data, dict) else None
|
|
73
|
+
if isinstance(name, str) and name:
|
|
74
|
+
return name
|
|
75
|
+
|
|
76
|
+
return project_root.name
|