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.
- hypergumbo_lang_rust_analyzer-5.0.0/.gitignore +97 -0
- hypergumbo_lang_rust_analyzer-5.0.0/PKG-INFO +59 -0
- hypergumbo_lang_rust_analyzer-5.0.0/README.md +43 -0
- hypergumbo_lang_rust_analyzer-5.0.0/pyproject.toml +41 -0
- hypergumbo_lang_rust_analyzer-5.0.0/src/hypergumbo_lang_rust_analyzer/__init__.py +51 -0
- hypergumbo_lang_rust_analyzer-5.0.0/src/hypergumbo_lang_rust_analyzer/analyzer.py +98 -0
- hypergumbo_lang_rust_analyzer-5.0.0/src/hypergumbo_lang_rust_analyzer/gate.py +88 -0
- hypergumbo_lang_rust_analyzer-5.0.0/src/hypergumbo_lang_rust_analyzer/graceful_degrade.py +121 -0
- hypergumbo_lang_rust_analyzer-5.0.0/src/hypergumbo_lang_rust_analyzer/invoke.py +165 -0
- hypergumbo_lang_rust_analyzer-5.0.0/src/hypergumbo_lang_rust_analyzer/translate.py +170 -0
- hypergumbo_lang_rust_analyzer-5.0.0/tests/test_analyzer.py +140 -0
- hypergumbo_lang_rust_analyzer-5.0.0/tests/test_gate.py +127 -0
- hypergumbo_lang_rust_analyzer-5.0.0/tests/test_graceful_degrade.py +190 -0
- hypergumbo_lang_rust_analyzer-5.0.0/tests/test_invoke.py +195 -0
- hypergumbo_lang_rust_analyzer-5.0.0/tests/test_translate.py +270 -0
|
@@ -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
|