|
| 1 | +#!/usr/bin/env -S uv run |
| 2 | +# /// script |
| 3 | +# requires-python = ">=3.11" |
| 4 | +# dependencies = [ |
| 5 | +# "rich>=13", |
| 6 | +# ] |
| 7 | +# /// |
| 8 | + |
| 9 | +"""Print a colorized table of `[lints.rust]` settings for all members of the Cargo workspace. |
| 10 | +
|
| 11 | +The script discovers workspace members via the root `Cargo.toml`, loads each manifest, |
| 12 | +and lists configured Rust lints (e.g., `dead_code`, `mismatched_lifetime_syntaxes`) so |
| 13 | +you can compare enforcement levels across crates. |
| 14 | +
|
| 15 | +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ |
| 16 | +┃ package ┃ dead_code ┃ deprecated ┃ elided_named_lifetimes ┃ unused_imports ┃ unused_must_use ┃ unused_variables ┃ |
| 17 | +┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ |
| 18 | +│ baml-ids/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 19 | +│ baml-lib/ast/Cargo.toml │ deny │ │ deny │ allow │ │ │ |
| 20 | +│ baml-lib/baml/Cargo.toml │ allow │ │ deny │ allow │ │ allow │ |
| 21 | +│ baml-lib/baml-core/Cargo.toml │ allow │ │ deny │ allow │ │ allow │ |
| 22 | +│ baml-lib/baml-log/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 23 | +│ baml-lib/baml-types/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 24 | +│ baml-lib/diagnostics/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 25 | +│ baml-lib/jinja/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 26 | +│ baml-lib/jinja-runtime/Cargo.toml │ allow │ │ deny │ deny │ │ deny │ |
| 27 | +│ baml-lib/jsonish/Cargo.toml │ allow │ │ deny │ allow │ │ allow │ |
| 28 | +│ baml-lib/llm-client/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 29 | +│ baml-lib/parser-database/Cargo.toml │ allow │ │ deny │ allow │ │ allow │ |
| 30 | +│ baml-lib/prompt-parser/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 31 | +│ baml-lsp-types/Cargo.toml │ │ │ deny │ │ │ │ |
| 32 | +│ baml-rpc/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 33 | +│ baml-runtime/Cargo.toml │ allow │ │ deny │ allow │ │ allow │ |
| 34 | +│ baml-schema-wasm/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 35 | +│ bstd/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 36 | +│ cli/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 37 | +│ language_client_python/Cargo.toml │ deny │ │ deny │ deny │ deny │ deny │ |
| 38 | +│ language_client_typescript/Cargo.toml │ deny │ allow │ deny │ allow │ │ allow │ |
| 39 | +│ language_server/Cargo.toml │ allow │ │ deny │ allow │ │ allow │ |
| 40 | +│ playground-server/Cargo.toml │ deny │ │ deny │ deny │ │ deny │ |
| 41 | +└───────────────────────────────────────┴───────────┴────────────┴────────────────────────┴────────────────┴─────────────────┴──────────────────┘ |
| 42 | +""" |
| 43 | + |
| 44 | +from __future__ import annotations |
| 45 | + |
| 46 | +import sys |
| 47 | +from pathlib import Path |
| 48 | +from typing import Any |
| 49 | + |
| 50 | +try: # Python 3.11+ |
| 51 | + import tomllib # type: ignore |
| 52 | +except ModuleNotFoundError: # pragma: no cover - fallback for older interpreters |
| 53 | + sys.stderr.write("Python 3.11+ required (missing tomllib)\n") |
| 54 | + sys.exit(1) |
| 55 | + |
| 56 | +from rich.console import Console |
| 57 | +from rich.table import Table |
| 58 | +from rich.theme import Theme |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +def find_workspace_root(start: Path) -> Path: |
| 63 | + for candidate in (start, *start.parents): |
| 64 | + manifest = candidate / "Cargo.toml" |
| 65 | + if manifest.exists(): |
| 66 | + with manifest.open("rb") as fh: |
| 67 | + data = tomllib.load(fh) |
| 68 | + if "workspace" in data: |
| 69 | + return candidate |
| 70 | + raise RuntimeError("Unable to locate Cargo workspace root") |
| 71 | + |
| 72 | + |
| 73 | +def read_workspace_members(root: Path) -> list[Path]: |
| 74 | + manifest_path = root / "Cargo.toml" |
| 75 | + with manifest_path.open("rb") as fh: |
| 76 | + manifest = tomllib.load(fh) |
| 77 | + |
| 78 | + members = manifest.get("workspace", {}).get("members", []) |
| 79 | + resolved: set[Path] = set() |
| 80 | + |
| 81 | + for member in members: |
| 82 | + if any(ch in member for ch in "*?[]"): |
| 83 | + for match in root.glob(member): |
| 84 | + candidate = match / "Cargo.toml" if match.is_dir() else match |
| 85 | + if candidate.is_file() and candidate.name == "Cargo.toml": |
| 86 | + resolved.add(candidate.resolve()) |
| 87 | + else: |
| 88 | + candidate = (root / member).resolve() |
| 89 | + manifest_file = candidate / "Cargo.toml" |
| 90 | + if manifest_file.is_file(): |
| 91 | + resolved.add(manifest_file.resolve()) |
| 92 | + |
| 93 | + resolved.add((root / "Cargo.toml").resolve()) |
| 94 | + |
| 95 | + return sorted(resolved) |
| 96 | + |
| 97 | + |
| 98 | +def read_rust_lints(manifest_path: Path) -> dict[str, Any]: |
| 99 | + with manifest_path.open("rb") as fh: |
| 100 | + manifest = tomllib.load(fh) |
| 101 | + |
| 102 | + lints = manifest.get("lints", {}).get("rust") |
| 103 | + if not isinstance(lints, dict): |
| 104 | + return {} |
| 105 | + return lints |
| 106 | + |
| 107 | + |
| 108 | +def format_value(value: Any) -> tuple[str, str]: |
| 109 | + level = None |
| 110 | + display: str |
| 111 | + |
| 112 | + if isinstance(value, str): |
| 113 | + display = value |
| 114 | + level = value.lower() |
| 115 | + elif isinstance(value, dict): |
| 116 | + parts = [] |
| 117 | + for key, val in value.items(): |
| 118 | + parts.append(f"{key}={val}") |
| 119 | + if key == "level" and isinstance(val, str): |
| 120 | + level = val.lower() |
| 121 | + display = ", ".join(parts) |
| 122 | + elif isinstance(value, bool): |
| 123 | + display = str(value).lower() |
| 124 | + elif isinstance(value, (int, float)): |
| 125 | + display = str(value) |
| 126 | + elif isinstance(value, list): |
| 127 | + display = ", ".join(map(str, value)) |
| 128 | + else: |
| 129 | + display = repr(value) |
| 130 | + |
| 131 | + style = { |
| 132 | + "allow": "green", |
| 133 | + "warn": "yellow", |
| 134 | + "deny": "bold red", |
| 135 | + "forbid": "bold red", |
| 136 | + "deny(warn)": "red", |
| 137 | + }.get(level, "white") |
| 138 | + |
| 139 | + return display, style |
| 140 | + |
| 141 | + |
| 142 | +def main() -> None: |
| 143 | + console = Console(theme=Theme({"path": "bold cyan"})) |
| 144 | + |
| 145 | + start = Path.cwd() |
| 146 | + root = find_workspace_root(start) |
| 147 | + manifests = read_workspace_members(root) |
| 148 | + |
| 149 | + rows: list[tuple[str, dict[str, tuple[str, str]]]] = [] |
| 150 | + lint_names: set[str] = set() |
| 151 | + |
| 152 | + for manifest in manifests: |
| 153 | + lints = read_rust_lints(manifest) |
| 154 | + if not lints: |
| 155 | + continue |
| 156 | + |
| 157 | + formatted: dict[str, tuple[str, str]] = {} |
| 158 | + for name, value in lints.items(): |
| 159 | + display, style = format_value(value) |
| 160 | + formatted[name] = (display, style) |
| 161 | + lint_names.add(name) |
| 162 | + |
| 163 | + rel_path = str(manifest.relative_to(root)) |
| 164 | + rows.append((rel_path, formatted)) |
| 165 | + |
| 166 | + if not rows: |
| 167 | + console.print("[bold yellow]No [lints.rust] sections found in workspace members.[/]") |
| 168 | + return |
| 169 | + |
| 170 | + sorted_lints = sorted(lint_names) |
| 171 | + table = Table(show_lines=False) |
| 172 | + table.add_column("package", style="path", overflow="fold") |
| 173 | + |
| 174 | + for lint_name in sorted_lints: |
| 175 | + table.add_column(lint_name, overflow="fold") |
| 176 | + |
| 177 | + for rel_path, formatted in rows: |
| 178 | + row_cells = [rel_path] |
| 179 | + for lint_name in sorted_lints: |
| 180 | + cell = formatted.get(lint_name) |
| 181 | + if cell is None: |
| 182 | + row_cells.append("") |
| 183 | + else: |
| 184 | + text, style = cell |
| 185 | + row_cells.append(f"[{style}]{text}[/]") |
| 186 | + table.add_row(*row_cells) |
| 187 | + |
| 188 | + console.print(table) |
| 189 | + |
| 190 | + |
| 191 | +if __name__ == "__main__": |
| 192 | + try: |
| 193 | + main() |
| 194 | + except Exception as exc: # pragma: no cover - surface errors clearly |
| 195 | + Console().print(f"[bold red]Error:[/] {exc}") |
| 196 | + sys.exit(1) |
0 commit comments