From 707d33f6eb14e2b108d64b61a3a0b5bcc6a70b05 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 30 May 2026 19:12:05 +0900 Subject: [PATCH] feat(shared-skills): batch 27 (3 files) --- .../scripts/rust/check-no-excuse-rules.py | 296 ++++++++++++++++++ .../scripts/rust/check-no-excuse-rules.sh | 158 ++++++++++ .../programming/scripts/rust/new-project.py | 175 +++++++++++ 3 files changed, 629 insertions(+) create mode 100755 packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.py create mode 100755 packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.sh create mode 100755 packages/shared-skills/skills/programming/scripts/rust/new-project.py diff --git a/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.py b/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.py new file mode 100755 index 000000000..2c2c5413b --- /dev/null +++ b/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.py @@ -0,0 +1,296 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// +# +# How to run: +# uv run --script check-no-excuse-rules.py src/lib.rs src/main.rs +# uv run --script check-no-excuse-rules.py src/ # recursively finds .rs files +# uv run --script check-no-excuse-rules.py . # entire tree +# +# No-excuse rule checker for Rust files — Python rewrite of check-no-excuse-rules.sh. +# Only rules enforceable via pure text matching live here. +# Everything semantic is on clippy + miri + nextest. +# +# Rules: +# unwrap .unwrap() outside tests without // SAFE-UNWRAP: +# expect .expect() outside tests without // SAFE-EXPECT: +# placeholder-macro todo!/unimplemented!/unreachable!/unreachable_unchecked! in committed code +# box-dyn-error Box in non-test code +# lib-panic panic!() in library code +# unsafe-no-safety unsafe { without // SAFETY: in preceding 5 lines +# unjustified-clippy-allow #[allow(clippy::...)] without // CLIPPY-ALLOW: +# narrowing-as-cast possible narrowing 'as' cast +# +# Opt-out: place the appropriate comment on the previous line: +# // SAFE-UNWRAP: +# // SAFE-EXPECT: +# // SAFETY: (for unsafe blocks, within 5 lines above) +# // CLIPPY-ALLOW: +# +# Test paths (exempt from unwrap/expect/placeholder/box-dyn-error/lib-panic): +# tests/, benches/, examples/, build.rs, *_test.rs, #[cfg(test)] regions + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +# --------------------------------------------------------------------------- +# Patterns (compiled once) +# --------------------------------------------------------------------------- + +RE_UNWRAP = re.compile(r"\.unwrap\(\)") +RE_EXPECT = re.compile(r"\.expect\(") +RE_PLACEHOLDER = re.compile(r"\b(todo!|unimplemented!|unreachable!|unreachable_unchecked!)") +RE_BOX_DYN_ERROR = re.compile(r"Box None: + """Emit a GitHub-Actions-compatible error annotation to stderr.""" + global violations + print(f"::error file={file},line={line}::[{rule}] {detail}", file=sys.stderr) + violations += 1 + + +def is_test_path(path: Path) -> bool: + """Return True if *path* is in a test/bench/example directory or is a test file.""" + parts = path.parts + for part in parts: + if part in _TEST_PATH_PARTS: + return True + if path.name == "build.rs": + return True + if path.name.endswith("_test.rs"): + return True + return False + + +def is_lib_path(file: Path) -> bool: + """Heuristic: is this file library code (not main.rs, not src/bin/*).""" + parts = path_parts_str(file) + # Must live under src/ + if "src" not in parts: + return False + if file.name == "main.rs": + return False + # src/bin/* is binary code + try: + src_idx = parts.index("src") + if src_idx + 1 < len(parts) and parts[src_idx + 1] == "bin": + return False + except ValueError: + return False + return True + + +def path_parts_str(p: Path) -> list[str]: + return list(p.parts) + + +def strip_line_comment(line: str) -> str: + """Return the portion of *line* before any ``//`` line comment. + + This is a crude heuristic — it does not handle ``//`` inside string + literals, but matches the behaviour of the bash version. + """ + idx = line.find("//") + if idx == -1: + return line + return line[:idx] + + +def collect_rs_files(args: list[str]) -> list[Path]: + """Expand CLI arguments: files are kept as-is, directories are walked.""" + result: list[Path] = [] + for arg in args: + p = Path(arg) + if p.is_file(): + if p.suffix == ".rs": + result.append(p) + elif p.is_dir(): + result.extend(sorted(p.rglob("*.rs"))) + # Ignore non-existent / non-.rs + return result + + +# --------------------------------------------------------------------------- +# Main checker +# --------------------------------------------------------------------------- + +def check_file(file: Path) -> None: + in_test_file = is_test_path(file) + in_cfg_test = False + cfg_test_brace_depth = 0 + + try: + lines = file.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError as exc: + print(f"warning: cannot read {file}: {exc}", file=sys.stderr) + return + + for line_no_0, raw_line in enumerate(lines): + line_no = line_no_0 + 1 # 1-indexed + + # --- #[cfg(test)] region tracker --- + if RE_CFG_TEST.search(raw_line): + in_cfg_test = True + cfg_test_brace_depth = 0 + + if in_cfg_test: + opens = raw_line.count("{") + closes = raw_line.count("}") + cfg_test_brace_depth += opens - closes + if cfg_test_brace_depth <= 0 and not RE_CFG_TEST.search(raw_line): + in_cfg_test = False + + exempt = in_test_file or in_cfg_test + code_only = strip_line_comment(raw_line) + + if not exempt: + # .unwrap() + if RE_UNWRAP.search(code_only): + prev = lines[line_no_0 - 1] if line_no_0 > 0 else "" + if not RE_SAFE_UNWRAP.search(prev): + report( + str(file), line_no, "unwrap", + ".unwrap() outside tests - use ? / ok_or / pattern match " + "or annotate previous line with // SAFE-UNWRAP: ", + ) + + # .expect(...) + if RE_EXPECT.search(code_only): + prev = lines[line_no_0 - 1] if line_no_0 > 0 else "" + if not RE_SAFE_EXPECT.search(prev): + report( + str(file), line_no, "expect", + ".expect() outside tests - use ? or annotate previous " + "line with // SAFE-EXPECT: ", + ) + + # todo!/unimplemented!/unreachable!/unreachable_unchecked! + if RE_PLACEHOLDER.search(code_only): + report( + str(file), line_no, "placeholder-macro", + "todo!/unimplemented!/unreachable! in committed code", + ) + + # Box + if RE_BOX_DYN_ERROR.search(code_only): + report( + str(file), line_no, "box-dyn-error", + "Box in non-test code - use anyhow::Error (apps) " + "or thiserror enum (libs)", + ) + + # panic!() in library code + if is_lib_path(file) and RE_PANIC.search(code_only): + report( + str(file), line_no, "lib-panic", + "panic!() in library code - return Result", + ) + + # unsafe { without // SAFETY: — always enforced, even in tests + if RE_UNSAFE_BLOCK.search(code_only): + start = max(0, line_no_0 - 5) + window = "\n".join(lines[start : line_no_0 + 1]) + if not RE_SAFETY.search(window): + report( + str(file), line_no, "unsafe-no-safety-comment", + "unsafe block without // SAFETY: comment in preceding 5 lines", + ) + + # #[allow(clippy::...)] without // CLIPPY-ALLOW: — always enforced + if RE_CLIPPY_ALLOW.search(code_only): + prev = lines[line_no_0 - 1] if line_no_0 > 0 else "" + if not RE_CLIPPY_ALLOW_JUST.search(prev): + report( + str(file), line_no, "unjustified-clippy-allow", + "#[allow(clippy::...)] without // CLIPPY-ALLOW: on " + "previous line", + ) + + # Narrowing numeric `as` casts + if RE_NARROWING_CAST.search(code_only): + report( + str(file), line_no, "narrowing-as-cast", + "possible narrowing 'as' cast - use TryFrom / try_into() for " + "fallible conversion", + ) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + global violations + + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [file.rs|dir ...]", file=sys.stderr) + sys.exit(2) + + files = collect_rs_files(sys.argv[1:]) + if not files: + print("warning: no .rs files found in the given arguments", file=sys.stderr) + sys.exit(0) + + for f in files: + check_file(f) + + if violations > 0: + print("", file=sys.stderr) + print( + f"rust-programmer: {violations} violation(s). Fix before declaring work done.", + file=sys.stderr, + ) + print("", file=sys.stderr) + print("Then run the full toolchain gate:", file=sys.stderr) + print(" cargo +stable fmt --all -- --check", file=sys.stderr) + print( + " cargo +stable clippy --all-targets --all-features -- -D warnings", + file=sys.stderr, + ) + print(" cargo nextest run --all-targets --all-features", file=sys.stderr) + print( + " cargo +nightly miri nextest run --all-features # if unsafe touched", + file=sys.stderr, + ) + print(" cargo machete", file=sys.stderr) + print(" cargo deny check", file=sys.stderr) + sys.exit(1) + + print(f"rust-programmer: no-excuse rules passed for {len(files)} file(s).") + + +if __name__ == "__main__": + main() diff --git a/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.sh b/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.sh new file mode 100755 index 000000000..d4fa8e917 --- /dev/null +++ b/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# No-excuse rule checker for Rust files. +# Mirrors the philosophy of python-programmer / typescript-programmer scripts: +# only rules that can be enforced via pure text matching live here. +# Everything semantic is on clippy + miri + nextest. + +set -euo pipefail + +if [ $# -eq 0 ]; then + echo "Usage: $0 [file.rs ...]" >&2 + exit 2 +fi + +violations=0 +report() { + local file="$1" + local line="$2" + local rule="$3" + local detail="$4" + echo "::error file=${file},line=${line}::[${rule}] ${detail}" >&2 + violations=$((violations + 1)) +} + +is_test_path() { + local path="$1" + case "$path" in + */tests/*|*/benches/*|*/examples/*|*/build.rs|*_test.rs|tests/*|benches/*|examples/*) return 0 ;; + esac + # In-file #[cfg(test)] modules are handled per-line below. + return 1 +} + +for file in "$@"; do + [ -f "$file" ] || continue + case "$file" in + *.rs) ;; + *) continue ;; + esac + + if is_test_path "$file"; then + # Test files are exempt from unwrap/expect/todo rules. + # Still enforce unsafe-comment, allow-comment, panic-in-lib rules below + # by setting a marker - keeping the loop unified. + in_test_file=1 + else + in_test_file=0 + fi + + # Track #[cfg(test)] regions for per-line exemptions. + in_cfg_test=0 + cfg_test_brace_depth=0 + line_no=0 + + while IFS= read -r raw_line || [ -n "$raw_line" ]; do + line_no=$((line_no + 1)) + line="$raw_line" + + # Crude #[cfg(test)] region tracker: when we see #[cfg(test)] on a + # line followed by a mod with `{`, count braces until depth returns + # to zero. This is approximate but matches typical formatting. + if [[ "$line" =~ \#\[cfg\(test\)\] ]]; then + in_cfg_test=1 + cfg_test_brace_depth=0 + fi + if [ "$in_cfg_test" -eq 1 ]; then + opens=$(printf '%s' "$line" | tr -cd '{' | wc -c) + closes=$(printf '%s' "$line" | tr -cd '}' | wc -c) + cfg_test_brace_depth=$((cfg_test_brace_depth + opens - closes)) + if [ "$cfg_test_brace_depth" -le 0 ] && [[ ! "$line" =~ \#\[cfg\(test\)\] ]]; then + in_cfg_test=0 + fi + fi + + exempt=0 + [ "$in_test_file" -eq 1 ] && exempt=1 + [ "$in_cfg_test" -eq 1 ] && exempt=1 + + # Strip line comments before pattern checks - so doc comments and + # explanatory prose do not trip the regexes. + code_only="${line%%//*}" + + if [ "$exempt" -eq 0 ]; then + # .unwrap() + if [[ "$code_only" =~ \.unwrap\(\) ]]; then + # Allow if previous line had // SAFE-UNWRAP: comment + prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true) + if [[ ! "$prev_line" =~ //[[:space:]]*SAFE-UNWRAP: ]]; then + report "$file" "$line_no" "unwrap" ".unwrap() outside tests - use ? / ok_or / pattern match or annotate previous line with // SAFE-UNWRAP: " + fi + fi + + # .expect("...") + if [[ "$code_only" =~ \.expect\( ]]; then + prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true) + if [[ ! "$prev_line" =~ //[[:space:]]*SAFE-EXPECT: ]]; then + report "$file" "$line_no" "expect" ".expect() outside tests - use ? or annotate previous line with // SAFE-EXPECT: " + fi + fi + + # todo!() / unimplemented!() / unreachable!() + if [[ "$code_only" =~ (todo!|unimplemented!|unreachable!|unreachable_unchecked!) ]]; then + report "$file" "$line_no" "placeholder-macro" "todo!/unimplemented!/unreachable! in committed code" + fi + + # Box in non-test code - use anyhow::Error (apps) or thiserror enum (libs)" + fi + + # panic!( in lib + if [[ "$file" == */src/lib.rs || "$file" == */src/*/mod.rs || ( "$file" == */src/*.rs && "$file" != */src/main.rs && "$file" != */src/bin/* ) ]]; then + if [[ "$code_only" =~ panic!\( ]]; then + report "$file" "$line_no" "lib-panic" "panic!() in library code - return Result" + fi + fi + fi + + # unsafe { without preceding // SAFETY: in the last 5 lines (always enforced) + if [[ "$code_only" =~ unsafe[[:space:]]*\{ ]]; then + start=$((line_no > 5 ? line_no - 5 : 1)) + window=$(sed -n "${start},${line_no}p" "$file" 2>/dev/null || true) + if [[ ! "$window" =~ //[[:space:]]*SAFETY: ]]; then + report "$file" "$line_no" "unsafe-no-safety-comment" "unsafe block without // SAFETY: comment in preceding 5 lines" + fi + fi + + # #[allow(clippy::...)] without preceding // CLIPPY-ALLOW: justification + if [[ "$code_only" =~ \#\[allow\(clippy:: ]]; then + prev_line=$(sed -n "$((line_no - 1))p" "$file" 2>/dev/null || true) + if [[ ! "$prev_line" =~ //[[:space:]]*CLIPPY-ALLOW: ]]; then + report "$file" "$line_no" "unjustified-clippy-allow" "#[allow(clippy::...)] without // CLIPPY-ALLOW: on previous line" + fi + fi + + # Narrowing numeric `as` casts - heuristic flag for human review. + # Catches the common shapes; precise type analysis belongs to clippy::cast_possible_truncation. + if [[ "$code_only" =~ as[[:space:]]+(u8|u16|u32|i8|i16|i32) ]] && \ + [[ "$code_only" =~ (u16|u32|u64|u128|usize|i16|i32|i64|i128|isize)[[:space:]]+as[[:space:]]+(u8|u16|u32|i8|i16|i32) ]]; then + report "$file" "$line_no" "narrowing-as-cast" "possible narrowing 'as' cast - use TryFrom / try_into() for fallible conversion" + fi + done < "$file" +done + +if [ "$violations" -gt 0 ]; then + echo "" >&2 + echo "rust-programmer: ${violations} violation(s). Fix before declaring work done." >&2 + echo "" >&2 + echo "Then run the full toolchain gate:" >&2 + echo " cargo +stable fmt --all -- --check" >&2 + echo " cargo +stable clippy --all-targets --all-features -- -D warnings" >&2 + echo " cargo nextest run --all-targets --all-features" >&2 + echo " cargo +nightly miri nextest run --all-features # if unsafe touched" >&2 + echo " cargo machete" >&2 + echo " cargo deny check" >&2 + exit 1 +fi + +echo "rust-programmer: no-excuse rules passed for $# file(s)." diff --git a/packages/shared-skills/skills/programming/scripts/rust/new-project.py b/packages/shared-skills/skills/programming/scripts/rust/new-project.py new file mode 100755 index 000000000..2445f8055 --- /dev/null +++ b/packages/shared-skills/skills/programming/scripts/rust/new-project.py @@ -0,0 +1,175 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "typer", +# "rich", +# ] +# /// + +# ─── How to run ─── +# 1. Install uv (if not installed): +# curl -LsSf https://astral.sh/uv/install.sh | sh +# 2. Run: +# uv run new-project.py myproject +# uv run new-project.py myproject --path ./workspace +# ────────────────── +# +# Creates a new Rust project with strict lints, deny.toml, rustfmt.toml, +# rust-toolchain.toml, and .cargo/config.toml pre-configured. + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import typer +from rich.console import Console + +console = Console(stderr=True) + +# ── Embedded config contents ───────────────────────────────────────────── + +RUST_TOOLCHAIN_TOML = """\ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy", "rust-src"] +profile = "default" +""" + +CARGO_TOML_LINTS = """ +[lints.rust] +unsafe_op_in_unsafe_fn = "deny" +missing_docs = "warn" +missing_debug_implementations = "warn" +unreachable_pub = "warn" +unused_must_use = "deny" +elided_lifetimes_in_paths = "warn" +non_ascii_idents = "deny" +trivial_numeric_casts = "warn" +unused_lifetimes = "warn" +single_use_lifetimes = "warn" + +[lints.clippy] +all = { level = "deny", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +undocumented_unsafe_blocks = "deny" +multiple_unsafe_ops_per_block = "deny" +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +dbg_macro = "deny" +print_stdout = "warn" +print_stderr = "warn" +module_name_repetitions = { level = "allow" } +must_use_candidate = { level = "allow" } +missing_errors_doc = { level = "allow" } +missing_panics_doc = { level = "allow" } +""" + +CARGO_CONFIG_TOML = """\ +[build] +rustflags = ["-C", "link-arg=-fuse-ld=lld"] + +[target.x86_64-unknown-linux-gnu] +linker = "clang" +rustflags = ["-C", "link-arg=-fuse-ld=lld"] + +[target.aarch64-apple-darwin] +rustflags = [] +""" + +DENY_TOML = """\ +[advisories] +vulnerability = "deny" +unmaintained = "warn" +yanked = "deny" + +[licenses] +unlicensed = "deny" +allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-3.0", "Zlib"] + +[bans] +multiple-versions = "warn" +wildcards = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +""" + +RUSTFMT_TOML = """\ +edition = "2024" +max_width = 100 +use_field_init_shorthand = true +use_try_shorthand = true +""" + +# ── Main ───────────────────────────────────────────────────────────────── + +app = typer.Typer(add_completion=False) + + +@app.command() +def main( + name: str = typer.Argument(help="Name of the new Rust project"), + path: Path = typer.Option( + Path.cwd(), + "--path", + "-p", + help="Parent directory where the project folder is created", + ), +) -> None: + """Scaffold a new Rust project with strict lints and tooling configs.""" + project_dir = path / name + + # ── cargo init ─────────────────────────────────────────────────── + console.print(f"[bold green]Creating[/] project [cyan]{name}[/] at [dim]{project_dir}[/]") + try: + subprocess.run( + ["cargo", "init", str(project_dir), "--name", name], + check=True, + capture_output=True, + text=True, + ) + except FileNotFoundError: + console.print("[bold red]Error:[/] cargo not found. Install Rust via https://rustup.rs") + sys.exit(1) + except subprocess.CalledProcessError as exc: + console.print(f"[bold red]cargo init failed:[/]\n{exc.stderr}") + sys.exit(1) + + # ── rust-toolchain.toml ────────────────────────────────────────── + (project_dir / "rust-toolchain.toml").write_text(RUST_TOOLCHAIN_TOML) + console.print(" [dim]wrote[/] rust-toolchain.toml") + + # ── Append [lints] to Cargo.toml ───────────────────────────────── + cargo_toml = project_dir / "Cargo.toml" + with cargo_toml.open("a") as f: + f.write(CARGO_TOML_LINTS) + console.print(" [dim]appended[/] [lints] to Cargo.toml") + + # ── .cargo/config.toml ─────────────────────────────────────────── + cargo_config_dir = project_dir / ".cargo" + cargo_config_dir.mkdir(parents=True, exist_ok=True) + (cargo_config_dir / "config.toml").write_text(CARGO_CONFIG_TOML) + console.print(" [dim]wrote[/] .cargo/config.toml") + + # ── deny.toml ──────────────────────────────────────────────────── + (project_dir / "deny.toml").write_text(DENY_TOML) + console.print(" [dim]wrote[/] deny.toml") + + # ── rustfmt.toml ───────────────────────────────────────────────── + (project_dir / "rustfmt.toml").write_text(RUSTFMT_TOML) + console.print(" [dim]wrote[/] rustfmt.toml") + + console.print(f"\n[bold green]Done![/] cd {project_dir} && cargo check") + + +if __name__ == "__main__": + app()