hypergumbo-lang-rust-analyzer 5.0.0__tar.gz

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.
@@ -0,0 +1,97 @@
1
+ # python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .env
9
+
10
+ # tooling
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .hypothesis/
15
+
16
+ # hypergumbo
17
+ .hypergumbo/
18
+ hypergumbo_capsule/
19
+ hypergumbo.results.json
20
+ hypergumbo.results.*.json
21
+ slice.json
22
+ slice.*.json
23
+
24
+ # Agent configuration
25
+ **/*.local.json
26
+ **/*.local.md
27
+ **/.claude/cache/
28
+ **/.gemini/cache/
29
+ **/.cursor/state/
30
+ **/*-session/
31
+ AUTONOMOUS_MODE.txt
32
+ autonomous_intent.txt
33
+ .agent/LOOP
34
+ .agent/disabled.LOOP
35
+ .agent/invariant-ledger.md
36
+ # Per-session transcript pipeline (ADR-0018, per-session amendment 2026-04-08).
37
+ # Each session writes per-session-keyed files. The .last_*/.second_to_last_*
38
+ # global slots are written at session END by rotate-on-session-end.sh.
39
+ .agent/.current_session_transcript.*.jsonl
40
+ .agent/.current_injection_history.*.jsonl
41
+ .agent/.transcript-sync.*.pid
42
+ .agent/.transcript-sync-state.*.json
43
+ .agent/.transcript-poll-state.*
44
+ .agent/.transcript-injection-state.*.json
45
+ .agent/.transcript-injection-state.*.lock
46
+ .agent/.last_session_transcript.jsonl
47
+ .agent/.second_to_last_transcript.jsonl
48
+ .agent/.last_injection_history.jsonl
49
+ .agent/.second_to_last_injection_history.jsonl
50
+ .agent/.archived-transcripts/
51
+ .agent/.rotation.lock
52
+ # Legacy paths from before the per-session amendment (ignored for one-time
53
+ # upgrade-cleanup safety; may be removed once no repo still has these).
54
+ .agent/.current_session_transcript.jsonl
55
+ .agent/.transcript-sync.pid
56
+ .agent/.transcript-sync-state.json
57
+ .agent/.transcript-poll-state
58
+ .agent/.transcript-injection-state.json
59
+ .agent/.transcript-session-token
60
+ .agent/.current_injection_history.jsonl
61
+ hook-canary.txt
62
+
63
+ # Coverage
64
+ .coverage
65
+ .coverage.*
66
+ htmlcov/
67
+ coverage-report.txt
68
+
69
+ # Snapshot reports (pytest-textual-snapshot)
70
+ snapshot_report.html
71
+
72
+ # Node (bats testing)
73
+ node_modules/
74
+ package.json
75
+ package-lock.json
76
+
77
+ # htrac-frontend: allow tracked JS/TS project files
78
+ !packages/htrac-frontend/package.json
79
+ !packages/htrac-frontend/package-lock.json
80
+
81
+ # Hypergumbo cache
82
+ .hypergumbo_cache/
83
+ .ci/pytest-output.log
84
+ .ci/pytest-results.xml
85
+ .ci/smart-test-fallback.log
86
+
87
+ # Tracker
88
+ **/config.yaml
89
+ !**/config.yaml.template
90
+ .agent/.cache-*.db
91
+ .agent/.sync-logs/
92
+ .agent/.training-data.jsonl
93
+ .agent/.training-data-selected.jsonl
94
+ .agent/.training-data-v1-snapshot-*.jsonl
95
+ .agent/.deprecated-datasets/
96
+ .agent/finetuned-model/
97
+ .agent/transcript-model.gguf
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: hypergumbo-lang-rust-analyzer
3
+ Version: 5.0.0
4
+ Summary: SCIP-backed Rust analyzer for hypergumbo (rust-analyzer integration)
5
+ Author: Hypergumbo contributors
6
+ License: AGPL-3.0-or-later
7
+ Keywords: code-graph,rust,rust-analyzer,scip,static-analysis
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: hypergumbo-core==5.0.0
14
+ Requires-Dist: hypergumbo-lang-mainstream==5.0.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
18
+ # hypergumbo-lang-rust-analyzer
19
+
20
+ SCIP-backed Rust analyzer for hypergumbo.
21
+
22
+ This optional package integrates `rust-analyzer scip` output into hypergumbo,
23
+ providing precise type-resolved symbols and call edges for Rust workspaces
24
+ beyond what the tree-sitter `rust.py` analyzer can recover. It is designed
25
+ as an opt-in alternative to `rust.py`, not a replacement.
26
+
27
+ ## Status
28
+
29
+ Slice A: pure-Python translation surface from parsed SCIP `Index` bytes to
30
+ `(symbols, edges)`. No live `rust-analyzer` invocation yet — that arrives
31
+ in Slice B alongside analyzer-registry wiring and the opt-in flag
32
+ (`HYPERGUMBO_RUST_ANALYZER` env var or `--backend rust-analyzer` CLI flag).
33
+
34
+ ## Why SCIP, not LSP
35
+
36
+ Rust-analyzer emits SCIP with a single shot of `rust-analyzer scip` instead
37
+ of requiring a long-lived LSP session. SCIP is slower than tree-sitter
38
+ (~10× at every realistic size per WI-zakub), so this backend is opt-in and
39
+ falls through to `rust.py` when unavailable or not requested.
40
+
41
+ ## Stable-ID parity
42
+
43
+ `rust.py` and this analyzer both produce `stable_id`s via
44
+ `hypergumbo_lang_mainstream.rust_scip.compute_rust_stable_id_from_source`,
45
+ so cross-pass dedup works. Shared symbols carry the same `stable_id` under
46
+ both backends; rust-analyzer-only symbols (e.g. trait-resolved method
47
+ dispatch) extend the id space with SCIP-only suffixes.
48
+
49
+ ## Upstream shim
50
+
51
+ Symbol / edge emission builds on `hypergumbo_core.scip.*`:
52
+
53
+ - `scip_index_to_symbols` — `Document` walk → `Symbol` objects.
54
+ - `scip_index_to_edges` — `SymbolInformation.relationships` → `Edge`s.
55
+ - `scip_index_to_call_edges` — non-Definition `Occurrence` → calls / references
56
+ edges via span-enclosure resolution.
57
+
58
+ Rust-analyzer's `Relationship` set is empty (per WI-zakub), so the primary
59
+ edge source here is `scip_index_to_call_edges`.
@@ -0,0 +1,43 @@
1
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
2
+ # hypergumbo-lang-rust-analyzer
3
+
4
+ SCIP-backed Rust analyzer for hypergumbo.
5
+
6
+ This optional package integrates `rust-analyzer scip` output into hypergumbo,
7
+ providing precise type-resolved symbols and call edges for Rust workspaces
8
+ beyond what the tree-sitter `rust.py` analyzer can recover. It is designed
9
+ as an opt-in alternative to `rust.py`, not a replacement.
10
+
11
+ ## Status
12
+
13
+ Slice A: pure-Python translation surface from parsed SCIP `Index` bytes to
14
+ `(symbols, edges)`. No live `rust-analyzer` invocation yet — that arrives
15
+ in Slice B alongside analyzer-registry wiring and the opt-in flag
16
+ (`HYPERGUMBO_RUST_ANALYZER` env var or `--backend rust-analyzer` CLI flag).
17
+
18
+ ## Why SCIP, not LSP
19
+
20
+ Rust-analyzer emits SCIP with a single shot of `rust-analyzer scip` instead
21
+ of requiring a long-lived LSP session. SCIP is slower than tree-sitter
22
+ (~10× at every realistic size per WI-zakub), so this backend is opt-in and
23
+ falls through to `rust.py` when unavailable or not requested.
24
+
25
+ ## Stable-ID parity
26
+
27
+ `rust.py` and this analyzer both produce `stable_id`s via
28
+ `hypergumbo_lang_mainstream.rust_scip.compute_rust_stable_id_from_source`,
29
+ so cross-pass dedup works. Shared symbols carry the same `stable_id` under
30
+ both backends; rust-analyzer-only symbols (e.g. trait-resolved method
31
+ dispatch) extend the id space with SCIP-only suffixes.
32
+
33
+ ## Upstream shim
34
+
35
+ Symbol / edge emission builds on `hypergumbo_core.scip.*`:
36
+
37
+ - `scip_index_to_symbols` — `Document` walk → `Symbol` objects.
38
+ - `scip_index_to_edges` — `SymbolInformation.relationships` → `Edge`s.
39
+ - `scip_index_to_call_edges` — non-Definition `Occurrence` → calls / references
40
+ edges via span-enclosure resolution.
41
+
42
+ Rust-analyzer's `Relationship` set is empty (per WI-zakub), so the primary
43
+ edge source here is `scip_index_to_call_edges`.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hypergumbo-lang-rust-analyzer"
7
+ version = "5.0.0"
8
+ description = "SCIP-backed Rust analyzer for hypergumbo (rust-analyzer integration)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "AGPL-3.0-or-later" }
12
+ authors = [{ name = "Hypergumbo contributors" }]
13
+ keywords = ["static-analysis", "code-graph", "rust", "rust-analyzer", "scip"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ ]
20
+ dependencies = [
21
+ "hypergumbo-core==5.0.0",
22
+ "hypergumbo-lang-mainstream==5.0.0",
23
+ ]
24
+
25
+ [project.entry-points."hypergumbo.analyzers"]
26
+ rust_analyzer = "hypergumbo_lang_rust_analyzer:ANALYZER_MODULES"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/hypergumbo_lang_rust_analyzer"]
30
+
31
+ [tool.ruff]
32
+ target-version = "py310"
33
+ line-length = 100
34
+ src = ["src", "tests"]
35
+
36
+ [tool.ruff.lint]
37
+ select = ["E", "W", "F", "B", "C4", "S", "RUF"]
38
+ ignore = ["E501", "S101", "S105", "S106", "S110", "RUF059", "RUF005", "B028"]
39
+
40
+ [tool.ruff.lint.per-file-ignores]
41
+ "tests/**/*.py" = ["S101", "S105", "S106", "S603", "S108", "E741", "F401", "F841", "RUF005"]
@@ -0,0 +1,51 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Hypergumbo SCIP-backed Rust analyzer.
3
+
4
+ Slice A surface: :func:`translate_scip_to_hg` converts a serialized SCIP
5
+ ``Index`` blob into ``(symbols, edges)`` using the in-core shim modules
6
+ plus the ``rust.py`` stable-id parity helper. Later slices add a live
7
+ ``rust-analyzer`` invocation, analyzer-registry wiring, and the opt-in
8
+ flag machinery.
9
+ """
10
+
11
+ from hypergumbo_lang_rust_analyzer.gate import (
12
+ ENV_VAR_NAME,
13
+ should_use_rust_analyzer_backend,
14
+ )
15
+ from hypergumbo_lang_rust_analyzer.graceful_degrade import (
16
+ try_analyze_with_rust_analyzer,
17
+ )
18
+ from hypergumbo_lang_rust_analyzer.invoke import (
19
+ RustAnalyzerError,
20
+ RustAnalyzerInvocationFailed,
21
+ RustAnalyzerNoOutput,
22
+ RustAnalyzerNotInstalled,
23
+ run_rust_analyzer_scip,
24
+ )
25
+ from hypergumbo_lang_rust_analyzer.translate import (
26
+ reassign_rust_stable_ids,
27
+ translate_scip_to_hg,
28
+ )
29
+
30
+ __version__ = "5.0.0"
31
+
32
+ # Module paths for analyzer discovery via entry-points (ADR-0012 Step 1).
33
+ # Importing the listed module triggers the @register_analyzer decorator.
34
+ ANALYZER_MODULES = [
35
+ "hypergumbo_lang_rust_analyzer.analyzer",
36
+ ]
37
+
38
+ __all__ = [
39
+ "ANALYZER_MODULES",
40
+ "ENV_VAR_NAME",
41
+ "RustAnalyzerError",
42
+ "RustAnalyzerInvocationFailed",
43
+ "RustAnalyzerNoOutput",
44
+ "RustAnalyzerNotInstalled",
45
+ "__version__",
46
+ "reassign_rust_stable_ids",
47
+ "run_rust_analyzer_scip",
48
+ "should_use_rust_analyzer_backend",
49
+ "translate_scip_to_hg",
50
+ "try_analyze_with_rust_analyzer",
51
+ ]
@@ -0,0 +1,98 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Registered analyzer entry point for the SCIP-backed Rust backend (WI-duzul Slice C-final).
3
+
4
+ This module threads the four primitives the earlier slices delivered
5
+ into the hypergumbo-core analyzer-registry surface:
6
+
7
+ 1. :func:`hypergumbo_lang_rust_analyzer.gate.should_use_rust_analyzer_backend`
8
+ gates the whole function on the user's opt-in + binary availability.
9
+ 2. :func:`hypergumbo_lang_rust_analyzer.graceful_degrade.try_analyze_with_rust_analyzer`
10
+ handles the actual shell-out + SCIP → IR translation and returns
11
+ ``None`` on any of the WI-nohah fall-through conditions.
12
+ 3. Stable-id parity is already threaded inside
13
+ :func:`~hypergumbo_lang_rust_analyzer.translate.translate_scip_to_hg`,
14
+ so the Symbol objects this analyzer emits are deduplicable against
15
+ the tree-sitter ``rust.py`` analyzer's output when cross-pass dedup
16
+ runs (WI-bajuz).
17
+
18
+ Registration
19
+ ------------
20
+ Registered as analyzer name ``"rust_analyzer"`` (distinct from
21
+ ``rust.py``'s ``"rust"`` registration) so both analyzers coexist in
22
+ the registry: ``rust.py`` remains the default, this one only produces
23
+ output when the user opts in. Priority 45 (vs. the registry default
24
+ 50) so the SCIP pass runs slightly earlier when active — not load-
25
+ bearing, just consistent with the "higher quality backend runs first"
26
+ convention.
27
+
28
+ When the opt-in gate returns False (the default for every session
29
+ that hasn't set ``HYPERGUMBO_RUST_ANALYZER=1`` or passed
30
+ ``--backend rust-analyzer``), this analyzer returns an empty
31
+ :class:`AnalysisResult` immediately. No file walk, no subprocess.
32
+ ``rust.py`` takes care of Rust analysis in that case.
33
+
34
+ Provenance
35
+ ----------
36
+ Successful SCIP-sourced Symbols already carry ``origin="scip"`` from
37
+ :func:`~hypergumbo_core.scip.index.scip_index_to_symbols`, which
38
+ preserves the provenance marker the WI-nohah description calls out.
39
+ Downstream tools can filter on ``symbol.origin`` to trust-weight SCIP-
40
+ derived symbols separately from tree-sitter-derived ones.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ from pathlib import Path
46
+
47
+ from hypergumbo_core.analyze.base import AnalysisResult
48
+ from hypergumbo_core.analyze.registry import register_analyzer
49
+
50
+ from hypergumbo_lang_rust_analyzer.gate import should_use_rust_analyzer_backend
51
+ from hypergumbo_lang_rust_analyzer.graceful_degrade import (
52
+ try_analyze_with_rust_analyzer,
53
+ )
54
+
55
+
56
+ def _disk_source_reader(path: str) -> bytes | None:
57
+ """Read *path* from disk, swallowing I/O errors as ``None``.
58
+
59
+ The translate-layer's reassign_rust_stable_ids takes a caller-owned
60
+ reader callable so tests can inject in-memory fakes. Production
61
+ callers (this function) pass a disk-backed reader with exception
62
+ handling that matches the reassignment pass's contract: any read
63
+ failure maps to "source unavailable, skip parity rewrite" rather
64
+ than crashing the analyzer.
65
+ """
66
+ try:
67
+ return Path(path).read_bytes()
68
+ except (OSError, ValueError): # pragma: no cover — pure defensive
69
+ return None
70
+
71
+
72
+ @register_analyzer("rust_analyzer", priority=45)
73
+ def analyze_rust_with_scip(repo_root: Path) -> AnalysisResult:
74
+ """Entry point for the SCIP-backed Rust analyzer.
75
+
76
+ Returns an empty result when the opt-in gate is False; otherwise
77
+ shells out to ``rust-analyzer scip <repo_root>``, translates the
78
+ emitted SCIP index into hypergumbo ``Symbol`` / ``Edge`` objects
79
+ (with rust.py stable_id parity), and returns them. All three
80
+ WI-nohah fall-through conditions are handled inside
81
+ :func:`try_analyze_with_rust_analyzer` and surface here as a
82
+ ``None`` return — this function swallows that to an empty
83
+ AnalysisResult so the registry treats "no SCIP run" identically
84
+ to "SCIP run produced nothing".
85
+
86
+ The registry calls this with ``repo_root`` being the workspace
87
+ root, which is exactly what ``cargo metadata`` + ``rust-analyzer
88
+ scip`` expect.
89
+ """
90
+ if not should_use_rust_analyzer_backend():
91
+ return AnalysisResult()
92
+
93
+ result = try_analyze_with_rust_analyzer(repo_root, _disk_source_reader)
94
+ if result is None:
95
+ return AnalysisResult()
96
+
97
+ symbols, edges = result
98
+ return AnalysisResult(symbols=symbols, edges=edges)
@@ -0,0 +1,88 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Opt-in gate for the SCIP-backed Rust analyzer (WI-duzul Slice C gate).
3
+
4
+ The rust-analyzer backend is opt-in because SCIP indexing is ~10x slower
5
+ than tree-sitter at every realistic size (WI-zakub §4). Activating it
6
+ requires two conditions:
7
+
8
+ 1. The user explicitly asked for it, either via the
9
+ ``HYPERGUMBO_RUST_ANALYZER`` environment variable (``"1"`` / ``"true"``
10
+ / ``"yes"``, case-insensitive) OR via the ``--backend rust-analyzer``
11
+ CLI flag (which the caller resolves to a string and passes in).
12
+ 2. The ``rust-analyzer`` binary is resolvable on ``PATH``
13
+ (:func:`hypergumbo_core.rust_analyzer_install.is_rust_analyzer_available`).
14
+
15
+ :func:`should_use_rust_analyzer_backend` is the single decision point.
16
+ The function is pure — ``environ`` / ``is_available`` are injected so
17
+ tests can exercise every branch without mutating ``os.environ`` or
18
+ shelling out to ``shutil.which``. Production callers pass ``None`` for
19
+ both and pick up :data:`os.environ` + the real availability check.
20
+
21
+ The split between this module and :mod:`graceful_degrade` is
22
+ intentional: graceful-degrade answers "the user asked; did it work?"
23
+ (handling runtime failures), while this gate answers "did the user
24
+ actually ask?" (handling opt-in). Slice C's analyzer-registry wrapper
25
+ chains them in that order.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ from typing import Callable, Mapping, Optional
32
+
33
+ ENV_VAR_NAME = "HYPERGUMBO_RUST_ANALYZER"
34
+
35
+ # Strings accepted as "yes, opt in" (case-insensitive). Matches the
36
+ # convention used by other tools' truthy-env-var parsing.
37
+ _TRUTHY_VALUES = frozenset({"1", "true", "yes", "on"})
38
+
39
+ # Backend-selector strings accepted by the --backend CLI flag. ``None`` /
40
+ # empty string means "use the default (tree-sitter rust.py)".
41
+ _RUST_ANALYZER_FLAG_VALUES = frozenset({"rust-analyzer", "rust_analyzer", "scip"})
42
+
43
+
44
+ def _is_env_enabled(environ: Mapping[str, str]) -> bool:
45
+ """Return True when the opt-in env var resolves to a truthy value."""
46
+ raw = environ.get(ENV_VAR_NAME, "")
47
+ return raw.strip().lower() in _TRUTHY_VALUES
48
+
49
+
50
+ def _is_flag_enabled(backend_flag: Optional[str]) -> bool:
51
+ """Return True when the caller-supplied ``--backend`` flag selects SCIP."""
52
+ if backend_flag is None:
53
+ return False
54
+ return backend_flag.strip().lower() in _RUST_ANALYZER_FLAG_VALUES
55
+
56
+
57
+ def should_use_rust_analyzer_backend(
58
+ *,
59
+ backend_flag: Optional[str] = None,
60
+ environ: Optional[Mapping[str, str]] = None,
61
+ is_available: Optional[Callable[[], bool]] = None,
62
+ ) -> bool:
63
+ """Return True iff the rust-analyzer backend should run.
64
+
65
+ Two conditions must both hold:
66
+
67
+ - The user opted in, via either ``backend_flag`` or the
68
+ ``HYPERGUMBO_RUST_ANALYZER`` env var. Either one alone is enough.
69
+ - The ``rust-analyzer`` binary is resolvable on ``PATH`` (so
70
+ activating the backend cannot fail at spawn-time for a configuration
71
+ error the user can fix with ``hypergumbo install-rust-analyzer``).
72
+
73
+ When the user opted in but the binary is missing, this helper
74
+ returns False silently — the opt-in is honoured up to the limits of
75
+ what is installed, and the analyzer-registry wrapper falls through
76
+ to ``rust.py``. Callers that want to warn the user about the
77
+ mismatch should check the two conditions separately.
78
+ """
79
+ env = environ if environ is not None else os.environ
80
+ if not (_is_flag_enabled(backend_flag) or _is_env_enabled(env)):
81
+ return False
82
+
83
+ if is_available is None:
84
+ from hypergumbo_core.rust_analyzer_install import (
85
+ is_rust_analyzer_available,
86
+ )
87
+ is_available = is_rust_analyzer_available
88
+ return is_available()
@@ -0,0 +1,121 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Graceful-degrade orchestrator for the SCIP-backed Rust analyzer (WI-nohah).
3
+
4
+ When a caller wants "use rust-analyzer if available, otherwise tell me
5
+ to fall through to :mod:`hypergumbo_lang_mainstream.rust`", it should
6
+ call :func:`try_analyze_with_rust_analyzer`. The helper returns
7
+ ``(symbols, edges)`` on the happy path and ``None`` on any of the
8
+ three fall-through conditions WI-nohah enumerates:
9
+
10
+ 1. ``rust-analyzer`` is not resolvable on ``PATH`` (no install, or
11
+ install in a dir we don't scan).
12
+ 2. ``rust-analyzer scip`` exits non-zero / times out
13
+ (:class:`RustAnalyzerInvocationFailed`), or the workspace does not
14
+ produce an ``index.scip`` file (:class:`RustAnalyzerNoOutput` —
15
+ typically a ``cargo metadata`` error on a workspace with private
16
+ deps or an unusual target triple).
17
+ 3. The SCIP bytes decode fails
18
+ (:class:`google.protobuf.message.DecodeError`) — defensive; the
19
+ only known way to trip this is a truncated file from a killed
20
+ ``rust-analyzer`` process, so treat it identically to failure
21
+ mode 2.
22
+
23
+ Returning ``None`` is the contract — the caller (WI-duzul Slice C's
24
+ analyzer-registry wrapper) is responsible for the actual fall-through
25
+ to ``rust.py``. Keeping the decision point pure lets the fall-through
26
+ logic stay testable without mounting a real analyzer registry.
27
+
28
+ The ``invoke`` and ``translate`` callables are injectable so tests can
29
+ exercise every failure mode without shelling out to a real
30
+ ``rust-analyzer`` binary. Production callers pass ``None`` to pick up
31
+ the default :func:`run_rust_analyzer_scip` /
32
+ :func:`translate_scip_to_hg` surfaces.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import tempfile
38
+ from pathlib import Path
39
+ from typing import Callable, List, Optional, Tuple
40
+
41
+ from google.protobuf.message import DecodeError
42
+
43
+ from hypergumbo_core.ir import Edge, Symbol
44
+
45
+ from .invoke import (
46
+ RustAnalyzerError,
47
+ run_rust_analyzer_scip,
48
+ )
49
+ from .translate import SourceReader, translate_scip_to_hg
50
+
51
+ InvokeFn = Callable[..., bytes]
52
+ TranslateFn = Callable[[bytes, SourceReader], Tuple[List[Symbol], List[Edge]]]
53
+
54
+ # One-time log marker so repeated fall-through attempts don't spam the
55
+ # user's terminal. A set because the helper may be called across
56
+ # multiple workspaces in a single process (monorepo analysis).
57
+ _LOGGED_FALLBACK: set[str] = set()
58
+
59
+
60
+ def _reset_logged_fallback_for_tests() -> None:
61
+ """Clear the once-per-process log marker; test-only helper."""
62
+ _LOGGED_FALLBACK.clear()
63
+
64
+
65
+ def try_analyze_with_rust_analyzer(
66
+ workspace: Path,
67
+ source_reader: SourceReader,
68
+ *,
69
+ invoke: Optional[InvokeFn] = None,
70
+ translate: Optional[TranslateFn] = None,
71
+ log: Optional[Callable[[str], None]] = None,
72
+ ) -> Optional[Tuple[List[Symbol], List[Edge]]]:
73
+ """Run rust-analyzer + SCIP translate on *workspace*, or ``None``.
74
+
75
+ The return type is intentionally ``None | (symbols, edges)`` rather
76
+ than raising — the caller wants the decision "was this a real
77
+ result, or should I fall through?" packaged as a single expression.
78
+ Every failure mode WI-nohah lists maps to ``None``; only a
79
+ successful invoke+translate produces a non-None return.
80
+
81
+ ``source_reader`` is forwarded to :func:`translate_scip_to_hg` for
82
+ the rust.py stable-id parity pass.
83
+
84
+ ``invoke`` defaults to :func:`run_rust_analyzer_scip`;
85
+ ``translate`` defaults to :func:`translate_scip_to_hg`. Both are
86
+ injected in tests to simulate each failure shape without spawning
87
+ a subprocess or constructing a SCIP fixture.
88
+
89
+ ``log`` defaults to a no-op; tests (and the future analyzer
90
+ registry wrapper) pass a real logger so the user sees one line
91
+ explaining why the backend degraded.
92
+ """
93
+ invoke_fn = invoke if invoke is not None else run_rust_analyzer_scip
94
+ translate_fn = translate if translate is not None else translate_scip_to_hg
95
+ emit = log if log is not None else (lambda _msg: None)
96
+
97
+ with tempfile.TemporaryDirectory(prefix="hg_rust_analyzer_") as tmpdir:
98
+ scratch = Path(tmpdir)
99
+ try:
100
+ scip_bytes = invoke_fn(workspace, cwd=scratch)
101
+ except RustAnalyzerError as exc:
102
+ key = f"{type(exc).__name__}:{workspace}"
103
+ if key not in _LOGGED_FALLBACK:
104
+ _LOGGED_FALLBACK.add(key)
105
+ emit(
106
+ f"rust-analyzer backend unavailable for {workspace}: "
107
+ f"{type(exc).__name__} — falling through to rust.py",
108
+ )
109
+ return None
110
+
111
+ try:
112
+ return translate_fn(scip_bytes, source_reader)
113
+ except DecodeError as exc:
114
+ key = f"DecodeError:{workspace}"
115
+ if key not in _LOGGED_FALLBACK:
116
+ _LOGGED_FALLBACK.add(key)
117
+ emit(
118
+ f"rust-analyzer SCIP decode failed for {workspace}: "
119
+ f"{exc} — falling through to rust.py",
120
+ )
121
+ return None