codedna-analyzer 1.4.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.
- codedna/__init__.py +3 -0
- codedna/analyzers/__init__.py +1 -0
- codedna/analyzers/ai_analyzer.py +111 -0
- codedna/analyzers/architecture_detector.py +150 -0
- codedna/analyzers/cache_manager.py +70 -0
- codedna/analyzers/code_smell_detector.py +302 -0
- codedna/analyzers/complexity_analyzer.py +329 -0
- codedna/analyzers/dependency_mapper.py +171 -0
- codedna/analyzers/developer_analyzer.py +148 -0
- codedna/analyzers/diff_engine.py +395 -0
- codedna/analyzers/dna_generator.py +308 -0
- codedna/analyzers/evolution_engine.py +193 -0
- codedna/analyzers/github_analyzer.py +86 -0
- codedna/analyzers/language_detector.py +151 -0
- codedna/analyzers/repo_cloner.py +91 -0
- codedna/analyzers/security_detector.py +271 -0
- codedna/analyzers/structure_analyzer.py +91 -0
- codedna/ci/__init__.py +5 -0
- codedna/ci/gatekeeper.py +421 -0
- codedna/cli.py +839 -0
- codedna/compliance/__init__.py +5 -0
- codedna/compliance/sbom.py +410 -0
- codedna/config.py +167 -0
- codedna/mcp/__init__.py +5 -0
- codedna/mcp/protocol.py +65 -0
- codedna/mcp/server.py +393 -0
- codedna/remediation/__init__.py +5 -0
- codedna/remediation/planner.py +244 -0
- codedna/visualization/__init__.py +1 -0
- codedna/visualization/html_export.py +507 -0
- codedna/visualization/renderer.py +373 -0
- codedna_analyzer-1.4.0.dist-info/METADATA +425 -0
- codedna_analyzer-1.4.0.dist-info/RECORD +37 -0
- codedna_analyzer-1.4.0.dist-info/WHEEL +5 -0
- codedna_analyzer-1.4.0.dist-info/entry_points.txt +2 -0
- codedna_analyzer-1.4.0.dist-info/licenses/LICENSE +21 -0
- codedna_analyzer-1.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Visualization Renderer — generates Rich terminal output and Mermaid diagrams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Renderer:
|
|
10
|
+
"""Renders analysis results using Rich for beautiful terminal output."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, console=None):
|
|
13
|
+
"""Initialize the Renderer.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
console: Optional rich Console instance. If None, it will be lazily
|
|
17
|
+
imported and instantiated to reduce module load time.
|
|
18
|
+
"""
|
|
19
|
+
if console is None:
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
self.console = Console()
|
|
22
|
+
else:
|
|
23
|
+
self.console = console
|
|
24
|
+
|
|
25
|
+
def render_dna_profile(self, profile: dict) -> None:
|
|
26
|
+
"""Render the full DNA profile to the terminal."""
|
|
27
|
+
self._render_header(profile)
|
|
28
|
+
self._render_languages(profile)
|
|
29
|
+
self._render_architecture(profile)
|
|
30
|
+
self._render_health(profile)
|
|
31
|
+
self._render_dependencies(profile)
|
|
32
|
+
self._render_developers(profile)
|
|
33
|
+
self._render_evolution(profile)
|
|
34
|
+
self._render_signature(profile)
|
|
35
|
+
|
|
36
|
+
def render_diff(self, diff: dict) -> None:
|
|
37
|
+
"""Render a comparative genetic drift report to the terminal."""
|
|
38
|
+
meta = diff.get("metadata", {})
|
|
39
|
+
source_a = meta.get("source_a", "Baseline (A)")
|
|
40
|
+
source_b = meta.get("source_b", "Target (B)")
|
|
41
|
+
sim = diff.get("similarity_score", 0.0)
|
|
42
|
+
verdict = diff.get("mutation_verdict", "Unknown")
|
|
43
|
+
|
|
44
|
+
sim_color = "green" if sim >= 80 else ("yellow" if sim >= 60 else "red")
|
|
45
|
+
bar_len = int(sim / 5)
|
|
46
|
+
bar = "█" * bar_len + "░" * (20 - bar_len)
|
|
47
|
+
|
|
48
|
+
self.console.print()
|
|
49
|
+
self.console.print(Panel(
|
|
50
|
+
f"[bold cyan]🧬 CodeDNA Genetic Drift & Mutation Diff[/]\n\n"
|
|
51
|
+
f"[white]Baseline (A):[/] [dim]{source_a}[/]\n"
|
|
52
|
+
f"[white]Target (B):[/] [dim]{source_b}[/]\n\n"
|
|
53
|
+
f"[white]Genetic Similarity:[/] [bold {sim_color}]{sim}%[/] [{sim_color}]{bar}[/]\n"
|
|
54
|
+
f"[white]Mutation Verdict:[/] [bold cyan]{verdict}[/]",
|
|
55
|
+
title="🧬 Architecture Diff",
|
|
56
|
+
border_style="cyan",
|
|
57
|
+
padding=(1, 2),
|
|
58
|
+
))
|
|
59
|
+
|
|
60
|
+
# Comparison Table
|
|
61
|
+
table = Table(title="📊 Architectural & Structural Drift", border_style="dim")
|
|
62
|
+
table.add_column("Dimension", style="bold")
|
|
63
|
+
table.add_column("Baseline (A)", justify="center")
|
|
64
|
+
table.add_column("Target (B)", justify="center")
|
|
65
|
+
table.add_column("Drift / Delta", justify="left")
|
|
66
|
+
|
|
67
|
+
# Architecture Pattern
|
|
68
|
+
arch = diff.get("architecture_shift", {})
|
|
69
|
+
pat_a = arch.get("from_pattern", "?")
|
|
70
|
+
pat_b = arch.get("to_pattern", "?")
|
|
71
|
+
pat_delta = "[dim]Unchanged[/]" if not arch.get("pattern_changed") else f"[yellow]{pat_a} ➔ {pat_b}[/]"
|
|
72
|
+
table.add_row("Architecture", pat_a, pat_b, pat_delta)
|
|
73
|
+
|
|
74
|
+
# Health
|
|
75
|
+
health = diff.get("health_shift", {})
|
|
76
|
+
h_a = health.get("from_health", "?")
|
|
77
|
+
h_b = health.get("to_health", "?")
|
|
78
|
+
h_status = health.get("status", "unchanged")
|
|
79
|
+
h_color = "green" if h_status == "improved" else ("red" if h_status == "degraded" else "dim")
|
|
80
|
+
h_delta = f"[{h_color}]{h_status.capitalize()}[/] [dim](crit: {health.get('critical_smells_delta', 0):+d}, warn: {health.get('warning_smells_delta', 0):+d})[/]" # noqa: E501
|
|
81
|
+
table.add_row("Health Score", h_a, h_b, h_delta)
|
|
82
|
+
|
|
83
|
+
# Files & Modules
|
|
84
|
+
struct = diff.get("structure_delta", {})
|
|
85
|
+
files_delta = struct.get("files", 0)
|
|
86
|
+
f_color = "green" if files_delta >= 0 else "yellow"
|
|
87
|
+
table.add_row("Files Count", "-", "-", f"[{f_color}]{files_delta:+d} files[/]")
|
|
88
|
+
|
|
89
|
+
modules_delta = struct.get("modules", 0)
|
|
90
|
+
table.add_row("Modules Count", "-", "-", f"{modules_delta:+d} modules")
|
|
91
|
+
|
|
92
|
+
# Circular Dependencies
|
|
93
|
+
dep = diff.get("dependency_shift", {})
|
|
94
|
+
circ = dep.get("circular_deps", {})
|
|
95
|
+
c_from = "Yes ⚠️" if circ.get("from") else "None ✅"
|
|
96
|
+
c_to = "Yes ⚠️" if circ.get("to") else "None ✅"
|
|
97
|
+
c_status = circ.get("status", "clean")
|
|
98
|
+
c_color = "red" if c_status == "introduced" else ("green" if c_status == "resolved" else "dim")
|
|
99
|
+
table.add_row("Circular Deps", c_from, c_to, f"[{c_color}]{c_status.capitalize()}[/]")
|
|
100
|
+
|
|
101
|
+
# Connections
|
|
102
|
+
edges_delta = dep.get("edges_delta", 0)
|
|
103
|
+
table.add_row("Dependency Edges", "-", "-", f"{edges_delta:+d} connections")
|
|
104
|
+
|
|
105
|
+
# Bus Factor
|
|
106
|
+
dev = diff.get("developer_shift", {})
|
|
107
|
+
bus_delta = dev.get("bus_factor_delta", 0)
|
|
108
|
+
table.add_row("Bus Factor", "-", "-", f"{bus_delta:+d}")
|
|
109
|
+
|
|
110
|
+
self.console.print(table)
|
|
111
|
+
|
|
112
|
+
# Language shifts
|
|
113
|
+
lang_deltas = diff.get("language_deltas", {})
|
|
114
|
+
active_langs = {k: v for k, v in lang_deltas.items() if v.get("pct_a", 0) > 0 or v.get("pct_b", 0) > 0}
|
|
115
|
+
if active_langs:
|
|
116
|
+
lang_table = Table(title="🌐 Language Composition Drift", border_style="dim")
|
|
117
|
+
lang_table.add_column("Language", style="bold")
|
|
118
|
+
lang_table.add_column("Baseline (A)", justify="right")
|
|
119
|
+
lang_table.add_column("Target (B)", justify="right")
|
|
120
|
+
lang_table.add_column("Shift", justify="right")
|
|
121
|
+
|
|
122
|
+
for lang, ldata in active_langs.items():
|
|
123
|
+
delta = ldata.get("delta", 0.0)
|
|
124
|
+
d_color = "green" if delta > 0 else ("red" if delta < 0 else "dim")
|
|
125
|
+
lang_table.add_row(
|
|
126
|
+
lang,
|
|
127
|
+
f"{ldata.get('pct_a', 0.0):.1f}%",
|
|
128
|
+
f"{ldata.get('pct_b', 0.0):.1f}%",
|
|
129
|
+
f"[{d_color}]{delta:+0.1f}%[/]",
|
|
130
|
+
)
|
|
131
|
+
self.console.print(lang_table)
|
|
132
|
+
self.console.print()
|
|
133
|
+
|
|
134
|
+
def render_gatekeeper(self, result) -> None:
|
|
135
|
+
"""Render CI gatekeeper evaluation to the terminal."""
|
|
136
|
+
self.console.print()
|
|
137
|
+
status_color = "green" if result.passed else "red"
|
|
138
|
+
status_text = "PASSED" if result.passed else "FAILED"
|
|
139
|
+
|
|
140
|
+
self.console.print(Panel(
|
|
141
|
+
f"[bold {status_color}]🛡️ CodeDNA Gatekeeper — {status_text}[/]\n\n"
|
|
142
|
+
f"[white]Overall Health:[/] [bold]{result.overall_health}[/]\n"
|
|
143
|
+
f"[white]Policies Evaluated:[/] {result.policies_evaluated} | "
|
|
144
|
+
f"[green]Passed:[/] {result.policies_passed} | "
|
|
145
|
+
f"[red]Failed:[/] {result.policies_failed}",
|
|
146
|
+
title="CI Gatekeeper Evaluation",
|
|
147
|
+
border_style=status_color,
|
|
148
|
+
padding=(1, 2),
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
table = Table(title="📋 Policy Evaluations", border_style="dim")
|
|
152
|
+
table.add_column("Status", justify="center", width=8)
|
|
153
|
+
table.add_column("Rule ID", style="cyan", width=13)
|
|
154
|
+
table.add_column("Policy", style="bold")
|
|
155
|
+
table.add_column("Severity", justify="center", width=10)
|
|
156
|
+
table.add_column("Message")
|
|
157
|
+
|
|
158
|
+
for eval_item in result.evaluations:
|
|
159
|
+
mark = "[bold green]PASS[/]" if eval_item.passed else "[bold red]FAIL[/]"
|
|
160
|
+
sev_color = "red" if eval_item.severity == "error" else "yellow"
|
|
161
|
+
sev_text = f"[{sev_color}]{eval_item.severity.upper()}[/]"
|
|
162
|
+
table.add_row(mark, eval_item.rule_id, eval_item.name, sev_text, eval_item.message)
|
|
163
|
+
|
|
164
|
+
self.console.print(table)
|
|
165
|
+
|
|
166
|
+
if result.passed:
|
|
167
|
+
self.console.print("\n[bold green]✨ All architecture and quality gates passed successfully![/]\n")
|
|
168
|
+
else:
|
|
169
|
+
self.console.print(f"\n[bold red]❌ Quality gate breach: {result.policies_failed} policy check(s) failed.[/]\n")
|
|
170
|
+
|
|
171
|
+
def render_drift_gate(self, passed: bool, reasons: list[str], diff: dict) -> None:
|
|
172
|
+
"""Render architectural drift regression gatekeeper evaluation to the terminal."""
|
|
173
|
+
self.console.print()
|
|
174
|
+
status_color = "green" if passed else "red"
|
|
175
|
+
status_text = "PASSED" if passed else "FAILED"
|
|
176
|
+
sim = diff.get("similarity_score", 0.0)
|
|
177
|
+
verdict = diff.get("mutation_verdict", "Unknown")
|
|
178
|
+
|
|
179
|
+
self.console.print(Panel(
|
|
180
|
+
f"[bold {status_color}]🛡️ CodeDNA Drift Regression Gate — {status_text}[/]\n\n"
|
|
181
|
+
f"[white]Genetic Similarity:[/] [bold]{sim}%[/]\n"
|
|
182
|
+
f"[white]Mutation Verdict:[/] [bold cyan]{verdict}[/]\n"
|
|
183
|
+
f"[white]Drift Status:[/] [bold {status_color}]{'PASS (No unacceptable drift)' if passed else 'FAIL (Architectural regression detected)'}[/]",
|
|
184
|
+
title="CI Drift Regression Gate",
|
|
185
|
+
border_style=status_color,
|
|
186
|
+
padding=(1, 2),
|
|
187
|
+
))
|
|
188
|
+
|
|
189
|
+
if not passed and reasons:
|
|
190
|
+
table = Table(title="⚠️ Drift Violations", border_style="red")
|
|
191
|
+
table.add_column("#", justify="center", width=4)
|
|
192
|
+
table.add_column("Violation Detail", style="bold red")
|
|
193
|
+
|
|
194
|
+
for idx, reason in enumerate(reasons, 1):
|
|
195
|
+
table.add_row(str(idx), reason)
|
|
196
|
+
|
|
197
|
+
self.console.print(table)
|
|
198
|
+
self.console.print(f"\n[bold red]❌ Drift gate breach: {len(reasons)} regression check(s) failed.[/]\n")
|
|
199
|
+
else:
|
|
200
|
+
self.console.print("\n[bold green]✨ Architectural drift within acceptable parameters. Gate passed![/]\n")
|
|
201
|
+
|
|
202
|
+
def _render_header(self, profile: dict) -> None:
|
|
203
|
+
"""Render the DNA header."""
|
|
204
|
+
self.console.print()
|
|
205
|
+
self.console.print(Panel(
|
|
206
|
+
f"[bold cyan]🧬 CodeDNA Profile[/]\n\n"
|
|
207
|
+
f"[white]Source:[/] [dim]{profile['metadata']['source']}[/]\n"
|
|
208
|
+
f"[white]System:[/] [bold green]{profile['system_type']}[/]\n"
|
|
209
|
+
f"[white]Date:[/] [dim]{profile['metadata']['analyzed_at'][:10]}[/]",
|
|
210
|
+
border_style="cyan",
|
|
211
|
+
padding=(1, 2),
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
def _render_languages(self, profile: dict) -> None:
|
|
215
|
+
"""Render language distribution."""
|
|
216
|
+
lang_data = profile.get("languages", {}).get("languages", {})
|
|
217
|
+
if not lang_data:
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
table = Table(title="📊 Language Distribution", border_style="dim")
|
|
221
|
+
table.add_column("Language", style="bold")
|
|
222
|
+
table.add_column("Files", justify="right")
|
|
223
|
+
table.add_column("Lines", justify="right")
|
|
224
|
+
table.add_column("Share", justify="right")
|
|
225
|
+
table.add_column("", min_width=20)
|
|
226
|
+
|
|
227
|
+
for lang, data in lang_data.items():
|
|
228
|
+
bar_len = max(1, int(data["percentage"] / 5))
|
|
229
|
+
bar = "█" * bar_len + "░" * (20 - bar_len)
|
|
230
|
+
color = self._lang_color(lang)
|
|
231
|
+
|
|
232
|
+
lines_val = data['lines']
|
|
233
|
+
if isinstance(lines_val, str):
|
|
234
|
+
try:
|
|
235
|
+
lines_val = float(lines_val)
|
|
236
|
+
if lines_val.is_integer():
|
|
237
|
+
lines_val = int(lines_val)
|
|
238
|
+
except ValueError:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
lines_str = f"{lines_val:,}" if isinstance(lines_val, (int, float)) else str(lines_val)
|
|
242
|
+
|
|
243
|
+
table.add_row(
|
|
244
|
+
lang,
|
|
245
|
+
str(data["files"]),
|
|
246
|
+
lines_str,
|
|
247
|
+
f"{data['percentage']}%",
|
|
248
|
+
f"[{color}]{bar}[/]",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
self.console.print(table)
|
|
252
|
+
|
|
253
|
+
def _render_architecture(self, profile: dict) -> None:
|
|
254
|
+
"""Render architecture analysis."""
|
|
255
|
+
arch = profile.get("architecture", {})
|
|
256
|
+
patterns = arch.get("detected_patterns", [])
|
|
257
|
+
traits = arch.get("traits", [])
|
|
258
|
+
|
|
259
|
+
if not patterns and not traits:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
lines = []
|
|
263
|
+
if patterns:
|
|
264
|
+
lines.append("[bold]Detected Patterns:[/]")
|
|
265
|
+
for p in patterns:
|
|
266
|
+
conf = int(p["confidence"] * 100)
|
|
267
|
+
bar = "█" * (conf // 10)
|
|
268
|
+
lines.append(f" • {p['pattern']} [dim]({conf}%)[/] [green]{bar}[/]")
|
|
269
|
+
|
|
270
|
+
if traits:
|
|
271
|
+
lines.append("\n[bold]Infrastructure Traits:[/]")
|
|
272
|
+
for t in traits:
|
|
273
|
+
lines.append(f" ✅ {t}")
|
|
274
|
+
|
|
275
|
+
lines.append(f"\n[bold]Coupling:[/] {arch.get('coupling', 'Unknown')}")
|
|
276
|
+
|
|
277
|
+
self.console.print(Panel(
|
|
278
|
+
"\n".join(lines),
|
|
279
|
+
title="🏗️ Architecture",
|
|
280
|
+
border_style="blue",
|
|
281
|
+
))
|
|
282
|
+
|
|
283
|
+
def _render_health(self, profile: dict) -> None:
|
|
284
|
+
"""Render health assessment."""
|
|
285
|
+
health = profile.get("health", {})
|
|
286
|
+
overall = health.get("overall", "Unknown")
|
|
287
|
+
counts = health.get("severity_counts", {})
|
|
288
|
+
|
|
289
|
+
color = {"Healthy": "green", "Fair": "yellow", "Needs Attention": "dark_orange", "Critical": "red"}.get(overall, "white") # noqa: E501
|
|
290
|
+
|
|
291
|
+
risks = profile.get("risks", [])
|
|
292
|
+
risk_text = "\n".join(risks[:6]) if risks else " No critical risks detected ✅"
|
|
293
|
+
|
|
294
|
+
self.console.print(Panel(
|
|
295
|
+
f"[bold {color}]Overall: {overall}[/]\n\n"
|
|
296
|
+
f"🔴 Critical: {counts.get('critical', 0)} "
|
|
297
|
+
f"🟡 Warning: {counts.get('warning', 0)} "
|
|
298
|
+
f"🔵 Info: {counts.get('info', 0)}\n\n"
|
|
299
|
+
f"[bold]Risk Signals:[/]\n{risk_text}",
|
|
300
|
+
title="🩺 Health",
|
|
301
|
+
border_style=color,
|
|
302
|
+
))
|
|
303
|
+
|
|
304
|
+
def _render_dependencies(self, profile: dict) -> None:
|
|
305
|
+
"""Render dependency graph stats."""
|
|
306
|
+
deps = profile.get("dependencies", {})
|
|
307
|
+
circular = "Yes ⚠️" if deps.get("has_circular_deps") else "None ✅"
|
|
308
|
+
|
|
309
|
+
self.console.print(Panel(
|
|
310
|
+
f"Modules: [bold]{deps.get('total_modules', 0)}[/]\n"
|
|
311
|
+
f"Connections: [bold]{deps.get('total_edges', 0)}[/]\n"
|
|
312
|
+
f"Density: [bold]{deps.get('density', 0)}[/]\n"
|
|
313
|
+
f"Circular Deps: [bold]{circular}[/]",
|
|
314
|
+
title="🔗 Dependency Graph",
|
|
315
|
+
border_style="magenta",
|
|
316
|
+
))
|
|
317
|
+
|
|
318
|
+
def _render_developers(self, profile: dict) -> None:
|
|
319
|
+
"""Render developer genome."""
|
|
320
|
+
dev = profile.get("developer_genome", {})
|
|
321
|
+
contribs = dev.get("top_contributors", [])
|
|
322
|
+
|
|
323
|
+
if not contribs:
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
table = Table(title="👥 Developer Genome", border_style="dim")
|
|
327
|
+
table.add_column("Developer", style="bold")
|
|
328
|
+
table.add_column("Role", style="cyan")
|
|
329
|
+
table.add_column("Commits", justify="right")
|
|
330
|
+
|
|
331
|
+
for c in contribs:
|
|
332
|
+
table.add_row(c["name"], c["role"], str(c["commits"]))
|
|
333
|
+
|
|
334
|
+
self.console.print(table)
|
|
335
|
+
self.console.print(f" Bus Factor: [bold]{dev.get('bus_factor', '?')}[/]")
|
|
336
|
+
|
|
337
|
+
def _render_evolution(self, profile: dict) -> None:
|
|
338
|
+
"""Render evolution timeline."""
|
|
339
|
+
evo = profile.get("evolution", {})
|
|
340
|
+
patterns = evo.get("patterns", [])
|
|
341
|
+
|
|
342
|
+
self.console.print(Panel(
|
|
343
|
+
f"Total Commits: [bold]{evo.get('total_commits', 0)}[/]\n"
|
|
344
|
+
f"First Commit: [dim]{(evo.get('first_commit') or '?')[:10]}[/]\n"
|
|
345
|
+
f"Patterns: [bold cyan]{', '.join(patterns) if patterns else 'Unknown'}[/]",
|
|
346
|
+
title="📈 Evolution",
|
|
347
|
+
border_style="green",
|
|
348
|
+
))
|
|
349
|
+
|
|
350
|
+
def _render_signature(self, profile: dict) -> None:
|
|
351
|
+
"""Render the DNA signature."""
|
|
352
|
+
sig = profile.get("signature", "")
|
|
353
|
+
self.console.print(Panel(
|
|
354
|
+
f"[bold cyan]{sig}[/]",
|
|
355
|
+
title="🧬 DNA Signature",
|
|
356
|
+
border_style="bright_cyan",
|
|
357
|
+
padding=(1, 2),
|
|
358
|
+
))
|
|
359
|
+
self.console.print()
|
|
360
|
+
|
|
361
|
+
def _lang_color(self, lang: str) -> str:
|
|
362
|
+
colors = {
|
|
363
|
+
"Python": "yellow",
|
|
364
|
+
"JavaScript": "bright_yellow",
|
|
365
|
+
"TypeScript": "blue",
|
|
366
|
+
"Go": "cyan",
|
|
367
|
+
"Rust": "red",
|
|
368
|
+
"Java": "bright_red",
|
|
369
|
+
"C#": "magenta",
|
|
370
|
+
"Ruby": "red",
|
|
371
|
+
"PHP": "blue",
|
|
372
|
+
}
|
|
373
|
+
return colors.get(lang, "white")
|