Option과 Result 문서의 검증 코드다.

//! Option과 Result 문서의 주장을 확인한다: Option<i8> + i8 금지(E0277), 결합 메서드의 결과,
//! unwrap_or의 즉시 계산과 unwrap_or_else의 지연 계산, ?로 일찍 돌려주기,
//! 빈 자리 최적화 크기(Option<&T> == &T 등), unwrap 패닉 메시지, must_use 경고, clippy 린트.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/12_option-result_verify.rs"

// 글자 그대로의 Some/None에 결합 메서드를 부르는 것은 동작을 보이려는 것이다.
#![allow(clippy::unnecessary_literal_unwrap)]

use std::cell::Cell;
use std::collections::HashMap;
use std::mem::size_of;
use std::num::NonZero;

#[derive(Debug, PartialEq)]
enum LookupError {
    NoSuchUser(u32),
    BadAge(String),
}

fn find_name(db: &HashMap<u32, &str>, id: u32) -> Option<String> {
    db.get(&id).map(|s| s.to_string())
}

fn parse_age(text: &str) -> Result<u8, LookupError> {
    text.trim()
        .parse::<u8>()
        .map_err(|_| LookupError::BadAge(text.to_string()))
}

/// ?는 None이나 Err를 만나면 그 자리에서 함수를 끝내고 돌려준다.
fn greeting(db: &HashMap<u32, &str>, id: u32, age_text: &str) -> Result<String, LookupError> {
    let name = find_name(db, id).ok_or(LookupError::NoSuchUser(id))?;
    let age = parse_age(age_text)?;
    Ok(format!("{name}({age})"))
}

fn compile_with(code: &str, tool: &str, extra: &[&str], tag: &str) -> String {
    let dir = std::env::temp_dir().join(format!("vr12_{}_{}", std::process::id(), tag));
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("snippet.rs");
    std::fs::write(&file, code).unwrap();
    let out = std::process::Command::new(tool)
        .args([
            "--edition",
            "2024",
            "--crate-type=lib",
            "--emit=metadata",
            "--out-dir",
        ])
        .arg(&dir)
        .args(extra)
        .arg(&file)
        .output()
        .expect("rustc 실행");
    String::from_utf8_lossy(&out.stderr).into_owned()
}

fn main() {
    // 1. Option<i8>과 i8은 더할 수 없다: E0277
    let e = compile_with(
        "pub fn f() -> i8 { let x: i8 = 5; let y: Option<i8> = Some(5); x + y }",
        "rustc",
        &[],
        "add",
    );
    assert!(e.contains("E0277"), "{e}");
    assert!(e.contains("cannot add `Option<i8>` to `i8`"), "{e}");
    println!("i8 + Option<i8>: E0277 cannot add `Option<i8>` to `i8`");

    // 2. 결합 메서드
    let some: Option<u32> = Some(4);
    let none: Option<u32> = None;
    assert_eq!(some.map(|x| x * 10), Some(40));
    assert_eq!(none.map(|x| x * 10), None);
    assert_eq!(some.and_then(|x| x.checked_sub(5)), None);
    assert_eq!(some.and_then(|x| x.checked_sub(1)), Some(3));
    assert_eq!(some.filter(|x| x % 2 == 0), Some(4));
    assert_eq!(none.ok_or("없음"), Err("없음"));
    assert_eq!(some.ok_or("없음"), Ok(4));
    assert_eq!(none.unwrap_or(7), 7);
    assert_eq!(none.unwrap_or_default(), 0);
    let r: Result<u32, String> = Err("x".into());
    assert_eq!(r.clone().ok(), None);
    assert_eq!(r.clone().unwrap_or_else(|e| e.len() as u32), 1);
    assert_eq!(r.map_err(|e| e.to_uppercase()), Err("X".to_string()));

    // 3. ?로 일찍 돌려주기
    let db: HashMap<u32, &str> = HashMap::from([(1, "ann")]);
    assert_eq!(greeting(&db, 1, "30"), Ok("ann(30)".into()));
    assert_eq!(greeting(&db, 2, "30"), Err(LookupError::NoSuchUser(2)));
    assert_eq!(
        greeting(&db, 1, "삼십"),
        Err(LookupError::BadAge("삼십".into()))
    );

    // 4. unwrap_or는 인자를 미리 계산한다, unwrap_or_else는 필요할 때만 (확인 문제 C3)
    let calls = Cell::new(0);
    let expensive = || {
        calls.set(calls.get() + 1);
        0u32
    };
    let a = Some(1u32).unwrap_or(expensive());
    let b = Some(1u32).unwrap_or_else(expensive);
    let c = None.unwrap_or_else(expensive);
    println!("C3: a={a} b={b} c={c} calls={}", calls.get());
    assert_eq!((a, b, c, calls.get()), (1, 1, 0, 2));

    // 5. 빈 자리 최적화: 보장 목록의 타입은 Option을 씌워도 크기가 그대로
    assert_eq!(size_of::<Option<&u64>>(), size_of::<&u64>());
    assert_eq!(size_of::<Option<&mut u64>>(), size_of::<&mut u64>());
    assert_eq!(size_of::<Option<Box<u64>>>(), size_of::<Box<u64>>());
    assert_eq!(size_of::<Option<fn()>>(), size_of::<fn()>());
    assert_eq!(size_of::<Option<NonZero<u32>>>(), 4);
    // 보장 목록에 없는 u32는 태그 자리가 따로 든다(관찰값)
    assert_eq!(size_of::<Option<u32>>(), 8);
    assert_eq!(size_of::<Result<NonZero<i32>, ()>>(), 4);
    println!(
        "size_of: &u64 {} / Option<&u64> {} / u32 {} / Option<u32> {} / Option<NonZero<u32>> {}",
        size_of::<&u64>(),
        size_of::<Option<&u64>>(),
        size_of::<u32>(),
        size_of::<Option<u32>>(),
        size_of::<Option<NonZero<u32>>>()
    );

    // 6. unwrap / expect 패닉 메시지
    std::panic::set_hook(Box::new(|_| {}));
    let msg = |r: Box<dyn std::any::Any + Send>| -> String {
        r.downcast_ref::<&str>()
            .map(|s| s.to_string())
            .or_else(|| r.downcast_ref::<String>().cloned())
            .unwrap_or_default()
    };
    let p = std::panic::catch_unwind(|| {
        let o: Option<u32> = None;
        o.unwrap()
    })
    .unwrap_err();
    let m1 = msg(p);
    println!("unwrap 패닉: {m1}");
    assert_eq!(m1, "called `Option::unwrap()` on a `None` value");
    let p = std::panic::catch_unwind(|| "x".parse::<u32>().unwrap()).unwrap_err();
    let m2 = msg(p);
    println!("Result unwrap 패닉: {m2}");
    assert!(m2.starts_with("called `Result::unwrap()` on an `Err` value"));
    let p = std::panic::catch_unwind(|| {
        let o: Option<u32> = None;
        o.expect("설정 파일에 port가 있어야 한다")
    })
    .unwrap_err();
    assert_eq!(msg(p), "설정 파일에 port가 있어야 한다");
    let _ = std::panic::take_hook();

    // 7. Result를 버리면 unused_must_use 경고
    let e = compile_with(
        "pub fn f() { \"5\".parse::<u32>(); }",
        "rustc",
        &[],
        "mustuse",
    );
    assert!(e.contains("unused `Result` that must be used") || e.contains("unused_must_use"));
    println!("Result 버림: unused_must_use 경고");

    // 8. clippy: unwrap_used는 restriction(기본 꺼짐), unnecessary_unwrap은 기본 켜짐
    let code = "pub fn f(o: Option<u32>) -> u32 { o.unwrap() }";
    let off = compile_with(code, "clippy-driver", &[], "u_off");
    // "error"가 없어야 조각이 실제로 컴파일된 것이다(호출이 실패해도 린트 이름은 안 나온다).
    assert!(
        !off.contains("error") && !off.contains("unwrap_used"),
        "{off}"
    );
    let on = compile_with(
        code,
        "clippy-driver",
        &["-W", "clippy::unwrap_used"],
        "u_on",
    );
    // 린트 이름만 보면 "unknown lint: `clippy::unwrap_used`" 경고와 구별되지 않으므로 메시지도 본다.
    assert!(
        on.contains("used `unwrap()` on an `Option` value") && on.contains("clippy::unwrap-used"),
        "{on}"
    );
    let e = compile_with(
        "pub fn f(o: Option<u32>) -> u32 { if o.is_some() { o.unwrap() } else { 0 } }",
        "clippy-driver",
        &[],
        "unn",
    );
    assert!(e.contains("clippy::unnecessary_unwrap"), "{e}");
    println!("clippy: unwrap_used 켜야 경고, unnecessary_unwrap 기본 경고");

    println!("ALL CHECKS PASSED");
}