diffcontext 0.5.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.
- diffcontext/__init__.py +233 -0
- diffcontext/_warn_once.py +112 -0
- diffcontext/cache.py +216 -0
- diffcontext/cli/__init__.py +655 -0
- diffcontext/context/__init__.py +1 -0
- diffcontext/context/compiler.py +643 -0
- diffcontext/context/selector.py +258 -0
- diffcontext/diff/__init__.py +1 -0
- diffcontext/diff/git_diff.py +298 -0
- diffcontext/diff/state_manager.py +75 -0
- diffcontext/graph_builder.py +1026 -0
- diffcontext/history.py +154 -0
- diffcontext/impact/__init__.py +1 -0
- diffcontext/impact/blast_radius.py +58 -0
- diffcontext/impact/scoring.py +223 -0
- diffcontext/impact/traversal.py +58 -0
- diffcontext/impact/visualizer.py +338 -0
- diffcontext/languages/__init__.py +80 -0
- diffcontext/languages/typescript.py +960 -0
- diffcontext/lexical.py +108 -0
- diffcontext/models.py +180 -0
- diffcontext/parser.py +183 -0
- diffcontext/pipeline.py +887 -0
- diffcontext/py.typed +0 -0
- diffcontext/rerank/__init__.py +17 -0
- diffcontext/rerank/features.py +356 -0
- diffcontext/rerank/model.py +175 -0
- diffcontext/resolver.py +288 -0
- diffcontext/scanner.py +153 -0
- diffcontext/symbols.py +254 -0
- diffcontext/verify/__init__.py +68 -0
- diffcontext/verify/cases.py +631 -0
- diffcontext/verify/history.py +396 -0
- diffcontext/verify/sufficiency.py +324 -0
- diffcontext-0.5.1.dist-info/METADATA +219 -0
- diffcontext-0.5.1.dist-info/RECORD +40 -0
- diffcontext-0.5.1.dist-info/WHEEL +5 -0
- diffcontext-0.5.1.dist-info/entry_points.txt +2 -0
- diffcontext-0.5.1.dist-info/licenses/LICENSE +21 -0
- diffcontext-0.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
visualizer.py — Visual blast radius renderer.
|
|
3
|
+
|
|
4
|
+
Renders the blast radius as a colored, indented tree showing:
|
|
5
|
+
- The changed symbol at the root
|
|
6
|
+
- Direct callers (who calls this?)
|
|
7
|
+
- Direct callees (what does this call?)
|
|
8
|
+
- Transitive impact propagation
|
|
9
|
+
- Proof chains: the actual code line creating each edge
|
|
10
|
+
|
|
11
|
+
Designed for terminal output with ANSI colors.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Dict, List, Optional, Set
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ANSI color codes
|
|
18
|
+
class _C:
|
|
19
|
+
RED = "\033[91m"
|
|
20
|
+
YELLOW = "\033[93m"
|
|
21
|
+
GREEN = "\033[92m"
|
|
22
|
+
CYAN = "\033[96m"
|
|
23
|
+
MAGENTA = "\033[95m"
|
|
24
|
+
BLUE = "\033[94m"
|
|
25
|
+
DIM = "\033[2m"
|
|
26
|
+
BOLD = "\033[1m"
|
|
27
|
+
RESET = "\033[0m"
|
|
28
|
+
WHITE = "\033[97m"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_blast_radius(
|
|
32
|
+
graph: Dict[str, List[str]],
|
|
33
|
+
changed_symbols: List[str],
|
|
34
|
+
symbols: dict,
|
|
35
|
+
max_depth: int = 3,
|
|
36
|
+
show_proof: bool = False,
|
|
37
|
+
repo_path: str = "",
|
|
38
|
+
) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Render a visual tree of the blast radius for changed symbols.
|
|
41
|
+
|
|
42
|
+
Returns a formatted string ready for terminal output.
|
|
43
|
+
"""
|
|
44
|
+
# Build reverse graph (callers)
|
|
45
|
+
reverse: Dict[str, Set[str]] = {}
|
|
46
|
+
for caller, callees in graph.items():
|
|
47
|
+
for callee in callees:
|
|
48
|
+
reverse.setdefault(callee, set()).add(caller)
|
|
49
|
+
|
|
50
|
+
lines: List[str] = []
|
|
51
|
+
|
|
52
|
+
lines.append("")
|
|
53
|
+
lines.append(f"{_C.BOLD}{_C.WHITE}{'═' * 70}{_C.RESET}")
|
|
54
|
+
lines.append(f"{_C.BOLD}{_C.WHITE} BLAST RADIUS ANALYSIS{_C.RESET}")
|
|
55
|
+
lines.append(f"{_C.BOLD}{_C.WHITE}{'═' * 70}{_C.RESET}")
|
|
56
|
+
|
|
57
|
+
for sym_id in changed_symbols:
|
|
58
|
+
lines.append("")
|
|
59
|
+
lines.append(f" {_C.BOLD}{_C.RED}⚡ CHANGED:{_C.RESET} {_C.BOLD}{sym_id}{_C.RESET}")
|
|
60
|
+
|
|
61
|
+
# Show symbol location info
|
|
62
|
+
if sym_id in symbols:
|
|
63
|
+
sym = symbols[sym_id]
|
|
64
|
+
lines.append(f" {_C.DIM} File: {sym.file}{_C.RESET}")
|
|
65
|
+
lines.append(f" {_C.DIM} Line: {sym.lineno}{_C.RESET}")
|
|
66
|
+
|
|
67
|
+
lines.append("")
|
|
68
|
+
|
|
69
|
+
# ---- CALLERS (who is affected by this change?) ----
|
|
70
|
+
direct_callers = sorted(reverse.get(sym_id, set()))
|
|
71
|
+
lines.append(f" {_C.BOLD}{_C.YELLOW}▲ WHO CALLS THIS? (directly affected){_C.RESET}")
|
|
72
|
+
|
|
73
|
+
if not direct_callers:
|
|
74
|
+
lines.append(f" {_C.DIM} (no direct callers found){_C.RESET}")
|
|
75
|
+
else:
|
|
76
|
+
for i, caller in enumerate(direct_callers):
|
|
77
|
+
is_last = i == len(direct_callers) - 1
|
|
78
|
+
prefix = "└──" if is_last else "├──"
|
|
79
|
+
lines.append(f" {_C.YELLOW} {prefix} {caller}{_C.RESET}")
|
|
80
|
+
|
|
81
|
+
if show_proof:
|
|
82
|
+
proof = _find_proof(caller, sym_id, symbols, graph)
|
|
83
|
+
if proof:
|
|
84
|
+
pad = " " if is_last else "│ "
|
|
85
|
+
lines.append(f" {_C.DIM} {pad} proof: {proof}{_C.RESET}")
|
|
86
|
+
|
|
87
|
+
# 2nd-level callers (transitive)
|
|
88
|
+
if max_depth >= 2:
|
|
89
|
+
indirect_callers = sorted(reverse.get(caller, set()))
|
|
90
|
+
for j, caller2 in enumerate(indirect_callers[:5]):
|
|
91
|
+
is_last2 = j == len(indirect_callers[:5]) - 1
|
|
92
|
+
indent = " " if is_last else "│ "
|
|
93
|
+
prefix2 = "└──" if is_last2 else "├──"
|
|
94
|
+
lines.append(
|
|
95
|
+
f" {_C.DIM} {indent}{prefix2} {caller2}{_C.RESET}"
|
|
96
|
+
)
|
|
97
|
+
if len(indirect_callers) > 5:
|
|
98
|
+
indent = " " if is_last else "│ "
|
|
99
|
+
lines.append(
|
|
100
|
+
f" {_C.DIM} {indent}... +{len(indirect_callers) - 5} more{_C.RESET}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
lines.append("")
|
|
104
|
+
|
|
105
|
+
# ---- CALLEES (what does this function depend on?) ----
|
|
106
|
+
direct_callees = sorted(graph.get(sym_id, []))
|
|
107
|
+
lines.append(f" {_C.BOLD}{_C.GREEN}▼ WHAT DOES THIS CALL? (dependencies){_C.RESET}")
|
|
108
|
+
|
|
109
|
+
if not direct_callees:
|
|
110
|
+
lines.append(f" {_C.DIM} (no outgoing calls resolved){_C.RESET}")
|
|
111
|
+
else:
|
|
112
|
+
for i, callee in enumerate(direct_callees):
|
|
113
|
+
is_last = i == len(direct_callees) - 1
|
|
114
|
+
prefix = "└──" if is_last else "├──"
|
|
115
|
+
lines.append(f" {_C.GREEN} {prefix} {callee}{_C.RESET}")
|
|
116
|
+
|
|
117
|
+
if show_proof:
|
|
118
|
+
proof = _find_proof(sym_id, callee, symbols, graph)
|
|
119
|
+
if proof:
|
|
120
|
+
pad = " " if is_last else "│ "
|
|
121
|
+
lines.append(f" {_C.DIM} {pad} proof: {proof}{_C.RESET}")
|
|
122
|
+
|
|
123
|
+
# 2nd-level callees
|
|
124
|
+
if max_depth >= 2:
|
|
125
|
+
indirect_callees = sorted(graph.get(callee, []))
|
|
126
|
+
for j, callee2 in enumerate(indirect_callees[:5]):
|
|
127
|
+
is_last2 = j == len(indirect_callees[:5]) - 1
|
|
128
|
+
indent = " " if is_last else "│ "
|
|
129
|
+
prefix2 = "└──" if is_last2 else "├──"
|
|
130
|
+
lines.append(
|
|
131
|
+
f" {_C.DIM} {indent}{prefix2} {callee2}{_C.RESET}"
|
|
132
|
+
)
|
|
133
|
+
if len(indirect_callees) > 5:
|
|
134
|
+
indent = " " if is_last else "│ "
|
|
135
|
+
lines.append(
|
|
136
|
+
f" {_C.DIM} {indent}... +{len(indirect_callees) - 5} more{_C.RESET}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
lines.append("")
|
|
140
|
+
|
|
141
|
+
# ---- FULL TRANSITIVE BLAST RADIUS ----
|
|
142
|
+
all_affected = _get_transitive_callers(reverse, sym_id, max_depth)
|
|
143
|
+
lines.append(f" {_C.BOLD}{_C.CYAN}◉ FULL BLAST RADIUS{_C.RESET}")
|
|
144
|
+
lines.append(
|
|
145
|
+
f" {_C.CYAN} {len(all_affected)} symbols transitively affected{_C.RESET}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Group by file
|
|
149
|
+
by_file: Dict[str, List[str]] = {}
|
|
150
|
+
for affected_sym in all_affected:
|
|
151
|
+
parts = affected_sym.split(":", 1)
|
|
152
|
+
filepath = parts[0] if len(parts) == 2 else "unknown"
|
|
153
|
+
by_file.setdefault(filepath, []).append(affected_sym)
|
|
154
|
+
|
|
155
|
+
for filepath, syms in sorted(by_file.items()):
|
|
156
|
+
lines.append(f" {_C.BLUE} 📄 {filepath} ({len(syms)} symbols){_C.RESET}")
|
|
157
|
+
for sym in sorted(syms)[:8]:
|
|
158
|
+
name = sym.split(":", 1)[1] if ":" in sym else sym
|
|
159
|
+
lines.append(f" {_C.DIM} · {name}{_C.RESET}")
|
|
160
|
+
if len(syms) > 8:
|
|
161
|
+
lines.append(f" {_C.DIM} ... +{len(syms) - 8} more{_C.RESET}")
|
|
162
|
+
|
|
163
|
+
# ---- SUMMARY ----
|
|
164
|
+
lines.append("")
|
|
165
|
+
lines.append(f"{_C.BOLD}{_C.WHITE}{'─' * 70}{_C.RESET}")
|
|
166
|
+
|
|
167
|
+
total_blast = set()
|
|
168
|
+
for sym_id in changed_symbols:
|
|
169
|
+
total_blast.update(_get_transitive_callers(reverse, sym_id, max_depth))
|
|
170
|
+
|
|
171
|
+
total_deps = set()
|
|
172
|
+
for sym_id in changed_symbols:
|
|
173
|
+
total_deps.update(_get_transitive_callees(graph, sym_id, max_depth))
|
|
174
|
+
|
|
175
|
+
blast_files = set()
|
|
176
|
+
for sym in total_blast:
|
|
177
|
+
parts = sym.split(":", 1)
|
|
178
|
+
if len(parts) == 2:
|
|
179
|
+
blast_files.add(parts[0])
|
|
180
|
+
|
|
181
|
+
lines.append(f" {_C.BOLD}Summary:{_C.RESET}")
|
|
182
|
+
lines.append(f" Changed symbols : {_C.RED}{len(changed_symbols)}{_C.RESET}")
|
|
183
|
+
lines.append(f" Direct callers : {_C.YELLOW}{sum(len(reverse.get(s, set())) for s in changed_symbols)}{_C.RESET}")
|
|
184
|
+
lines.append(f" Direct dependencies: {_C.GREEN}{sum(len(graph.get(s, [])) for s in changed_symbols)}{_C.RESET}")
|
|
185
|
+
lines.append(f" Total blast radius : {_C.CYAN}{len(total_blast)} symbols across {len(blast_files)} files{_C.RESET}")
|
|
186
|
+
lines.append(f" Total dependencies : {_C.GREEN}{len(total_deps)} symbols{_C.RESET}")
|
|
187
|
+
lines.append(f"{_C.BOLD}{_C.WHITE}{'═' * 70}{_C.RESET}")
|
|
188
|
+
lines.append("")
|
|
189
|
+
|
|
190
|
+
return "\n".join(lines)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def render_verification(
|
|
194
|
+
graph: Dict[str, List[str]],
|
|
195
|
+
changed_symbols: List[str],
|
|
196
|
+
symbols: dict,
|
|
197
|
+
) -> str:
|
|
198
|
+
"""
|
|
199
|
+
Render verification proof: for each edge in the blast radius,
|
|
200
|
+
show the actual code line that creates the dependency.
|
|
201
|
+
"""
|
|
202
|
+
reverse: Dict[str, Set[str]] = {}
|
|
203
|
+
for caller, callees in graph.items():
|
|
204
|
+
for callee in callees:
|
|
205
|
+
reverse.setdefault(callee, set()).add(caller)
|
|
206
|
+
|
|
207
|
+
lines: List[str] = []
|
|
208
|
+
lines.append("")
|
|
209
|
+
lines.append(f"{_C.BOLD}{_C.WHITE}{'═' * 70}{_C.RESET}")
|
|
210
|
+
lines.append(f"{_C.BOLD}{_C.WHITE} VERIFICATION: Proof of each connection{_C.RESET}")
|
|
211
|
+
lines.append(f"{_C.BOLD}{_C.WHITE}{'═' * 70}{_C.RESET}")
|
|
212
|
+
|
|
213
|
+
for sym_id in changed_symbols:
|
|
214
|
+
lines.append("")
|
|
215
|
+
lines.append(f" {_C.BOLD}{_C.RED}⚡ {sym_id}{_C.RESET}")
|
|
216
|
+
lines.append("")
|
|
217
|
+
|
|
218
|
+
# Verify each caller
|
|
219
|
+
callers = sorted(reverse.get(sym_id, set()))
|
|
220
|
+
if callers:
|
|
221
|
+
lines.append(f" {_C.BOLD}{_C.YELLOW} Callers (these functions call the changed code):{_C.RESET}")
|
|
222
|
+
for caller in callers:
|
|
223
|
+
lines.append(f" {_C.YELLOW} → {caller}{_C.RESET}")
|
|
224
|
+
proof = _find_proof(caller, sym_id, symbols, graph)
|
|
225
|
+
if proof:
|
|
226
|
+
lines.append(f" {_C.DIM} evidence: {proof}{_C.RESET}")
|
|
227
|
+
else:
|
|
228
|
+
lines.append(f" {_C.DIM} evidence: (edge exists in call graph){_C.RESET}")
|
|
229
|
+
lines.append("")
|
|
230
|
+
|
|
231
|
+
# Verify each callee
|
|
232
|
+
callees = sorted(graph.get(sym_id, []))
|
|
233
|
+
if callees:
|
|
234
|
+
lines.append(f" {_C.BOLD}{_C.GREEN} Callees (the changed code calls these):{_C.RESET}")
|
|
235
|
+
for callee in callees:
|
|
236
|
+
lines.append(f" {_C.GREEN} → {callee}{_C.RESET}")
|
|
237
|
+
proof = _find_proof(sym_id, callee, symbols, graph)
|
|
238
|
+
if proof:
|
|
239
|
+
lines.append(f" {_C.DIM} evidence: {proof}{_C.RESET}")
|
|
240
|
+
else:
|
|
241
|
+
lines.append(f" {_C.DIM} evidence: (edge exists in call graph){_C.RESET}")
|
|
242
|
+
|
|
243
|
+
lines.append("")
|
|
244
|
+
lines.append(f"{_C.BOLD}{_C.WHITE}{'═' * 70}{_C.RESET}")
|
|
245
|
+
lines.append("")
|
|
246
|
+
return "\n".join(lines)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
# Internal helpers
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _get_transitive_callers(
|
|
255
|
+
reverse: Dict[str, Set[str]],
|
|
256
|
+
start: str,
|
|
257
|
+
max_depth: int,
|
|
258
|
+
) -> List[str]:
|
|
259
|
+
"""BFS up the reverse graph to find all transitively affected symbols."""
|
|
260
|
+
visited: Set[str] = {start}
|
|
261
|
+
result: List[str] = []
|
|
262
|
+
frontier = [start]
|
|
263
|
+
depth = 0
|
|
264
|
+
|
|
265
|
+
while frontier and depth < max_depth:
|
|
266
|
+
next_frontier = []
|
|
267
|
+
for node in frontier:
|
|
268
|
+
for caller in reverse.get(node, set()):
|
|
269
|
+
if caller not in visited:
|
|
270
|
+
visited.add(caller)
|
|
271
|
+
result.append(caller)
|
|
272
|
+
next_frontier.append(caller)
|
|
273
|
+
frontier = next_frontier
|
|
274
|
+
depth += 1
|
|
275
|
+
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _get_transitive_callees(
|
|
280
|
+
graph: Dict[str, List[str]],
|
|
281
|
+
start: str,
|
|
282
|
+
max_depth: int,
|
|
283
|
+
) -> List[str]:
|
|
284
|
+
"""BFS down the forward graph to find all dependencies."""
|
|
285
|
+
visited: Set[str] = {start}
|
|
286
|
+
result: List[str] = []
|
|
287
|
+
frontier = [start]
|
|
288
|
+
depth = 0
|
|
289
|
+
|
|
290
|
+
while frontier and depth < max_depth:
|
|
291
|
+
next_frontier = []
|
|
292
|
+
for node in frontier:
|
|
293
|
+
for callee in graph.get(node, []):
|
|
294
|
+
if callee not in visited:
|
|
295
|
+
visited.add(callee)
|
|
296
|
+
result.append(callee)
|
|
297
|
+
next_frontier.append(callee)
|
|
298
|
+
frontier = next_frontier
|
|
299
|
+
depth += 1
|
|
300
|
+
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _find_proof(
|
|
305
|
+
caller_id: str,
|
|
306
|
+
callee_id: str,
|
|
307
|
+
symbols: dict,
|
|
308
|
+
graph: Dict[str, List[str]],
|
|
309
|
+
) -> Optional[str]:
|
|
310
|
+
"""
|
|
311
|
+
Find the actual code line in `caller` that references `callee`.
|
|
312
|
+
|
|
313
|
+
This is the "proof" that the edge is real — a grep through the caller's
|
|
314
|
+
source code for the callee's function name.
|
|
315
|
+
"""
|
|
316
|
+
if caller_id not in symbols:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
caller_sym = symbols[caller_id]
|
|
320
|
+
callee_name = callee_id.split(":")[-1] if ":" in callee_id else callee_id
|
|
321
|
+
|
|
322
|
+
# Strip class prefix for method calls: "ClassName.method" -> "method"
|
|
323
|
+
if "." in callee_name:
|
|
324
|
+
bare_name = callee_name.split(".")[-1]
|
|
325
|
+
else:
|
|
326
|
+
bare_name = callee_name
|
|
327
|
+
|
|
328
|
+
# Search caller's code for the call
|
|
329
|
+
for i, line in enumerate(caller_sym.code.splitlines(), start=1):
|
|
330
|
+
stripped = line.strip()
|
|
331
|
+
# Look for function/method call pattern
|
|
332
|
+
if bare_name + "(" in stripped or f".{bare_name}(" in stripped:
|
|
333
|
+
# Truncate long lines
|
|
334
|
+
if len(stripped) > 80:
|
|
335
|
+
stripped = stripped[:77] + "..."
|
|
336
|
+
return f"line {caller_sym.lineno + i - 1}: {stripped}"
|
|
337
|
+
|
|
338
|
+
return None
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
languages/ — Optional per-language adapters beyond the built-in Python
|
|
3
|
+
support.
|
|
4
|
+
|
|
5
|
+
The core pipeline (scoring, selection, compilation, caching, diff mapping)
|
|
6
|
+
is language-agnostic: it operates on `Symbol` records and a symbol-id
|
|
7
|
+
graph. What a language needs to supply is exactly what parser.py,
|
|
8
|
+
resolver.py, and graph_builder.py supply for Python:
|
|
9
|
+
|
|
10
|
+
1. symbols per file (id "./rel/path.ext:Name", code, lineno)
|
|
11
|
+
2. a dependency graph over those symbol ids
|
|
12
|
+
|
|
13
|
+
An adapter provides both. Adapters have runtime dependencies (tree-sitter
|
|
14
|
+
grammars) that the core deliberately does not: they are OPTIONAL extras,
|
|
15
|
+
probed at import time — without them installed, DiffContext behaves
|
|
16
|
+
exactly as the Python-only tool it was, no warnings, no degradation of
|
|
17
|
+
the Python path.
|
|
18
|
+
|
|
19
|
+
Honesty contract: adapter-produced graphs are shallower than the Python
|
|
20
|
+
graph (no attribute-type tracking, no cross-file MRO). Retrieval quality
|
|
21
|
+
for adapter languages is NOT covered by the benchmark numbers in the
|
|
22
|
+
README until measured separately — see the language support table there.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from typing import List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
_adapters: "Optional[List]" = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def available_adapters() -> "List":
|
|
34
|
+
"""
|
|
35
|
+
Adapters whose runtime dependencies are importable, probed once per
|
|
36
|
+
process. An adapter that fails to import (missing extra, grammar ABI
|
|
37
|
+
mismatch) is skipped silently at INFO level — the Python path must
|
|
38
|
+
never degrade because an optional extra is absent or broken.
|
|
39
|
+
"""
|
|
40
|
+
global _adapters
|
|
41
|
+
if _adapters is None:
|
|
42
|
+
_adapters = []
|
|
43
|
+
try:
|
|
44
|
+
from .typescript import TypeScriptAdapter
|
|
45
|
+
_adapters.append(TypeScriptAdapter())
|
|
46
|
+
except Exception as e: # ImportError, or grammar version mismatch
|
|
47
|
+
logger.info("TypeScript adapter unavailable: %s", e)
|
|
48
|
+
return _adapters
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def discover_files(adapter, repo_path: str) -> "List[str]":
|
|
52
|
+
"""All files this adapter should index in repo_path: extension match
|
|
53
|
+
(gitignore-aware) filtered through the adapter's own indexing policy
|
|
54
|
+
(vendored/minified/test-file exclusions)."""
|
|
55
|
+
from ..scanner import find_source_files
|
|
56
|
+
return [
|
|
57
|
+
f for f in find_source_files(repo_path, tuple(adapter.extensions))
|
|
58
|
+
if adapter.should_index(f)
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def adapter_for_path(path: str):
|
|
63
|
+
"""The adapter that handles this file's extension, or None."""
|
|
64
|
+
for adapter in available_adapters():
|
|
65
|
+
if path.endswith(adapter.extensions):
|
|
66
|
+
return adapter
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def indexable_extensions() -> "Tuple[str, ...]":
|
|
71
|
+
"""
|
|
72
|
+
Every file extension the current environment can index: Python always,
|
|
73
|
+
plus each available adapter's extensions. Used by file discovery and
|
|
74
|
+
git-diff filtering so a changed file is only ever reported when its
|
|
75
|
+
symbols can actually exist in the index.
|
|
76
|
+
"""
|
|
77
|
+
exts: "Tuple[str, ...]" = (".py",)
|
|
78
|
+
for adapter in available_adapters():
|
|
79
|
+
exts = exts + tuple(adapter.extensions)
|
|
80
|
+
return exts
|