rpr-cli 0.1.1__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.
- rpr/__init__.py +1 -0
- rpr/agent/__init__.py +29 -0
- rpr/agent/approval.py +19 -0
- rpr/agent/bootstrap.py +60 -0
- rpr/agent/client.py +115 -0
- rpr/agent/control.py +16 -0
- rpr/agent/mock.py +158 -0
- rpr/agent/runtime.py +291 -0
- rpr/agent/session.py +91 -0
- rpr/agent/tools/__init__.py +10 -0
- rpr/agent/tools/base.py +62 -0
- rpr/agent/tools/mutating.py +47 -0
- rpr/agent/tools/readonly.py +147 -0
- rpr/agent/tools/registry.py +30 -0
- rpr/application/__init__.py +1 -0
- rpr/application/catalog.py +427 -0
- rpr/application/chat_service.py +413 -0
- rpr/application/checks.py +49 -0
- rpr/application/cli_adapter.py +36 -0
- rpr/application/completer.py +163 -0
- rpr/application/conversation_service.py +92 -0
- rpr/application/prompt_service.py +85 -0
- rpr/application/selector.py +240 -0
- rpr/application/shell.py +543 -0
- rpr/checks/__init__.py +0 -0
- rpr/checks/base.py +13 -0
- rpr/checks/instructions.py +89 -0
- rpr/checks/packages.py +619 -0
- rpr/checks/workspace.py +170 -0
- rpr/cli.py +78 -0
- rpr/commands/__init__.py +0 -0
- rpr/commands/add.py +166 -0
- rpr/commands/chat.py +48 -0
- rpr/commands/check.py +53 -0
- rpr/commands/generate/__init__.py +0 -0
- rpr/commands/generate/api.py +228 -0
- rpr/commands/generate/domain.py +383 -0
- rpr/commands/generate/engine.py +148 -0
- rpr/commands/generate/storybook.py +442 -0
- rpr/commands/generate/ui.py +414 -0
- rpr/commands/init.py +822 -0
- rpr/commands/map.py +113 -0
- rpr/commands/settings.py +102 -0
- rpr/commands/sync.py +97 -0
- rpr/context.py +203 -0
- rpr/generators/__init__.py +0 -0
- rpr/generators/base.py +110 -0
- rpr/generators/claude.py +33 -0
- rpr/generators/copilot.py +36 -0
- rpr/generators/cursor.py +38 -0
- rpr/generators/gemini.py +33 -0
- rpr/map/__init__.py +0 -0
- rpr/map/architecture.py +495 -0
- rpr/map/chains.py +317 -0
- rpr/map/classifier.py +170 -0
- rpr/map/coverage.py +200 -0
- rpr/map/dependencies.py +243 -0
- rpr/map/extractor.py +223 -0
- rpr/map/graph.py +318 -0
- rpr/map/output.py +1030 -0
- rpr/map/responsibility.py +345 -0
- rpr/map/topology.py +327 -0
- rpr/map/walker.py +151 -0
- rpr/scaffolds/domain/base_entity.md +30 -0
- rpr/scaffolds/domain/base_repo.md +48 -0
- rpr/scaffolds/domain/container.md +76 -0
- rpr/scaffolds/domain/settings.md +57 -0
- rpr/scaffolds/instructions/all.instructions.md +50 -0
- rpr/scaffolds/instructions/api.instructions.md +42 -0
- rpr/scaffolds/instructions/domain.instructions.md +93 -0
- rpr/scaffolds/instructions/frontend.instructions.md +97 -0
- rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
- rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
- rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
- rpr/scaffolds/instructions/tooling.instructions.md +42 -0
- rpr/scaffolds/js_special_files/fetch.service.md +222 -0
- rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
- rpr/scaffolds/special_files/domain_container.md +76 -0
- rpr/scaffolds/special_files/domain_settings.md +57 -0
- rpr/scaffolds/special_files/dto_util.md +62 -0
- rpr/scaffolds/special_files/encrypted_column.md +98 -0
- rpr/scaffolds/special_files/mapper_util.md +159 -0
- rpr/scaffolds/special_files/partial_update.md +61 -0
- rpr/templates/__init__.py +0 -0
- rpr/templates/registry.py +81 -0
- rpr/ui/__init__.py +0 -0
- rpr/ui/console.py +32 -0
- rpr/ui/markdown.py +59 -0
- rpr/ui/prompt_session.py +430 -0
- rpr/ui/renderers.py +167 -0
- rpr/ui/theme.py +286 -0
- rpr/workspace.py +131 -0
- rpr_cli-0.1.1.dist-info/METADATA +201 -0
- rpr_cli-0.1.1.dist-info/RECORD +97 -0
- rpr_cli-0.1.1.dist-info/WHEEL +4 -0
- rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
- rpr_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
rpr/map/dependencies.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""External dependency summarisation for the code map.
|
|
2
|
+
|
|
3
|
+
For each analysed directory, this module identifies which third-party (or
|
|
4
|
+
system) package identifiers appeared in raw import strings but were *not*
|
|
5
|
+
resolved to in-repo edges by the dependency-graph builder. The result is a
|
|
6
|
+
per-directory inventory of external package names that developers can review
|
|
7
|
+
without consulting raw source files.
|
|
8
|
+
|
|
9
|
+
The ``DependencyGraph`` contract is intentionally left unchanged — this module
|
|
10
|
+
is a sibling analysis artifact, not an extension of the resolved graph.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from rpr.map.graph import DependencyGraph
|
|
18
|
+
from rpr.map.topology import DirectoryTopology
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Constants
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
# Standard Rust crate roots that are not third-party packages.
|
|
25
|
+
_RUST_STDLIB_ROOTS: frozenset[str] = frozenset(
|
|
26
|
+
{
|
|
27
|
+
"std",
|
|
28
|
+
"core",
|
|
29
|
+
"alloc",
|
|
30
|
+
"proc_macro",
|
|
31
|
+
"test",
|
|
32
|
+
"self",
|
|
33
|
+
"super",
|
|
34
|
+
"crate",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Max number of packages listed per directory in the report.
|
|
39
|
+
_MAX_PACKAGES_PER_DIR = 15
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Domain value object
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ExternalDependencySummary:
|
|
49
|
+
"""Aggregated external packages pulled in by files in a single directory.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
directory: Repo-relative POSIX path (root is ``""``).
|
|
53
|
+
packages: Sorted, deduplicated external package identifiers.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
directory: str
|
|
57
|
+
packages: tuple[str, ...]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Language-specific import normalisation
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _normalise_python(raw: str) -> str | None:
|
|
66
|
+
"""Return the top-level Python package name, or None to discard.
|
|
67
|
+
|
|
68
|
+
Relative imports (starting with ``.``) came from the same repository and
|
|
69
|
+
were already considered by the graph builder; they are discarded here.
|
|
70
|
+
"""
|
|
71
|
+
if raw.startswith("."):
|
|
72
|
+
return None
|
|
73
|
+
segment = raw.split(".")[0]
|
|
74
|
+
return segment if segment else None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _normalise_ts_js(raw: str) -> str | None:
|
|
78
|
+
"""Return the npm package specifier, or None to discard.
|
|
79
|
+
|
|
80
|
+
Relative imports starting with ``.`` or ``/`` are in-repository references
|
|
81
|
+
that were already processed by the graph builder.
|
|
82
|
+
"""
|
|
83
|
+
if raw.startswith(".") or raw.startswith("/"):
|
|
84
|
+
return None
|
|
85
|
+
# Scoped packages: "@scope/name/sub" → "@scope/name"
|
|
86
|
+
if raw.startswith("@"):
|
|
87
|
+
parts = raw.split("/")
|
|
88
|
+
if len(parts) >= 2:
|
|
89
|
+
return "/".join(parts[:2])
|
|
90
|
+
return raw
|
|
91
|
+
# Bare specifier: "react/jsx-runtime" → "react"
|
|
92
|
+
return raw.split("/")[0]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalise_rust(raw: str) -> str | None:
|
|
96
|
+
"""Return the Rust crate root, or None to discard stdlib/self/crate paths."""
|
|
97
|
+
# Strip leading "::" that sometimes appears on absolute paths
|
|
98
|
+
clean = raw.lstrip(":")
|
|
99
|
+
root = clean.split("::")[0].split("/")[0]
|
|
100
|
+
if root in _RUST_STDLIB_ROOTS:
|
|
101
|
+
return None
|
|
102
|
+
return root if root else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _normalise_cpp(raw: str) -> str | None:
|
|
106
|
+
"""Return the first path segment of a C++ include, or None to discard.
|
|
107
|
+
|
|
108
|
+
Quoted includes (``"…"``) are typically project-relative and were already
|
|
109
|
+
resolved by the graph builder; only angle-bracket system/library includes
|
|
110
|
+
are treated as external here.
|
|
111
|
+
"""
|
|
112
|
+
# Quoted includes are relative → discard
|
|
113
|
+
if raw.startswith('"'):
|
|
114
|
+
return None
|
|
115
|
+
# Angle-bracket includes: strip surrounding < >
|
|
116
|
+
bare = raw.strip("<>")
|
|
117
|
+
segment = bare.split("/")[0]
|
|
118
|
+
return segment if segment else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_NORMALISE_BY_LANGUAGE = {
|
|
122
|
+
"python": _normalise_python,
|
|
123
|
+
"typescript": _normalise_ts_js,
|
|
124
|
+
"javascript": _normalise_ts_js,
|
|
125
|
+
"rust": _normalise_rust,
|
|
126
|
+
"cpp": _normalise_cpp,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Core builder
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_external_dependencies(
|
|
136
|
+
graph: DependencyGraph,
|
|
137
|
+
raw_imports: dict[str, list[str]],
|
|
138
|
+
topology: DirectoryTopology,
|
|
139
|
+
) -> list[ExternalDependencySummary]:
|
|
140
|
+
"""Build per-directory external dependency summaries.
|
|
141
|
+
|
|
142
|
+
For every file in *graph*, any raw import string that was *not* resolved to
|
|
143
|
+
an in-repo graph edge is normalised into an external package name and
|
|
144
|
+
attributed to the file's parent directory. Results are deduplicated and
|
|
145
|
+
sorted both within each directory and across the returned list.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
graph: The resolved dependency graph (read-only; provides resolved
|
|
149
|
+
edges and per-node language information).
|
|
150
|
+
raw_imports: Mapping ``{file_rel_path: [raw_import, ...]}``. Typically
|
|
151
|
+
produced by ``build_raw_import_inventory(graph)``.
|
|
152
|
+
topology: Directory topology (used to obtain the canonical directory
|
|
153
|
+
label for each file).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A list of :class:`ExternalDependencySummary` objects — one per directory
|
|
157
|
+
that has at least one external import — sorted by ``directory``.
|
|
158
|
+
Directories with no external packages are omitted.
|
|
159
|
+
"""
|
|
160
|
+
# Build a fast lookup set of resolved in-repo edges per file.
|
|
161
|
+
resolved_per_file: dict[str, set[str]] = {
|
|
162
|
+
rel: set(deps) for rel, deps in graph.edges.items()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Build a fast lookup: file rel_path → language
|
|
166
|
+
lang_by_file: dict[str, str] = {
|
|
167
|
+
str(sf.rel_path).replace("\\", "/"): sf.language
|
|
168
|
+
for sf in graph.nodes
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Build file → directory mapping from topology
|
|
172
|
+
file_to_dir: dict[str, str] = {}
|
|
173
|
+
for dir_node in topology.nodes.values():
|
|
174
|
+
for file_rel in dir_node.files:
|
|
175
|
+
file_to_dir[file_rel] = dir_node.path
|
|
176
|
+
|
|
177
|
+
# Accumulate external packages per directory
|
|
178
|
+
accumulator: dict[str, set[str]] = {}
|
|
179
|
+
|
|
180
|
+
for file_rel, raw_list in raw_imports.items():
|
|
181
|
+
lang = lang_by_file.get(file_rel)
|
|
182
|
+
if lang is None:
|
|
183
|
+
continue # file not in graph nodes; skip
|
|
184
|
+
normalise = _NORMALISE_BY_LANGUAGE.get(lang)
|
|
185
|
+
if normalise is None:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
resolved = resolved_per_file.get(file_rel, set())
|
|
189
|
+
directory = file_to_dir.get(file_rel, "")
|
|
190
|
+
bucket = accumulator.setdefault(directory, set())
|
|
191
|
+
|
|
192
|
+
for raw in raw_list:
|
|
193
|
+
# Check whether this raw import was resolved into an in-repo edge.
|
|
194
|
+
# The graph stores edge targets as rel_paths, not raw import strings,
|
|
195
|
+
# so we identify external imports as those that produce no in-repo edge
|
|
196
|
+
# pointing to a path that starts with the normalised identifier.
|
|
197
|
+
# Simpler heuristic: if the raw import starts with "." or the directory
|
|
198
|
+
# contains a file whose rel_path ends with the import path, it's
|
|
199
|
+
# already captured in resolved edges. Here we rely on the normaliser
|
|
200
|
+
# to filter out relative imports and then check whether *any* resolved
|
|
201
|
+
# edge target is "plausibly" this import. The key insight is that the
|
|
202
|
+
# graph builder already resolves everything it can; what remains in
|
|
203
|
+
# raw_list but is absent from resolved edges is external.
|
|
204
|
+
package = normalise(raw)
|
|
205
|
+
if package is None:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# If every resolved edge target represents an in-repo file, and none
|
|
209
|
+
# of them looks like it came from this raw import, treat it as external.
|
|
210
|
+
# We do this conservatively: we check whether any resolved edge target
|
|
211
|
+
# for this file has the package as a prefix segment.
|
|
212
|
+
is_in_repo = _looks_like_in_repo(package, resolved, lang)
|
|
213
|
+
if not is_in_repo:
|
|
214
|
+
bucket.add(package)
|
|
215
|
+
|
|
216
|
+
# Build sorted result, omitting directories with no external packages
|
|
217
|
+
result: list[ExternalDependencySummary] = []
|
|
218
|
+
for directory in sorted(accumulator):
|
|
219
|
+
packages = tuple(sorted(accumulator[directory]))
|
|
220
|
+
if packages:
|
|
221
|
+
capped = packages[:_MAX_PACKAGES_PER_DIR]
|
|
222
|
+
result.append(ExternalDependencySummary(directory=directory, packages=capped))
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _looks_like_in_repo(package: str, resolved_edges: set[str], lang: str) -> bool:
|
|
227
|
+
"""Return True if *package* appears to be satisfied by an in-repo resolved edge.
|
|
228
|
+
|
|
229
|
+
For Python and TS/JS the package name appears as a leading path segment or
|
|
230
|
+
module path component in the resolved edge targets. For Rust and C++ the
|
|
231
|
+
same heuristic applies to crate or include names.
|
|
232
|
+
"""
|
|
233
|
+
lower_pkg = package.lower()
|
|
234
|
+
for edge_target in resolved_edges:
|
|
235
|
+
# Normalise path separators and drop extension for comparison
|
|
236
|
+
target_norm = edge_target.replace("\\", "/").lower()
|
|
237
|
+
# e.g. "rpr/map/walker" starts with "rpr" → matches package "rpr"
|
|
238
|
+
parts = target_norm.split("/")
|
|
239
|
+
for part in parts:
|
|
240
|
+
stem = part.rsplit(".", 1)[0] # strip extension
|
|
241
|
+
if stem == lower_pkg or part == lower_pkg:
|
|
242
|
+
return True
|
|
243
|
+
return False
|
rpr/map/extractor.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Analysis bridge — Rust-first project dependency extraction.
|
|
2
|
+
|
|
3
|
+
Primary API
|
|
4
|
+
-----------
|
|
5
|
+
``analyze_project(root)`` is the single public entry-point consumed by the
|
|
6
|
+
``rpr map`` command. It returns a fully-resolved ``DependencyGraph`` domain
|
|
7
|
+
object, delegating the expensive crawl/parse/resolve work to the compiled Rust
|
|
8
|
+
extension when available and falling back to the pure-Python pipeline
|
|
9
|
+
otherwise.
|
|
10
|
+
|
|
11
|
+
Fallback extraction helpers
|
|
12
|
+
----------------------------
|
|
13
|
+
``extract_imports`` / ``extract_imports_batch`` parse raw import strings from
|
|
14
|
+
source files using the standard library (``ast`` for Python) and regular
|
|
15
|
+
expressions (TypeScript, JavaScript, Rust, C++). These helpers power the
|
|
16
|
+
Python compatibility path and remain available for callers that need them
|
|
17
|
+
directly.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
import re
|
|
24
|
+
import warnings
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Callable
|
|
27
|
+
|
|
28
|
+
from rpr.map.graph import DependencyGraph, build_graph
|
|
29
|
+
from rpr.map.walker import Language, SourceFile, walk
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import rpr_parser
|
|
33
|
+
|
|
34
|
+
_RUST_AVAILABLE = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
_RUST_AVAILABLE = False
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Type alias for per-language extraction functions (LSP: uniform signature)
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
_ExtractFn = Callable[[str], list[str]]
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Private per-language extractors (SRP: one concern each)
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _extract_python(source: str) -> list[str]:
|
|
50
|
+
"""Extract raw import strings from Python source using ``ast``."""
|
|
51
|
+
try:
|
|
52
|
+
tree = ast.parse(source)
|
|
53
|
+
except SyntaxError:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
results: list[str] = []
|
|
57
|
+
for node in ast.walk(tree):
|
|
58
|
+
if isinstance(node, ast.Import):
|
|
59
|
+
for alias in node.names:
|
|
60
|
+
results.append(alias.name)
|
|
61
|
+
elif isinstance(node, ast.ImportFrom):
|
|
62
|
+
dots = "." * (node.level or 0)
|
|
63
|
+
module = node.module or ""
|
|
64
|
+
results.append(dots + module)
|
|
65
|
+
return results
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Patterns compiled once at import time for efficiency.
|
|
69
|
+
_TS_JS_PATTERNS = (
|
|
70
|
+
# import ... from 'X' / "X"
|
|
71
|
+
re.compile(r"""import\s+.*?from\s+['"]([^'"]+)['"]""", re.DOTALL),
|
|
72
|
+
# require('X') / require("X")
|
|
73
|
+
re.compile(r"""require\s*\(\s*['"]([^'"]+)['"]\s*\)"""),
|
|
74
|
+
# import('X') — dynamic
|
|
75
|
+
re.compile(r"""import\s*\(\s*['"]([^'"]+)['"]\s*\)"""),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _extract_ts_js(source: str) -> list[str]:
|
|
80
|
+
"""Extract raw import specifiers from TypeScript / JavaScript source."""
|
|
81
|
+
results: list[str] = []
|
|
82
|
+
for pattern in _TS_JS_PATTERNS:
|
|
83
|
+
results.extend(pattern.findall(source))
|
|
84
|
+
return results
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_RUST_USE = re.compile(r"use\s+([\w:]+(?:\s+as\s+\w+)?)\s*;")
|
|
88
|
+
_RUST_MOD = re.compile(r"mod\s+(\w+)\s*;")
|
|
89
|
+
_RUST_EXTERN = re.compile(r"extern\s+crate\s+(\w+)\s*;")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _extract_rust(source: str) -> list[str]:
|
|
93
|
+
"""Extract raw import/module strings from Rust source."""
|
|
94
|
+
results: list[str] = []
|
|
95
|
+
|
|
96
|
+
for match in _RUST_USE.finditer(source):
|
|
97
|
+
# Drop the alias part: "std::io as io" → "std::io"
|
|
98
|
+
path = match.group(1).split(" as ")[0].strip()
|
|
99
|
+
results.append(path)
|
|
100
|
+
|
|
101
|
+
results.extend(_RUST_MOD.findall(source))
|
|
102
|
+
results.extend(_RUST_EXTERN.findall(source))
|
|
103
|
+
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_CPP_QUOTED = re.compile(r'#include\s+"([^"]+)"')
|
|
108
|
+
_CPP_ANGLE = re.compile(r"#include\s+<([^>]+)>")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_cpp(source: str) -> list[str]:
|
|
112
|
+
"""Extract raw include strings from C++ source.
|
|
113
|
+
|
|
114
|
+
Angle-bracket includes (system headers) are emitted without the ``<>``
|
|
115
|
+
delimiters; the graph resolver will drop them as external.
|
|
116
|
+
"""
|
|
117
|
+
results: list[str] = _CPP_QUOTED.findall(source)
|
|
118
|
+
results.extend(_CPP_ANGLE.findall(source))
|
|
119
|
+
return results
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Dispatch dict — adding a new language requires only a new entry here (OCP).
|
|
123
|
+
_EXTRACTORS: dict[Language, _ExtractFn] = {
|
|
124
|
+
"python": _extract_python,
|
|
125
|
+
"typescript": _extract_ts_js,
|
|
126
|
+
"javascript": _extract_ts_js,
|
|
127
|
+
"rust": _extract_rust,
|
|
128
|
+
"cpp": _extract_cpp,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Public extraction API
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def extract_imports(file: SourceFile) -> list[str]:
|
|
137
|
+
"""Extract raw import strings from a single ``SourceFile``.
|
|
138
|
+
|
|
139
|
+
Returns an empty list for unsupported languages. Returns an empty list
|
|
140
|
+
and emits a ``UserWarning`` when the file cannot be read.
|
|
141
|
+
"""
|
|
142
|
+
extractor = _EXTRACTORS.get(file.language)
|
|
143
|
+
if extractor is None:
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
source = file.abs_path.read_text(encoding="utf-8", errors="replace")
|
|
148
|
+
except OSError:
|
|
149
|
+
warnings.warn(f"Cannot read {file.rel_path} — skipping", stacklevel=2)
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
return extractor(source)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def extract_imports_batch(
|
|
156
|
+
files: list[SourceFile],
|
|
157
|
+
) -> dict[str, list[str]]:
|
|
158
|
+
"""Extract raw imports for every file; returns ``{rel_path: [import, ...]}``."""
|
|
159
|
+
return {str(f.rel_path).replace("\\", "/"): extract_imports(f) for f in files}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def build_raw_import_inventory(graph: DependencyGraph) -> dict[str, list[str]]:
|
|
163
|
+
"""Return raw (unresolved) import strings for every file in *graph*.
|
|
164
|
+
|
|
165
|
+
This is a convenience wrapper around :func:`extract_imports_batch` that
|
|
166
|
+
accepts an already-constructed :class:`~rpr.map.graph.DependencyGraph` and
|
|
167
|
+
returns the same ``{rel_path: [import, ...]}`` mapping. The result includes
|
|
168
|
+
*all* imports — both those that were resolved to in-repo edges and those
|
|
169
|
+
that remain unresolved (i.e., external packages or unreadable paths).
|
|
170
|
+
|
|
171
|
+
Callers that need only external imports should subtract the resolved edges
|
|
172
|
+
stored in ``graph.edges`` from this inventory.
|
|
173
|
+
"""
|
|
174
|
+
return extract_imports_batch(graph.nodes)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# Private path implementations
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _rust_path(root: Path) -> DependencyGraph:
|
|
183
|
+
"""Delegate the full analysis to the compiled Rust extension."""
|
|
184
|
+
payload = rpr_parser.build_dependency_graph(str(root))
|
|
185
|
+
nodes = [
|
|
186
|
+
SourceFile(
|
|
187
|
+
abs_path=Path(n["abs_path"]),
|
|
188
|
+
rel_path=Path(n["rel_path"]),
|
|
189
|
+
language=n["language"],
|
|
190
|
+
)
|
|
191
|
+
for n in payload["nodes"]
|
|
192
|
+
]
|
|
193
|
+
return DependencyGraph(
|
|
194
|
+
nodes=nodes,
|
|
195
|
+
edges=payload["edges"],
|
|
196
|
+
reverse_edges=payload["reverse_edges"],
|
|
197
|
+
cycles=payload["cycles"],
|
|
198
|
+
backend="rust",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _python_path(root: Path) -> DependencyGraph:
|
|
203
|
+
"""Pure-Python compatibility pipeline: walk → extract → build graph."""
|
|
204
|
+
files = walk(root)
|
|
205
|
+
imports_by_file = extract_imports_batch(files)
|
|
206
|
+
return build_graph(files, imports_by_file)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# Primary public API
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def analyze_project(root: Path) -> DependencyGraph:
|
|
215
|
+
"""Crawl *root* and return a fully-resolved ``DependencyGraph``.
|
|
216
|
+
|
|
217
|
+
Delegates to the Rust engine when ``rpr_parser`` is importable; falls back
|
|
218
|
+
to the Python-only pipeline otherwise. The returned graph's ``backend``
|
|
219
|
+
field indicates which path was taken.
|
|
220
|
+
"""
|
|
221
|
+
if _RUST_AVAILABLE:
|
|
222
|
+
return _rust_path(root)
|
|
223
|
+
return _python_path(root)
|