code-review-graph-codeblackwell 2.3.6.post1__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.
- code_review_graph/__init__.py +20 -0
- code_review_graph/__main__.py +4 -0
- code_review_graph/analysis.py +410 -0
- code_review_graph/changes.py +409 -0
- code_review_graph/cli.py +1255 -0
- code_review_graph/communities.py +874 -0
- code_review_graph/constants.py +23 -0
- code_review_graph/context_savings.py +317 -0
- code_review_graph/custom_languages.py +322 -0
- code_review_graph/daemon.py +1009 -0
- code_review_graph/daemon_cli.py +320 -0
- code_review_graph/docs/LLM-OPTIMIZED-REFERENCE.md +71 -0
- code_review_graph/embeddings.py +1006 -0
- code_review_graph/enrich.py +303 -0
- code_review_graph/eval/__init__.py +33 -0
- code_review_graph/eval/benchmarks/__init__.py +1 -0
- code_review_graph/eval/benchmarks/agent_baseline.py +193 -0
- code_review_graph/eval/benchmarks/build_performance.py +60 -0
- code_review_graph/eval/benchmarks/flow_completeness.py +36 -0
- code_review_graph/eval/benchmarks/impact_accuracy.py +220 -0
- code_review_graph/eval/benchmarks/multi_hop_retrieval.py +125 -0
- code_review_graph/eval/benchmarks/search_quality.py +59 -0
- code_review_graph/eval/benchmarks/token_efficiency.py +143 -0
- code_review_graph/eval/configs/code-review-graph.yaml +50 -0
- code_review_graph/eval/configs/express.yaml +45 -0
- code_review_graph/eval/configs/fastapi.yaml +48 -0
- code_review_graph/eval/configs/flask.yaml +50 -0
- code_review_graph/eval/configs/gin.yaml +51 -0
- code_review_graph/eval/configs/httpx.yaml +48 -0
- code_review_graph/eval/reporter.py +301 -0
- code_review_graph/eval/runner.py +211 -0
- code_review_graph/eval/scorer.py +85 -0
- code_review_graph/eval/token_benchmark.py +182 -0
- code_review_graph/exports.py +409 -0
- code_review_graph/flows.py +698 -0
- code_review_graph/graph.py +1427 -0
- code_review_graph/graph_diff.py +122 -0
- code_review_graph/hints.py +384 -0
- code_review_graph/incremental.py +1245 -0
- code_review_graph/jedi_resolver.py +303 -0
- code_review_graph/main.py +1079 -0
- code_review_graph/memory.py +142 -0
- code_review_graph/migrations.py +284 -0
- code_review_graph/parser.py +6957 -0
- code_review_graph/postprocessing.py +134 -0
- code_review_graph/prompts.py +159 -0
- code_review_graph/refactor.py +852 -0
- code_review_graph/registry.py +319 -0
- code_review_graph/rescript_resolver.py +206 -0
- code_review_graph/search.py +447 -0
- code_review_graph/skills.py +1481 -0
- code_review_graph/spring_resolver.py +200 -0
- code_review_graph/temporal_resolver.py +199 -0
- code_review_graph/token_benchmark.py +125 -0
- code_review_graph/tools/__init__.py +156 -0
- code_review_graph/tools/_common.py +176 -0
- code_review_graph/tools/analysis_tools.py +184 -0
- code_review_graph/tools/build.py +541 -0
- code_review_graph/tools/community_tools.py +246 -0
- code_review_graph/tools/context.py +152 -0
- code_review_graph/tools/docs.py +274 -0
- code_review_graph/tools/flows_tools.py +176 -0
- code_review_graph/tools/query.py +692 -0
- code_review_graph/tools/refactor_tools.py +168 -0
- code_review_graph/tools/registry_tools.py +125 -0
- code_review_graph/tools/review.py +477 -0
- code_review_graph/tsconfig_resolver.py +257 -0
- code_review_graph/visualization.py +2184 -0
- code_review_graph/wiki.py +305 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/METADATA +718 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/RECORD +74 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/WHEEL +4 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/entry_points.txt +3 -0
- code_review_graph_codeblackwell-2.3.6.post1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""TypeScript tsconfig.json path alias resolver.
|
|
2
|
+
|
|
3
|
+
Resolves TypeScript path aliases (e.g., ``@/ -> src/``) declared in
|
|
4
|
+
``compilerOptions.paths`` so that ``IMPORTS_FROM`` edges can point to
|
|
5
|
+
real file paths instead of raw alias strings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Extensions probed when resolving an alias target
|
|
19
|
+
_PROBE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".vue"]
|
|
20
|
+
|
|
21
|
+
# Tsconfig filenames to look for when walking up the directory tree
|
|
22
|
+
_TSCONFIG_NAMES = ["tsconfig.json", "tsconfig.app.json"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TsconfigResolver:
|
|
26
|
+
"""Resolves TypeScript path aliases (e.g., @/ -> src/) using tsconfig.json."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._cache: dict[str, Optional[dict]] = {}
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# Public API
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def resolve_alias(self, import_str: str, file_path: str) -> Optional[str]:
|
|
36
|
+
"""Resolve a TS path alias to an absolute file path, or None."""
|
|
37
|
+
try:
|
|
38
|
+
config = self._load_tsconfig_for_file(file_path)
|
|
39
|
+
if config is None:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
base_url: Optional[str] = config.get("baseUrl")
|
|
43
|
+
paths: dict[str, list[str]] = config.get("paths", {})
|
|
44
|
+
tsconfig_dir: str = config.get("_tsconfig_dir", "")
|
|
45
|
+
|
|
46
|
+
if not paths:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
if base_url:
|
|
50
|
+
base_dir = (Path(tsconfig_dir) / base_url).resolve()
|
|
51
|
+
else:
|
|
52
|
+
base_dir = Path(tsconfig_dir).resolve()
|
|
53
|
+
|
|
54
|
+
return self._match_and_probe(import_str, paths, base_dir)
|
|
55
|
+
except (OSError, ValueError, TypeError):
|
|
56
|
+
logger.debug(
|
|
57
|
+
"TsconfigResolver: unexpected error for %s", file_path, exc_info=True,
|
|
58
|
+
)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
# Internal helpers
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def _load_tsconfig_for_file(self, file_path: str) -> Optional[dict]:
|
|
66
|
+
"""Find and load tsconfig.json for the given file."""
|
|
67
|
+
start_dir = Path(file_path).parent.resolve()
|
|
68
|
+
current = start_dir
|
|
69
|
+
visited: list[str] = []
|
|
70
|
+
|
|
71
|
+
while True:
|
|
72
|
+
dir_str = str(current)
|
|
73
|
+
if dir_str in self._cache:
|
|
74
|
+
result = self._cache[dir_str]
|
|
75
|
+
for visited_dir in visited:
|
|
76
|
+
self._cache[visited_dir] = result
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
visited.append(dir_str)
|
|
80
|
+
|
|
81
|
+
for name in _TSCONFIG_NAMES:
|
|
82
|
+
candidate = current / name
|
|
83
|
+
if candidate.is_file():
|
|
84
|
+
config = self._parse_tsconfig(candidate)
|
|
85
|
+
config["_tsconfig_dir"] = dir_str
|
|
86
|
+
for visited_dir in visited:
|
|
87
|
+
self._cache[visited_dir] = config
|
|
88
|
+
return config
|
|
89
|
+
|
|
90
|
+
parent = current.parent
|
|
91
|
+
if parent == current:
|
|
92
|
+
for visited_dir in visited:
|
|
93
|
+
self._cache[visited_dir] = None
|
|
94
|
+
return None
|
|
95
|
+
current = parent
|
|
96
|
+
|
|
97
|
+
def _parse_tsconfig(self, tsconfig_path: Path) -> dict:
|
|
98
|
+
"""Parse a tsconfig.json file (supports JSONC comments)."""
|
|
99
|
+
seen: set[str] = set()
|
|
100
|
+
return self._resolve_extends(tsconfig_path, seen)
|
|
101
|
+
|
|
102
|
+
def _resolve_extends(self, tsconfig_path: Path, seen: set[str]) -> dict:
|
|
103
|
+
"""Recursively resolve the tsconfig extends chain."""
|
|
104
|
+
canonical = str(tsconfig_path.resolve())
|
|
105
|
+
if canonical in seen:
|
|
106
|
+
logger.debug("TsconfigResolver: cycle detected at %s", canonical)
|
|
107
|
+
return {}
|
|
108
|
+
seen = seen | {canonical}
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
raw = tsconfig_path.read_text(encoding="utf-8", errors="replace")
|
|
112
|
+
except OSError:
|
|
113
|
+
logger.debug("TsconfigResolver: cannot read %s", tsconfig_path)
|
|
114
|
+
return {}
|
|
115
|
+
|
|
116
|
+
stripped = self._strip_jsonc_comments(raw)
|
|
117
|
+
try:
|
|
118
|
+
data: dict = json.loads(stripped)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
logger.debug("TsconfigResolver: invalid JSON in %s", tsconfig_path)
|
|
121
|
+
return {}
|
|
122
|
+
|
|
123
|
+
result: dict = {}
|
|
124
|
+
|
|
125
|
+
extends: Optional[str] = data.get("extends")
|
|
126
|
+
if extends and isinstance(extends, str) and extends.startswith("."):
|
|
127
|
+
parent_path = (tsconfig_path.parent / extends).resolve()
|
|
128
|
+
if not parent_path.suffix:
|
|
129
|
+
parent_path = parent_path.with_suffix(".json")
|
|
130
|
+
if parent_path.is_file():
|
|
131
|
+
parent_config = self._resolve_extends(parent_path, seen)
|
|
132
|
+
parent_opts = parent_config.get("compilerOptions", {})
|
|
133
|
+
result.setdefault("compilerOptions", {}).update(parent_opts)
|
|
134
|
+
|
|
135
|
+
child_opts: dict = data.get("compilerOptions", {})
|
|
136
|
+
result.setdefault("compilerOptions", {}).update(child_opts)
|
|
137
|
+
|
|
138
|
+
compiler_options = result.get("compilerOptions", {})
|
|
139
|
+
if "baseUrl" in compiler_options:
|
|
140
|
+
result["baseUrl"] = compiler_options["baseUrl"]
|
|
141
|
+
if "paths" in compiler_options:
|
|
142
|
+
result["paths"] = compiler_options["paths"]
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
def _strip_jsonc_comments(self, text: str) -> str:
|
|
147
|
+
"""Remove // and /* */ comments and trailing commas from JSONC."""
|
|
148
|
+
result: list[str] = []
|
|
149
|
+
i = 0
|
|
150
|
+
n = len(text)
|
|
151
|
+
|
|
152
|
+
while i < n:
|
|
153
|
+
ch = text[i]
|
|
154
|
+
|
|
155
|
+
if ch == '"':
|
|
156
|
+
result.append(ch)
|
|
157
|
+
i += 1
|
|
158
|
+
while i < n:
|
|
159
|
+
c = text[i]
|
|
160
|
+
result.append(c)
|
|
161
|
+
if c == "\\" and i + 1 < n:
|
|
162
|
+
i += 1
|
|
163
|
+
result.append(text[i])
|
|
164
|
+
elif c == '"':
|
|
165
|
+
break
|
|
166
|
+
i += 1
|
|
167
|
+
i += 1
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if ch == "/" and i + 1 < n and text[i + 1] == "*":
|
|
171
|
+
i += 2
|
|
172
|
+
while i < n - 1:
|
|
173
|
+
if text[i] == "*" and text[i + 1] == "/":
|
|
174
|
+
i += 2
|
|
175
|
+
break
|
|
176
|
+
i += 1
|
|
177
|
+
else:
|
|
178
|
+
i = n
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
if ch == "/" and i + 1 < n and text[i + 1] == "/":
|
|
182
|
+
i += 2
|
|
183
|
+
while i < n and text[i] != "\n":
|
|
184
|
+
i += 1
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
result.append(ch)
|
|
188
|
+
i += 1
|
|
189
|
+
|
|
190
|
+
stripped = "".join(result)
|
|
191
|
+
stripped = re.sub(r",\s*([\]}])", r"\1", stripped)
|
|
192
|
+
return stripped
|
|
193
|
+
|
|
194
|
+
def _match_and_probe(
|
|
195
|
+
self,
|
|
196
|
+
import_str: str,
|
|
197
|
+
paths: dict[str, list[str]],
|
|
198
|
+
base_dir: Path,
|
|
199
|
+
) -> Optional[str]:
|
|
200
|
+
"""Match import_str against alias patterns and probe the filesystem."""
|
|
201
|
+
def _pattern_specificity(item: tuple[str, list[str]]) -> int:
|
|
202
|
+
pat = item[0]
|
|
203
|
+
return len(pat.partition("*")[0])
|
|
204
|
+
|
|
205
|
+
sorted_paths = sorted(paths.items(), key=_pattern_specificity, reverse=True)
|
|
206
|
+
|
|
207
|
+
for pattern, replacements in sorted_paths:
|
|
208
|
+
suffix = _match_pattern(pattern, import_str)
|
|
209
|
+
if suffix is None:
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
for replacement in replacements:
|
|
213
|
+
if "*" in replacement:
|
|
214
|
+
mapped = replacement.replace("*", suffix, 1)
|
|
215
|
+
else:
|
|
216
|
+
mapped = replacement
|
|
217
|
+
|
|
218
|
+
candidate_base = (base_dir / mapped).resolve()
|
|
219
|
+
found = _probe_path(candidate_base)
|
|
220
|
+
if found:
|
|
221
|
+
return str(found)
|
|
222
|
+
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Module-level helpers
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _match_pattern(pattern: str, import_str: str) -> Optional[str]:
|
|
232
|
+
"""Return the wildcard-matched suffix if pattern matches import_str."""
|
|
233
|
+
if "*" not in pattern:
|
|
234
|
+
return "" if import_str == pattern else None
|
|
235
|
+
|
|
236
|
+
prefix, _, suffix_pat = pattern.partition("*")
|
|
237
|
+
if not (import_str.startswith(prefix) and import_str.endswith(suffix_pat)):
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
end = len(import_str) - len(suffix_pat) if suffix_pat else len(import_str)
|
|
241
|
+
return import_str[len(prefix):end]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _probe_path(base: Path) -> Optional[Path]:
|
|
245
|
+
"""Probe base and base + extensions for an existing file."""
|
|
246
|
+
if base.is_file():
|
|
247
|
+
return base
|
|
248
|
+
for ext in _PROBE_EXTENSIONS:
|
|
249
|
+
candidate = base.with_suffix(ext) if not base.suffix else Path(str(base) + ext)
|
|
250
|
+
if candidate.is_file():
|
|
251
|
+
return candidate
|
|
252
|
+
if base.is_dir():
|
|
253
|
+
for ext in _PROBE_EXTENSIONS:
|
|
254
|
+
candidate = base / f"index{ext}"
|
|
255
|
+
if candidate.is_file():
|
|
256
|
+
return candidate
|
|
257
|
+
return None
|