hypergumbo-lang-rust-analyzer 5.0.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.
- hypergumbo_lang_rust_analyzer/__init__.py +51 -0
- hypergumbo_lang_rust_analyzer/analyzer.py +98 -0
- hypergumbo_lang_rust_analyzer/gate.py +88 -0
- hypergumbo_lang_rust_analyzer/graceful_degrade.py +121 -0
- hypergumbo_lang_rust_analyzer/invoke.py +165 -0
- hypergumbo_lang_rust_analyzer/translate.py +170 -0
- hypergumbo_lang_rust_analyzer-5.0.0.dist-info/METADATA +59 -0
- hypergumbo_lang_rust_analyzer-5.0.0.dist-info/RECORD +10 -0
- hypergumbo_lang_rust_analyzer-5.0.0.dist-info/WHEEL +4 -0
- hypergumbo_lang_rust_analyzer-5.0.0.dist-info/entry_points.txt +2 -0
|
@@ -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
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Shell-out wrapper for ``rust-analyzer scip`` (WI-duzul Slice B-first).
|
|
3
|
+
|
|
4
|
+
How It Works
|
|
5
|
+
------------
|
|
6
|
+
Calling ``rust-analyzer scip /path/to/workspace`` emits ``index.scip``
|
|
7
|
+
in the current working directory. This module wraps that invocation in
|
|
8
|
+
a single function, :func:`run_rust_analyzer_scip`, that:
|
|
9
|
+
|
|
10
|
+
1. Confirms ``rust-analyzer`` is resolvable on ``PATH`` (or at the
|
|
11
|
+
explicit binary path the caller passes in).
|
|
12
|
+
2. Invokes ``rust-analyzer scip <workspace>`` with stdout/stderr
|
|
13
|
+
captured, a configurable timeout, and a scratch cwd.
|
|
14
|
+
3. Returns the emitted SCIP index as ``bytes`` suitable for feeding
|
|
15
|
+
straight into :func:`translate_scip_to_hg`.
|
|
16
|
+
|
|
17
|
+
Failure modes are mapped to three dedicated exceptions so callers can
|
|
18
|
+
discriminate between "binary not installed" (WI-nohah territory — fall
|
|
19
|
+
through to ``rust.py``), "invocation failed or timed out" (genuine
|
|
20
|
+
error — surface to the user), and "binary ran but produced no .scip
|
|
21
|
+
file" (likely a cargo metadata error — surface with the captured
|
|
22
|
+
stderr).
|
|
23
|
+
|
|
24
|
+
Why This Design
|
|
25
|
+
---------------
|
|
26
|
+
* WI-zakub established that rust-analyzer SCIP indexing is 10x slower
|
|
27
|
+
than tree-sitter at every realistic size. The ``timeout`` argument
|
|
28
|
+
defaults to 600 seconds so an unexpectedly large workspace fails
|
|
29
|
+
visibly rather than hanging the analyzer pipeline.
|
|
30
|
+
* A dedicated ``rust_analyzer_bin`` kwarg (default ``"rust-analyzer"``)
|
|
31
|
+
keeps the function testable without shell PATH mutations: tests
|
|
32
|
+
pass an absolute path to a fake script. Production callers use the
|
|
33
|
+
default to pick up the user's install.
|
|
34
|
+
* The scratch ``cwd`` is supplied by the caller (typically a
|
|
35
|
+
``tempfile.mkdtemp()``); this module does not own temporary
|
|
36
|
+
directories so callers can manage cleanup and reuse for caching if
|
|
37
|
+
they choose.
|
|
38
|
+
|
|
39
|
+
Out of scope for this module (tracked under WI-duzul Slice C+):
|
|
40
|
+
* Analyzer-registry wiring — ``RustAnalyzerAnalyzer`` class that hooks
|
|
41
|
+
into the analyzer base class and is registered at higher priority
|
|
42
|
+
than ``rust.py``.
|
|
43
|
+
* The opt-in flag (``HYPERGUMBO_RUST_ANALYZER`` env var +
|
|
44
|
+
``--backend rust-analyzer`` CLI flag) that gates whether the shell-
|
|
45
|
+
out fires at all.
|
|
46
|
+
* Graceful-degrade when the binary is absent (WI-nohah). Callers
|
|
47
|
+
decide what to do with :class:`RustAnalyzerNotInstalled`.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import shutil
|
|
53
|
+
import subprocess # nosec B404 — required for rust-analyzer scip invocation
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from typing import Optional
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RustAnalyzerError(Exception):
|
|
59
|
+
"""Base class for :func:`run_rust_analyzer_scip` failures."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RustAnalyzerNotInstalled(RustAnalyzerError):
|
|
63
|
+
"""Raised when the ``rust-analyzer`` binary is not resolvable.
|
|
64
|
+
|
|
65
|
+
Callers should catch this and fall through to the tree-sitter
|
|
66
|
+
``rust.py`` analyzer (WI-nohah). The message carries the binary
|
|
67
|
+
name the caller asked for so a misconfigured ``rust_analyzer_bin``
|
|
68
|
+
kwarg is visible in the error text.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RustAnalyzerInvocationFailed(RustAnalyzerError):
|
|
73
|
+
"""Raised when the binary ran but exited non-zero or timed out.
|
|
74
|
+
|
|
75
|
+
The exception carries the captured stderr (best-effort; empty
|
|
76
|
+
bytes when the process was killed before producing any output) so
|
|
77
|
+
the caller can surface it to the user.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, message: str, stderr: bytes) -> None:
|
|
81
|
+
super().__init__(message)
|
|
82
|
+
self.stderr = stderr
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RustAnalyzerNoOutput(RustAnalyzerError):
|
|
86
|
+
"""Raised when the invocation succeeded but no ``index.scip`` was written.
|
|
87
|
+
|
|
88
|
+
This happens when ``rust-analyzer`` cannot parse the workspace
|
|
89
|
+
(e.g. missing ``Cargo.toml`` at the requested path, or a
|
|
90
|
+
``cargo metadata`` error) but still exits 0. The captured stderr is
|
|
91
|
+
attached so the caller can surface the underlying cargo diagnostic.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, message: str, stderr: bytes) -> None:
|
|
95
|
+
super().__init__(message)
|
|
96
|
+
self.stderr = stderr
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_rust_analyzer_scip(
|
|
100
|
+
workspace: Path,
|
|
101
|
+
*,
|
|
102
|
+
cwd: Path,
|
|
103
|
+
rust_analyzer_bin: str = "rust-analyzer",
|
|
104
|
+
timeout_sec: float = 600.0,
|
|
105
|
+
which: Optional[callable] = None, # type: ignore[valid-type]
|
|
106
|
+
runner: Optional[callable] = None, # type: ignore[valid-type]
|
|
107
|
+
) -> bytes:
|
|
108
|
+
"""Run ``rust-analyzer scip`` on *workspace* and return the SCIP bytes.
|
|
109
|
+
|
|
110
|
+
``cwd`` is the directory in which the subprocess runs; this is where
|
|
111
|
+
``index.scip`` is written and subsequently read. Callers are
|
|
112
|
+
expected to pass a fresh empty directory (``tempfile.mkdtemp()`` is
|
|
113
|
+
the usual pattern) so the read is unambiguous.
|
|
114
|
+
|
|
115
|
+
``rust_analyzer_bin`` is looked up via ``shutil.which`` unless it is
|
|
116
|
+
already an absolute path. The lookup is injectable via the ``which``
|
|
117
|
+
kwarg so tests can simulate a missing binary without mutating the
|
|
118
|
+
process environment.
|
|
119
|
+
|
|
120
|
+
``runner`` is an injectable ``subprocess.run`` stand-in (signature
|
|
121
|
+
``runner(cmd, *, cwd, capture_output, timeout) -> CompletedProcess``)
|
|
122
|
+
so tests can exercise the success / non-zero-exit / timeout /
|
|
123
|
+
no-output paths without actually spawning rust-analyzer.
|
|
124
|
+
|
|
125
|
+
Raises :class:`RustAnalyzerNotInstalled`, :class:`RustAnalyzerInvocationFailed`,
|
|
126
|
+
or :class:`RustAnalyzerNoOutput` per the module docstring.
|
|
127
|
+
"""
|
|
128
|
+
resolve = which if which is not None else shutil.which
|
|
129
|
+
resolved = resolve(rust_analyzer_bin)
|
|
130
|
+
if resolved is None:
|
|
131
|
+
raise RustAnalyzerNotInstalled(
|
|
132
|
+
f"rust-analyzer binary not found on PATH "
|
|
133
|
+
f"(requested: {rust_analyzer_bin!r})",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# resolved comes from shutil.which (or caller-supplied absolute
|
|
137
|
+
# path); never shell=True; args are list-form.
|
|
138
|
+
run = runner if runner is not None else subprocess.run
|
|
139
|
+
cmd = [resolved, "scip", str(workspace)]
|
|
140
|
+
try:
|
|
141
|
+
completed = run( # nosec B603
|
|
142
|
+
cmd,
|
|
143
|
+
cwd=str(cwd),
|
|
144
|
+
capture_output=True,
|
|
145
|
+
timeout=timeout_sec,
|
|
146
|
+
)
|
|
147
|
+
except subprocess.TimeoutExpired as exc:
|
|
148
|
+
raise RustAnalyzerInvocationFailed(
|
|
149
|
+
f"rust-analyzer scip timed out after {timeout_sec}s",
|
|
150
|
+
exc.stderr or b"",
|
|
151
|
+
) from exc
|
|
152
|
+
|
|
153
|
+
if completed.returncode != 0:
|
|
154
|
+
raise RustAnalyzerInvocationFailed(
|
|
155
|
+
f"rust-analyzer scip exited {completed.returncode}",
|
|
156
|
+
completed.stderr or b"",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
index_path = cwd / "index.scip"
|
|
160
|
+
if not index_path.is_file():
|
|
161
|
+
raise RustAnalyzerNoOutput(
|
|
162
|
+
"rust-analyzer scip exited 0 but produced no index.scip",
|
|
163
|
+
completed.stderr or b"",
|
|
164
|
+
)
|
|
165
|
+
return index_path.read_bytes()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""SCIP bytes → hypergumbo ``(Symbol, Edge)`` translation with rust.py parity.
|
|
3
|
+
|
|
4
|
+
How It Works
|
|
5
|
+
------------
|
|
6
|
+
``rust-analyzer scip`` emits a Protobuf-encoded ``scip.Index`` file
|
|
7
|
+
describing every defined symbol, every reference, and every
|
|
8
|
+
relationship in a Rust workspace. This module wraps the hypergumbo-core
|
|
9
|
+
SCIP shim (``hypergumbo_core.scip.*``) with two things the generic
|
|
10
|
+
shim cannot do on its own:
|
|
11
|
+
|
|
12
|
+
1. **Stable-ID parity with rust.py.** The core shim fills
|
|
13
|
+
``Symbol.stable_id`` with the raw SCIP symbol string. That string is
|
|
14
|
+
correct for cross-SCIP-pass dedup but does not match the
|
|
15
|
+
signature-derived stable_id rust.py computes for the same function
|
|
16
|
+
in the same source file. Without reassignment, enabling this
|
|
17
|
+
backend on a cached analysis double-counts every Rust symbol.
|
|
18
|
+
:func:`reassign_rust_stable_ids` replaces the SCIP-derived
|
|
19
|
+
``stable_id`` with the rust.py-equivalent one whenever a Rust
|
|
20
|
+
function span can be located in the caller-provided source reader,
|
|
21
|
+
using
|
|
22
|
+
:func:`hypergumbo_lang_mainstream.rust_scip.compute_rust_stable_id_from_source`.
|
|
23
|
+
Non-Rust Symbols (SCIP can carry multiple languages in one index),
|
|
24
|
+
non-function Rust Symbols (structs, modules, constants — rust.py
|
|
25
|
+
has no signature-level parity for these), and Symbols whose source
|
|
26
|
+
cannot be read pass through unchanged.
|
|
27
|
+
|
|
28
|
+
2. **One-shot translate.** :func:`translate_scip_to_hg` bundles the
|
|
29
|
+
three core shim calls (``scip_index_to_symbols``,
|
|
30
|
+
``scip_index_to_edges``, ``scip_index_to_call_edges``) with the
|
|
31
|
+
parity reassignment so Slice-B's analyzer wrapper can consume a
|
|
32
|
+
single entry point.
|
|
33
|
+
|
|
34
|
+
Why This Design
|
|
35
|
+
---------------
|
|
36
|
+
WI-zakub (2026-04-17 mini trial) established that rust-analyzer leaves
|
|
37
|
+
``SymbolInformation.relationships`` empty; the primary edge source is
|
|
38
|
+
therefore non-Definition ``Occurrence``s routed through
|
|
39
|
+
``scip_index_to_call_edges``. Both ``scip_index_to_edges`` and
|
|
40
|
+
``scip_index_to_call_edges`` are still called here — the former is
|
|
41
|
+
zero-cost when the relationship set is empty, and keeping both in the
|
|
42
|
+
pipeline lets the translator also work on SCIP indexes produced by
|
|
43
|
+
other tools (scip-python, scip-java) that do populate relationships.
|
|
44
|
+
|
|
45
|
+
The ``source_reader`` callable is a caller-owned I/O boundary:
|
|
46
|
+
production callers pass a function that reads ``path`` bytes from the
|
|
47
|
+
indexed workspace; tests pass a dict-backed fake so the whole
|
|
48
|
+
translate path can be exercised without filesystem access. Reader
|
|
49
|
+
failures (``OSError``, ``FileNotFoundError``, arbitrary exceptions)
|
|
50
|
+
degrade to "skip this symbol's reassignment" rather than aborting the
|
|
51
|
+
whole translate — the SCIP-derived ``stable_id`` remains as a
|
|
52
|
+
best-effort fallback.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
from typing import Callable, Iterable, List, Optional
|
|
58
|
+
|
|
59
|
+
from hypergumbo_core.ir import Edge, Symbol
|
|
60
|
+
from hypergumbo_core.scip._generated import scip_pb2
|
|
61
|
+
from hypergumbo_core.scip.calls import scip_index_to_call_edges
|
|
62
|
+
from hypergumbo_core.scip.edges import scip_index_to_edges
|
|
63
|
+
from hypergumbo_core.scip.index import scip_index_to_symbols
|
|
64
|
+
from hypergumbo_lang_mainstream.rust_scip import (
|
|
65
|
+
compute_rust_stable_id_from_source,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
SourceReader = Callable[[str], Optional[bytes]]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _read_source(source_reader: SourceReader, path: str) -> Optional[bytes]:
|
|
73
|
+
"""Invoke *source_reader*, swallowing I/O errors as ``None``.
|
|
74
|
+
|
|
75
|
+
Reader exceptions (FileNotFoundError, OSError, or anything the
|
|
76
|
+
caller's implementation chooses to raise) map to "source
|
|
77
|
+
unavailable, skip reassignment" — the translate pass still emits
|
|
78
|
+
the Symbol with the SCIP-derived stable_id rather than dropping it.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
return source_reader(path)
|
|
82
|
+
except (OSError, ValueError):
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def reassign_rust_stable_ids(
|
|
87
|
+
symbols: Iterable[Symbol],
|
|
88
|
+
source_reader: SourceReader,
|
|
89
|
+
) -> List[Symbol]:
|
|
90
|
+
"""Return *symbols* with Rust function ``stable_id``s rust.py-compatible.
|
|
91
|
+
|
|
92
|
+
For each input Symbol, the reassignment applies only when:
|
|
93
|
+
|
|
94
|
+
- ``language == "rust"``
|
|
95
|
+
- ``kind == "function"`` or ``kind == "method"``
|
|
96
|
+
- ``span`` is present (SCIP Definition Occurrences always carry one)
|
|
97
|
+
- ``source_reader(path)`` returns bytes
|
|
98
|
+
- ``compute_rust_stable_id_from_source`` finds the function at the
|
|
99
|
+
span and returns a non-None stable_id
|
|
100
|
+
|
|
101
|
+
All other Symbols (non-Rust, non-function, span-less, source-less,
|
|
102
|
+
or parity-lookup-failed) pass through with their original
|
|
103
|
+
``stable_id`` untouched. The return value is a new list — inputs
|
|
104
|
+
are not mutated.
|
|
105
|
+
"""
|
|
106
|
+
reassigned: List[Symbol] = []
|
|
107
|
+
source_cache: dict[str, Optional[bytes]] = {}
|
|
108
|
+
for sym in symbols:
|
|
109
|
+
new_sym = sym
|
|
110
|
+
if (
|
|
111
|
+
sym.language == "rust"
|
|
112
|
+
and sym.kind in {"function", "method"}
|
|
113
|
+
and sym.span is not None
|
|
114
|
+
and sym.path
|
|
115
|
+
):
|
|
116
|
+
if sym.path not in source_cache:
|
|
117
|
+
source_cache[sym.path] = _read_source(source_reader, sym.path)
|
|
118
|
+
source = source_cache[sym.path]
|
|
119
|
+
if source is not None:
|
|
120
|
+
parity = compute_rust_stable_id_from_source(
|
|
121
|
+
source, sym.span.start_line, sym.span.end_line,
|
|
122
|
+
)
|
|
123
|
+
if parity is not None:
|
|
124
|
+
new_sym = Symbol(
|
|
125
|
+
id=sym.id,
|
|
126
|
+
name=sym.name,
|
|
127
|
+
kind=sym.kind,
|
|
128
|
+
language=sym.language,
|
|
129
|
+
path=sym.path,
|
|
130
|
+
span=sym.span,
|
|
131
|
+
origin=sym.origin,
|
|
132
|
+
stable_id=parity,
|
|
133
|
+
signature=sym.signature,
|
|
134
|
+
meta=sym.meta,
|
|
135
|
+
)
|
|
136
|
+
reassigned.append(new_sym)
|
|
137
|
+
return reassigned
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def translate_scip_to_hg(
|
|
141
|
+
scip_bytes: bytes,
|
|
142
|
+
source_reader: SourceReader,
|
|
143
|
+
) -> tuple[List[Symbol], List[Edge]]:
|
|
144
|
+
"""Parse *scip_bytes* and return ``(symbols, edges)`` with parity.
|
|
145
|
+
|
|
146
|
+
The three core SCIP shim passes are invoked in order:
|
|
147
|
+
|
|
148
|
+
1. :func:`scip_index_to_symbols` to build the Symbol list.
|
|
149
|
+
2. :func:`reassign_rust_stable_ids` to overwrite Rust function
|
|
150
|
+
``stable_id`` fields with rust.py-compatible values.
|
|
151
|
+
3. :func:`scip_index_to_edges` for Relationship-derived edges.
|
|
152
|
+
4. :func:`scip_index_to_call_edges` for Occurrence-ref edges via
|
|
153
|
+
span-enclosure resolution.
|
|
154
|
+
|
|
155
|
+
The Edge list is the concatenation of the two edge sources; callers
|
|
156
|
+
expecting a deduplicated edge set should run the standard
|
|
157
|
+
``deduplicate_edges`` pass themselves (not done here — translate
|
|
158
|
+
preserves the shim outputs verbatim so downstream passes can see
|
|
159
|
+
the raw signal and ``deduplicate_edges`` is the only place the
|
|
160
|
+
dedup rule lives).
|
|
161
|
+
"""
|
|
162
|
+
index = scip_pb2.Index()
|
|
163
|
+
index.ParseFromString(scip_bytes)
|
|
164
|
+
symbols = reassign_rust_stable_ids(
|
|
165
|
+
scip_index_to_symbols(index), source_reader,
|
|
166
|
+
)
|
|
167
|
+
edges: List[Edge] = []
|
|
168
|
+
edges.extend(scip_index_to_edges(index))
|
|
169
|
+
edges.extend(scip_index_to_call_edges(index))
|
|
170
|
+
return symbols, edges
|
|
@@ -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,10 @@
|
|
|
1
|
+
hypergumbo_lang_rust_analyzer/__init__.py,sha256=7e2d2EwdsWlN0yDYvYxyWvmVfVtqc_J69rA0aWcmfvg,1513
|
|
2
|
+
hypergumbo_lang_rust_analyzer/analyzer.py,sha256=_HVAsSmWeMuYV9y5qhHcqV6-oqSQZSEG7lglWBZSQdA,4224
|
|
3
|
+
hypergumbo_lang_rust_analyzer/gate.py,sha256=jw5sK4FDFQHvNSp8WpShS1lgHy5aM86n3lCPHTPoJ4E,3714
|
|
4
|
+
hypergumbo_lang_rust_analyzer/graceful_degrade.py,sha256=QacACz2Yuakjje5GPAkizgpni3z5w-qrtgSqnY38PgI,4912
|
|
5
|
+
hypergumbo_lang_rust_analyzer/invoke.py,sha256=HeyDkfxVN-_ztoX5KqSZRB_oVAwjv0blRZnZ5yHV-Ok,6569
|
|
6
|
+
hypergumbo_lang_rust_analyzer/translate.py,sha256=_mJPxZmThaKwZUw5iC3xshkF-BdEhNrBtypuxCJezgA,7080
|
|
7
|
+
hypergumbo_lang_rust_analyzer-5.0.0.dist-info/METADATA,sha256=twHOxfBPg1u36WgjbQ1SvRLzQlOyEOQTGwQ46X2cYic,2517
|
|
8
|
+
hypergumbo_lang_rust_analyzer-5.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
hypergumbo_lang_rust_analyzer-5.0.0.dist-info/entry_points.txt,sha256=zJQjXxwrzFMhVWkXb5UbF8RzjcgnrdcOWepL6oewJog,86
|
|
10
|
+
hypergumbo_lang_rust_analyzer-5.0.0.dist-info/RECORD,,
|