codevanta 0.1.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.
Files changed (65) hide show
  1. codevanta/__init__.py +1 -0
  2. codevanta/analysis/__init__.py +1 -0
  3. codevanta/analysis/binary_diff.py +1168 -0
  4. codevanta/analysis/callgraph.py +542 -0
  5. codevanta/analysis/cfg.py +1201 -0
  6. codevanta/analysis/dataflow.py +762 -0
  7. codevanta/analysis/debugger.py +289 -0
  8. codevanta/analysis/debuginfo.py +304 -0
  9. codevanta/analysis/decompile.py +1094 -0
  10. codevanta/analysis/decompilers/__init__.py +250 -0
  11. codevanta/analysis/decompilers/angr_backend.py +251 -0
  12. codevanta/analysis/decompilers/base.py +278 -0
  13. codevanta/analysis/decompilers/ghidra_backend.py +316 -0
  14. codevanta/analysis/dynamic.py +756 -0
  15. codevanta/analysis/emulate.py +1349 -0
  16. codevanta/analysis/evidence_bundle.py +736 -0
  17. codevanta/analysis/function_hash.py +562 -0
  18. codevanta/analysis/graph_export.py +441 -0
  19. codevanta/analysis/html_view.py +695 -0
  20. codevanta/analysis/jump_tables.py +546 -0
  21. codevanta/analysis/solver.py +715 -0
  22. codevanta/analysis/symbolic.py +1809 -0
  23. codevanta/analysis/trace.py +646 -0
  24. codevanta/analysis/type_propagation.py +1055 -0
  25. codevanta/analysis/type_recovery.py +1214 -0
  26. codevanta/analysis/vtables.py +572 -0
  27. codevanta/analysis/xrefs.py +434 -0
  28. codevanta/api/__init__.py +1 -0
  29. codevanta/cli/__init__.py +1 -0
  30. codevanta/cli/main.py +3038 -0
  31. codevanta/core/__init__.py +31 -0
  32. codevanta/core/disassembler.py +121 -0
  33. codevanta/core/entropy.py +237 -0
  34. codevanta/core/functions.py +176 -0
  35. codevanta/core/loader.py +1351 -0
  36. codevanta/core/report.py +306 -0
  37. codevanta/core/strings.py +109 -0
  38. codevanta/evidence/__init__.py +7 -0
  39. codevanta/evidence/confidence.py +36 -0
  40. codevanta/evidence/models.py +43 -0
  41. codevanta/evidence/provenance.py +37 -0
  42. codevanta/gui/__init__.py +46 -0
  43. codevanta/gui/layout.py +1384 -0
  44. codevanta/gui/server.py +1305 -0
  45. codevanta/ir/__init__.py +6 -0
  46. codevanta/ir/adapters/__init__.py +31 -0
  47. codevanta/ir/base.py +31 -0
  48. codevanta/ir/instruction.py +42 -0
  49. codevanta/mcp_server/__init__.py +5 -0
  50. codevanta/mcp_server/server.py +710 -0
  51. codevanta/mcp_server/tools.py +1364 -0
  52. codevanta/plugins/__init__.py +748 -0
  53. codevanta/setup/__init__.py +28 -0
  54. codevanta/setup/apply.py +22 -0
  55. codevanta/setup/clients.py +285 -0
  56. codevanta/setup/plan.py +79 -0
  57. codevanta/setup/verify.py +43 -0
  58. codevanta/storage/__init__.py +1 -0
  59. codevanta/storage/session.py +514 -0
  60. codevanta-0.1.0.dist-info/METADATA +688 -0
  61. codevanta-0.1.0.dist-info/RECORD +65 -0
  62. codevanta-0.1.0.dist-info/WHEEL +5 -0
  63. codevanta-0.1.0.dist-info/entry_points.txt +2 -0
  64. codevanta-0.1.0.dist-info/licenses/LICENSE +21 -0
  65. codevanta-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,250 @@
1
+ """Pluggable decompiler providers: real decompilers when installed, native always.
2
+
3
+ CODEVANTA can render one function with three different providers, and picks one
4
+ without ever assuming a third-party package is present:
5
+
6
+ =========== ============================================== =====================
7
+ backend runs when output
8
+ =========== ============================================== =====================
9
+ ``ghidra`` ``pyghidra`` imports **and** ``GHIDRA_INSTALL_DIR`` Ghidra's C
10
+ points at a Ghidra installation
11
+ ``angr`` ``angr`` imports angr's C
12
+ ``native`` always heuristic pseudo-C
13
+ =========== ============================================== =====================
14
+
15
+ Only the last one is a CODEVANTA dependency: the optional backends import their
16
+ heavy module lazily inside ``availability()`` / ``decompile()``, so this package
17
+ imports and answers :func:`available_backends` on a machine with neither angr nor
18
+ Ghidra, and never raises ``ImportError`` for a missing decompiler.
19
+
20
+ Public API
21
+ ----------
22
+ * :class:`BackendAvailability` -- ``name``, ``available``, a non-empty ``reason``
23
+ and the ``requires`` tuple for each backend, in the order ``ghidra``, ``angr``,
24
+ ``native``.
25
+ * :class:`BackendResult` -- ``text``, ``backend``, ``language``, ``warnings`` and
26
+ :class:`Evidence`, with a JSON-safe :meth:`BackendResult.to_dict`.
27
+ * :class:`DecompilerBackend` -- the protocol a provider implements.
28
+ * :func:`available_backends` -- probe all three, in order.
29
+ * :func:`get_backend` -- look one up by name; an unknown name raises
30
+ :class:`AnalysisError` listing the supported set.
31
+ * :func:`decompile_function` -- the one entry point callers need.
32
+
33
+ Selection policy
34
+ ----------------
35
+ ``backend="native"`` always works. ``backend="angr"`` / ``"ghidra"`` are strict:
36
+ when the backend is unavailable the call raises :class:`AnalysisError` whose
37
+ message carries the probe's reason (install hint included) -- it never leaks an
38
+ ``ImportError`` and never silently substitutes another provider.
39
+ ``backend="auto"`` (the default) prefers the first available external backend in
40
+ the order ``ghidra``, ``angr`` and falls back to ``native``; whatever it chose,
41
+ the returned :class:`BackendResult` carries one warning per unavailable backend
42
+ naming that backend and its reason, so a fallback is never silent.
43
+
44
+ Budgets are validated once, here (:func:`codevanta.analysis.decompilers.base.
45
+ require_budget`), before any backend is touched: an out-of-range
46
+ ``max_instructions`` / ``max_blocks`` raises regardless of the backend.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ from dataclasses import replace
52
+ from pathlib import Path
53
+
54
+ from codevanta.analysis.decompile import (
55
+ DEFAULT_MAX_BLOCKS,
56
+ DEFAULT_MAX_INSTRUCTIONS,
57
+ MAX_MAX_BLOCKS,
58
+ MAX_MAX_INSTRUCTIONS,
59
+ )
60
+ from codevanta.core.loader import BinaryInfo
61
+ from codevanta.core.report import AnalysisError
62
+
63
+ from .angr_backend import AngrBackend
64
+ from .base import (
65
+ LANGUAGE,
66
+ BackendAvailability,
67
+ BackendResult,
68
+ DecompilerBackend,
69
+ NativeBackend,
70
+ require_budget,
71
+ )
72
+ from .ghidra_backend import GhidraBackend
73
+
74
+ __all__ = [
75
+ "AUTO",
76
+ "LANGUAGE",
77
+ "AngrBackend",
78
+ "BackendAvailability",
79
+ "BackendResult",
80
+ "DecompilerBackend",
81
+ "GhidraBackend",
82
+ "NativeBackend",
83
+ "available_backends",
84
+ "decompile_function",
85
+ "get_backend",
86
+ "require_budget",
87
+ "supported_backends",
88
+ ]
89
+
90
+ #: External backends in preference order for ``backend="auto"``.
91
+ _EXTERNAL_ORDER: tuple[str, ...] = ("ghidra", "angr")
92
+
93
+ #: Every supported backend, in report order (external ones first).
94
+ _BACKEND_ORDER: tuple[str, ...] = (*_EXTERNAL_ORDER, "native")
95
+
96
+ #: Backend instances by name; one shared stateless instance each.
97
+ _BACKENDS: dict[str, DecompilerBackend] = {
98
+ backend.name: backend for backend in (GhidraBackend(), AngrBackend(), NativeBackend())
99
+ }
100
+
101
+ #: Name used by :func:`decompile_function` to mean "pick for me".
102
+ AUTO = "auto"
103
+
104
+
105
+ def supported_backends() -> tuple[str, ...]:
106
+ """Return the supported backend names, in report order."""
107
+ return _BACKEND_ORDER
108
+
109
+
110
+ def _probe(backend: DecompilerBackend) -> BackendAvailability:
111
+ """Run one availability probe, turning a crash into an "unavailable" record."""
112
+ try:
113
+ return backend.availability()
114
+ except Exception as exc: # a broken optional dependency must never break discovery
115
+ return BackendAvailability(
116
+ name=backend.name,
117
+ available=False,
118
+ reason=f"the availability probe itself failed ({type(exc).__name__}: {exc})",
119
+ requires=tuple(backend.requires),
120
+ )
121
+
122
+
123
+ def available_backends() -> tuple[BackendAvailability, ...]:
124
+ """Probe every backend without installing or launching anything.
125
+
126
+ Returns:
127
+ One record per supported backend, always in the order ``ghidra``, ``angr``,
128
+ ``native``. Unavailable records always carry a non-empty reason.
129
+ """
130
+ return tuple(_probe(_BACKENDS[name]) for name in _BACKEND_ORDER)
131
+
132
+
133
+ def get_backend(name: str) -> DecompilerBackend:
134
+ """Return the backend registered under ``name``.
135
+
136
+ Args:
137
+ name: ``ghidra``, ``angr`` or ``native``; matched case-insensitively and
138
+ ignoring surrounding whitespace.
139
+
140
+ Returns:
141
+ The shared backend instance. Availability is *not* checked here; call
142
+ :meth:`DecompilerBackend.availability` or use :func:`decompile_function`.
143
+
144
+ Raises:
145
+ AnalysisError: When the name is unknown; the message lists the supported set.
146
+ """
147
+ key = name.strip().lower() if isinstance(name, str) else ""
148
+ backend = _BACKENDS.get(key)
149
+ if backend is None:
150
+ raise AnalysisError(
151
+ f"unknown decompiler backend {name!r}; supported backends: {', '.join(_BACKEND_ORDER)}"
152
+ )
153
+ return backend
154
+
155
+
156
+ def _unavailable_message(availability: BackendAvailability) -> str:
157
+ """Explain why a strictly requested backend cannot run."""
158
+ return (
159
+ f"decompiler backend {availability.name!r} is unavailable: {availability.reason}; "
160
+ "use backend='auto' to fall back to another provider, or backend='native'"
161
+ )
162
+
163
+
164
+ def decompile_function(
165
+ path: str | Path,
166
+ info: BinaryInfo,
167
+ address: int,
168
+ backend: str = AUTO,
169
+ max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
170
+ max_blocks: int = DEFAULT_MAX_BLOCKS,
171
+ ) -> BackendResult:
172
+ """Decompile one function with the requested backend.
173
+
174
+ Args:
175
+ path: Binary the function lives in (opened read-only; never executed).
176
+ info: Loaded metadata for ``path``.
177
+ address: Virtual address of the function.
178
+ backend: ``"auto"`` (default), ``"native"``, ``"angr"`` or ``"ghidra"``.
179
+ max_instructions: Instruction budget, validated against
180
+ :data:`~codevanta.analysis.decompile.MAX_MAX_INSTRUCTIONS`.
181
+ max_blocks: Block budget, validated against
182
+ :data:`~codevanta.analysis.decompile.MAX_MAX_BLOCKS`.
183
+
184
+ Returns:
185
+ The :class:`BackendResult` of whichever backend produced the text.
186
+
187
+ Raises:
188
+ AnalysisError: On an unknown backend name, on an out-of-range budget, when
189
+ a strictly requested backend is unavailable (the message carries the
190
+ reason), or when the backend itself rejects the request. A missing
191
+ optional dependency never surfaces as ``ImportError``.
192
+ """
193
+ instruction_budget = require_budget("max_instructions", max_instructions, MAX_MAX_INSTRUCTIONS)
194
+ block_budget = require_budget("max_blocks", max_blocks, MAX_MAX_BLOCKS)
195
+ if not isinstance(backend, str):
196
+ raise AnalysisError(f"backend must be a string, not {type(backend).__name__}")
197
+ key = backend.strip().lower() or AUTO
198
+ if key == AUTO:
199
+ return _decompile_auto(path, info, address, instruction_budget, block_budget)
200
+ chosen = get_backend(key)
201
+ availability = _probe(chosen)
202
+ if not availability.available:
203
+ raise AnalysisError(_unavailable_message(availability))
204
+ return chosen.decompile(
205
+ path,
206
+ info,
207
+ address,
208
+ max_instructions=instruction_budget,
209
+ max_blocks=block_budget,
210
+ )
211
+
212
+
213
+ def _decompile_auto(
214
+ path: str | Path,
215
+ info: BinaryInfo,
216
+ address: int,
217
+ max_instructions: int,
218
+ max_blocks: int,
219
+ ) -> BackendResult:
220
+ """Pick the best available provider and record every fallback in ``warnings``."""
221
+ probes = tuple(_probe(_BACKENDS[name]) for name in _EXTERNAL_ORDER)
222
+ notes = [
223
+ f"auto: backend {probe.name!r} was unavailable and skipped: {probe.reason}"
224
+ for probe in probes
225
+ if not probe.available
226
+ ]
227
+ for probe in probes:
228
+ if not probe.available:
229
+ continue
230
+ try:
231
+ result = _BACKENDS[probe.name].decompile(
232
+ path,
233
+ info,
234
+ address,
235
+ max_instructions=max_instructions,
236
+ max_blocks=max_blocks,
237
+ )
238
+ except AnalysisError as exc:
239
+ notes.append(f"auto: backend {probe.name!r} failed and was skipped: {exc}")
240
+ continue
241
+ return replace(result, warnings=(*notes, *result.warnings))
242
+
243
+ result = _BACKENDS["native"].decompile(
244
+ path,
245
+ info,
246
+ address,
247
+ max_instructions=max_instructions,
248
+ max_blocks=max_blocks,
249
+ )
250
+ return replace(result, warnings=(*notes, *result.warnings))
@@ -0,0 +1,251 @@
1
+ """angr decompiler backend (optional dependency, imported lazily).
2
+
3
+ ``angr`` ships a decompiler analysis on top of its CFG recovery, so it is the
4
+ cheapest way to get real pseudo-C without an external process. The package is
5
+ *not* a hard requirement of CODEVANTA: nothing here is imported until
6
+ :meth:`AngrBackend.availability` or :meth:`AngrBackend.decompile` runs, so
7
+ ``import codevanta.analysis.decompilers`` never fails on a machine without angr.
8
+
9
+ Pipeline: ``angr.Project(path)`` (no shared libraries, no DWARF) -> ``CFGFast``
10
+ -> ``project.analyses.Decompiler(function, cfg=cfg.model)`` -> ``codegen.text``.
11
+
12
+ Budgeting: angr exposes no instruction or block budget on ``CFGFast`` and none on
13
+ ``Decompiler``. The work is bounded *indirectly* -- the CFG starts from the one
14
+ requested address and indirect-jump resolution is disabled -- and the resulting
15
+ block count is checked against ``max_blocks`` afterwards, which can only be
16
+ reported as a warning. ``max_instructions`` cannot be enforced at all; both
17
+ budgets are recorded in the evidence so a reader can see what was asked for.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from codevanta.analysis.decompile import (
26
+ DEFAULT_MAX_BLOCKS,
27
+ DEFAULT_MAX_INSTRUCTIONS,
28
+ MAX_MAX_BLOCKS,
29
+ MAX_MAX_INSTRUCTIONS,
30
+ )
31
+ from codevanta.core.loader import BinaryInfo
32
+ from codevanta.core.report import AnalysisError
33
+ from codevanta.evidence.confidence import Confidence
34
+ from codevanta.evidence.models import Evidence
35
+
36
+ from .base import (
37
+ LANGUAGE,
38
+ BackendAvailability,
39
+ BackendResult,
40
+ require_budget,
41
+ )
42
+
43
+ __all__ = ["AngrBackend", "INSTALL_HINT", "NAME"]
44
+
45
+ #: Backend identifier.
46
+ NAME = "angr"
47
+
48
+ #: What a user has to install for this backend to become available.
49
+ INSTALL_HINT = "pip install angr"
50
+
51
+ #: Evidence source label.
52
+ _SOURCE = "angr.decompiler"
53
+
54
+ #: Evidence method label.
55
+ _METHOD = "analyses.Decompiler"
56
+
57
+ #: Comment emitted when angr's decompiler produced no code.
58
+ _EMPTY_TEXT = "/* angr produced no pseudocode for 0x{address:x} */"
59
+
60
+
61
+ def _unavailable(availability: BackendAvailability) -> AnalysisError:
62
+ """Build the error raised when a caller asked for an unavailable backend."""
63
+ return AnalysisError(
64
+ f"decompiler backend {availability.name!r} is unavailable: {availability.reason}; "
65
+ "use backend='native' or backend='auto' instead"
66
+ )
67
+
68
+
69
+ class AngrBackend:
70
+ """Decompilation through angr's ``analyses.Decompiler``.
71
+
72
+ Attributes:
73
+ name: ``"angr"``.
74
+ requires: ``("angr",)``.
75
+ """
76
+
77
+ name: str = NAME
78
+ requires: tuple[str, ...] = ("angr",)
79
+
80
+ def availability(self) -> BackendAvailability:
81
+ """Probe ``import angr``; never raises, never installs anything."""
82
+ try:
83
+ import angr
84
+ except ImportError as exc:
85
+ return BackendAvailability(
86
+ name=self.name,
87
+ available=False,
88
+ reason=(
89
+ f"the 'angr' package is not installed ({type(exc).__name__}: {exc}); "
90
+ f"install it with '{INSTALL_HINT}'"
91
+ ),
92
+ requires=self.requires,
93
+ )
94
+ except Exception as exc: # a broken install must not break discovery
95
+ return BackendAvailability(
96
+ name=self.name,
97
+ available=False,
98
+ reason=(
99
+ f"importing 'angr' failed ({type(exc).__name__}: {exc}); "
100
+ f"reinstall it with '{INSTALL_HINT}'"
101
+ ),
102
+ requires=self.requires,
103
+ )
104
+ version = str(getattr(angr, "__version__", "unknown"))
105
+ return BackendAvailability(
106
+ name=self.name,
107
+ available=True,
108
+ reason=f"angr {version} is importable; CFGFast + analyses.Decompiler can run",
109
+ requires=self.requires,
110
+ )
111
+
112
+ def decompile(
113
+ self,
114
+ path: str | Path,
115
+ info: BinaryInfo,
116
+ address: int,
117
+ max_instructions: int = DEFAULT_MAX_INSTRUCTIONS,
118
+ max_blocks: int = DEFAULT_MAX_BLOCKS,
119
+ ) -> BackendResult:
120
+ """Decompile ``address`` with angr's decompiler analysis.
121
+
122
+ Args:
123
+ path: Binary to analyse (opened read-only by angr; never executed).
124
+ info: Loaded metadata; angr loads the file itself, so only the path
125
+ is used. Kept for a uniform backend signature.
126
+ address: Virtual address of the function to decompile.
127
+ max_instructions: Recorded and validated, but see the module
128
+ docstring: angr cannot enforce an instruction budget.
129
+ max_blocks: Checked against the recovered block count after the fact;
130
+ an overrun becomes a warning, not an error.
131
+
132
+ Returns:
133
+ The rendering plus any warnings angr could not prevent.
134
+
135
+ Raises:
136
+ AnalysisError: When angr is unavailable, a budget is out of range,
137
+ angr cannot load the file, or no function is recovered.
138
+ """
139
+ instruction_budget = require_budget(
140
+ "max_instructions", max_instructions, MAX_MAX_INSTRUCTIONS
141
+ )
142
+ block_budget = require_budget("max_blocks", max_blocks, MAX_MAX_BLOCKS)
143
+ availability = self.availability()
144
+ if not availability.available:
145
+ raise _unavailable(availability)
146
+
147
+ import angr
148
+
149
+ warnings: list[str] = []
150
+ try:
151
+ project = angr.Project(str(path), auto_load_libs=False, load_debug_info=False)
152
+ except Exception as exc:
153
+ raise AnalysisError(
154
+ f"angr could not load {str(path)!r}: {type(exc).__name__}: {exc}"
155
+ ) from exc
156
+ cfg = _recover_cfg(project, address)
157
+ function = _lookup_function(cfg, address)
158
+ try:
159
+ analysis = project.analyses.Decompiler(function, cfg=cfg.model)
160
+ except Exception as exc:
161
+ raise AnalysisError(
162
+ f"angr could not decompile 0x{address:x}: {type(exc).__name__}: {exc}"
163
+ ) from exc
164
+
165
+ text = _codegen_text(analysis)
166
+ if not text.strip():
167
+ warnings.append(
168
+ f"angr returned no decompilation for 0x{address:x}: the analysis "
169
+ "produced no codegen text, so the rendering is a placeholder comment"
170
+ )
171
+ text = _EMPTY_TEXT.format(address=address)
172
+
173
+ block_count = len(list(getattr(function, "blocks", ()) or ()))
174
+ if block_count > block_budget:
175
+ warnings.append(
176
+ f"angr decompiled {block_count} block(s), above max_blocks={block_budget}; "
177
+ "angr exposes no block budget, so the text is complete rather than truncated"
178
+ )
179
+
180
+ version = _angr_version(angr)
181
+ evidence = Evidence(
182
+ source=_SOURCE,
183
+ method=_METHOD,
184
+ confidence=Confidence.MEDIUM,
185
+ address=address,
186
+ detail=(
187
+ f"angr {version} decompiled the CFGFast function at 0x{address:x} "
188
+ f"({block_count} block(s)); angr exposes no instruction or block budget, so "
189
+ f"max_instructions={instruction_budget} could not be enforced and "
190
+ f"max_blocks={block_budget} is checked afterwards only"
191
+ ),
192
+ extra={
193
+ "backend": self.name,
194
+ "angr_version": version,
195
+ "max_instructions": instruction_budget,
196
+ "max_blocks": block_budget,
197
+ "block_count": block_count,
198
+ },
199
+ )
200
+ return BackendResult(
201
+ text=text,
202
+ backend=self.name,
203
+ language=LANGUAGE,
204
+ warnings=tuple(warnings),
205
+ evidence=evidence,
206
+ )
207
+
208
+
209
+ def _angr_version(module: Any) -> str:
210
+ """Return the installed angr version, or ``"unknown"``."""
211
+ return str(getattr(module, "__version__", "unknown"))
212
+
213
+
214
+ def _recover_cfg(project: Any, address: int) -> Any:
215
+ """Recover a CFG bounded to the one function start at ``address``.
216
+
217
+ Indirect-jump resolution and data-reference discovery are switched off
218
+ because both widen the scan without helping a single function. The keyword set
219
+ differs across angr releases and angr rejects an unknown arch-specific option
220
+ with ``KeyError`` (not ``TypeError``), so both are treated as "try the minimal
221
+ call": ``function_starts`` alone is the option that does the actual bounding.
222
+ """
223
+ try:
224
+ return project.analyses.CFGFast(
225
+ function_starts=[address],
226
+ resolve_indirect_jumps=False,
227
+ data_references=False,
228
+ normalize=True,
229
+ )
230
+ except (TypeError, KeyError):
231
+ return project.analyses.CFGFast(function_starts=[address])
232
+
233
+
234
+ def _lookup_function(cfg: Any, address: int) -> Any:
235
+ """Return the recovered function at ``address`` or raise :class:`AnalysisError`."""
236
+ function = cfg.kb.functions.get(address)
237
+ if function is None:
238
+ raise AnalysisError(
239
+ f"angr recovered no function at 0x{address:x}; the address may not be a "
240
+ "function start or may be unreachable"
241
+ )
242
+ return function
243
+
244
+
245
+ def _codegen_text(analysis: Any) -> str:
246
+ """Extract the pseudo-C text from a ``Decompiler`` analysis result."""
247
+ codegen = getattr(analysis, "codegen", None)
248
+ if codegen is None:
249
+ return ""
250
+ text = getattr(codegen, "text", "")
251
+ return "" if text is None else str(text)