열거형과 패턴 매칭 문서의 검증 코드다.

//! 열거형과 패턴 매칭 문서의 주장을 확인한다: 데이터를 가진 변형, 빠짐없는 match(E0004),
//! 가드와 빠짐없음 검사, @ 바인딩, if let·let-else·let 체인(2024 에디션), matches!,
//! #[non_exhaustive](크레이트 두 개로), C 공용체(union)와의 크기 비교, clippy 린트.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/11_enums-pattern-matching_verify.rs"

use std::mem::size_of;

#[derive(Debug)]
enum Shape {
    Circle(f64),
    Rect(f64, f64),
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => 3.0 * r * r, // 단순화를 위해 원주율 3
            Shape::Rect(w, h) => w * h,
        }
    }
}

#[allow(dead_code)]
union ShapeData {
    circle: f64,
    rect: [f64; 2],
}

#[derive(Debug)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

fn describe(m: &Message) -> String {
    match m {
        Message::Quit => "종료".to_string(),
        Message::Move { x: 0, y } => format!("세로로 {y}"),
        Message::Move { x, y } => format!("({x}, {y})로"),
        Message::Write(text) if text.is_empty() => "빈 글".to_string(),
        Message::Write(text) => format!("글: {text}"),
        Message::ChangeColor(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
    }
}

fn grade(score: u32) -> String {
    match score {
        s @ 90..=100 => format!("A({s})"),
        s @ 80..=89 => format!("B({s})"),
        0..=79 => "C".to_string(),
        s => format!("잘못된 점수 {s}"),
    }
}

/// let-else: 패턴이 안 맞으면 else가 반드시 빠져나간다.
fn parse_pair(line: &str) -> Result<(String, i64), String> {
    let Some((key, value)) = line.split_once('=') else {
        return Err(format!("'=' 없음: {line}"));
    };
    let Ok(n) = value.trim().parse::<i64>() else {
        return Err(format!("숫자 아님: {}", value.trim()));
    };
    Ok((key.trim().to_string(), n))
}

/// let 체인 (Rust 1.88, 2024 에디션)
fn port_of(line: &str) -> Option<u16> {
    if let Some((k, v)) = line.split_once('=')
        && k.trim() == "port"
        && let Ok(p) = v.trim().parse::<u16>()
        && p != 0
    {
        Some(p)
    } else {
        None
    }
}

fn compile_with(code: &str, tool: &str, extra: &[&str], tag: &str) -> String {
    let dir = std::env::temp_dir().join(format!("vr11_{}_{}", 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(["--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 compile(code: &str, tag: &str) -> String {
    compile_with(code, "rustc", &["--edition", "2024"], tag)
}

fn main() {
    // 1. 데이터를 가진 변형과 match
    let shapes = [Shape::Circle(1.0), Shape::Rect(2.0, 3.0)];
    let areas: Vec<f64> = shapes.iter().map(Shape::area).collect();
    assert_eq!(areas, vec![3.0, 6.0]);
    println!("{:?} {:?}", shapes[0], areas);

    // 2. C 공용체와 비교: enum은 공용체 + 어느 변형인지 적는 칸(태그)
    println!(
        "size_of: union {} / enum Shape {} / Option<Shape> {}",
        size_of::<ShapeData>(),
        size_of::<Shape>(),
        size_of::<Option<Shape>>()
    );
    assert_eq!(size_of::<ShapeData>(), 16);
    assert_eq!(size_of::<Shape>(), 24);
    println!("size_of::<Message>() = {}", size_of::<Message>());

    // 공용체 필드 읽기는 unsafe 없이 안 된다: E0133
    let e = compile(
        "pub union U { pub a: u32, pub b: f32 }\npub fn f(u: U) -> u32 { u.a }",
        "union",
    );
    assert!(e.contains("E0133"), "{e}");
    println!("union 필드 읽기: E0133");

    // 3. 빠짐없는 match: 변형 하나를 빠뜨리면 E0004
    let e = compile(
        "pub enum S { A, B, C }\npub fn f(s: S) -> u8 { match s { S::A => 1, S::B => 2 } }",
        "e0004",
    );
    assert!(e.contains("E0004"), "{e}");
    assert!(e.contains("`S::C` not covered"), "{e}");
    println!("변형 빠뜨림: E0004 `S::C` not covered");

    // 가드는 빠짐없음 검사에 들어가지 않는다
    let e = compile(
        "pub fn f(x: Option<i32>) -> i32 { match x { Some(n) if n >= 0 => n, Some(n) if n < 0 => -n, None => 0 } }",
        "guard",
    );
    assert!(e.contains("E0004"), "{e}");
    println!("가드 두 개로 모든 정수를 덮어도: E0004");

    // 4. describe와 grade (확인 문제 C3)
    let msgs = [
        Message::Move { x: 0, y: 5 },
        Message::Move { x: 2, y: 5 },
        Message::Write(String::new()),
        Message::Write("hi".into()),
        Message::ChangeColor(255, 0, 16),
        Message::Quit,
    ];
    let d: Vec<String> = msgs.iter().map(describe).collect();
    println!("describe: {d:?}");
    assert_eq!(
        d,
        vec!["세로로 5", "(2, 5)로", "빈 글", "글: hi", "#ff0010", "종료"]
    );
    let g: Vec<String> = [95, 90, 89, 79, 101].into_iter().map(grade).collect();
    println!("C3 grade: {}", g.join(" "));
    assert_eq!(g.join(" "), "A(95) A(90) B(89) C 잘못된 점수 101");

    // 5. if let / let-else / let 체인
    let config_max = Some(3u8);
    let mut seen = 0;
    if let Some(max) = config_max {
        seen = max;
    }
    assert_eq!(seen, 3);
    assert_eq!(parse_pair("retries = 3"), Ok(("retries".into(), 3)));
    assert_eq!(parse_pair("retries"), Err("'=' 없음: retries".into()));
    assert_eq!(parse_pair("retries = x"), Err("숫자 아님: x".into()));

    // Option을 돌려주는 함수에서 else { return None }만 하면 clippy::question_mark가 ?를 권한다
    let e = compile_with(
        "pub fn f(s: &str) -> Option<usize> { let Some(i) = s.find('=') else { return None; }; Some(i) }",
        "clippy-driver",
        &["--edition", "2024"],
        "clipq",
    );
    assert!(e.contains("clippy::question_mark"), "{e}");
    assert_eq!(port_of("port = 8080"), Some(8080));
    assert_eq!(port_of("port = 0"), None);
    assert_eq!(port_of("host = a"), None);

    // let-else의 else가 빠져나가지 않으면 E0308
    let e = compile(
        "pub fn f(x: Option<i32>) -> i32 { let Some(n) = x else { 0 }; n }",
        "letelse",
    );
    assert!(e.contains("E0308"), "{e}");
    assert!(e.contains("does not diverge"), "{e}");
    println!("let-else의 else가 안 빠져나감: E0308 does not diverge");

    // let 체인은 2024 에디션에서만
    let chain = "pub fn f(x: Option<i32>) -> i32 { if let Some(n) = x && n > 0 { n } else { 0 } }";
    let e2024 = compile(chain, "chain24");
    assert!(!e2024.contains("error"), "{e2024}");
    let e2021 = compile_with(chain, "rustc", &["--edition", "2021"], "chain21");
    assert!(e2021.contains("error"), "{e2021}");
    println!(
        "let 체인 2021 에디션 오류: {}",
        e2021.lines().next().unwrap_or("")
    );

    // 6. matches!
    let bar = Some(4);
    assert!(matches!(bar, Some(x) if x > 2));
    let quit_count = msgs.iter().filter(|m| matches!(m, Message::Quit)).count();
    assert_eq!(quit_count, 1);

    // 7. #[non_exhaustive]: 정의한 크레이트 밖에서는 와일드카드가 필요하다
    let dir = std::env::temp_dir().join(format!("vr11_{}_ne", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let up = dir.join("upstream.rs");
    std::fs::write(
        &up,
        "#[non_exhaustive]\npub enum Error { Message(String), Other }\npub fn inside(e: &Error) -> u8 { match e { Error::Message(_) => 1, Error::Other => 2 } }",
    )
    .unwrap();
    let out = std::process::Command::new("rustc")
        .args([
            "--edition",
            "2024",
            "--crate-type=rlib",
            "--crate-name",
            "upstream",
            "--out-dir",
        ])
        .arg(&dir)
        .arg(&up)
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "{}",
        String::from_utf8_lossy(&out.stderr)
    );
    let rlib = dir.join("libupstream.rlib");
    let ext = format!("upstream={}", rlib.display());
    let down_no = "pub fn f(e: &upstream::Error) -> u8 { match e { upstream::Error::Message(_) => 1, upstream::Error::Other => 2 } }";
    let e = compile_with(
        down_no,
        "rustc",
        &["--edition", "2024", "--extern", &ext],
        "down1",
    );
    assert!(e.contains("E0004"), "{e}");
    let down_ok = "pub fn f(e: &upstream::Error) -> u8 { match e { upstream::Error::Message(_) => 1, upstream::Error::Other => 2, _ => 3 } }";
    let e = compile_with(
        down_ok,
        "rustc",
        &["--edition", "2024", "--extern", &ext],
        "down2",
    );
    assert!(!e.contains("error"), "{e}");
    println!("#[non_exhaustive] 밖에서 와일드카드 없이: E0004, 안에서는 통과");

    // 8. clippy: bool을 돌려주는 match는 matches!로 (match_like_matches_macro, 기본 켜짐)
    let e = compile_with(
        "pub enum S { A, B, C }\npub fn f(s: &S) -> bool { match s { S::A | S::B => true, _ => false } }",
        "clippy-driver",
        &["--edition", "2024"],
        "clip1",
    );
    assert!(e.contains("clippy::match_like_matches_macro"), "{e}");
    // wildcard_enum_match_arm은 restriction 그룹이라 켜야 나온다
    let code = "pub enum S { A, B, C }\npub fn f(s: &S) -> u8 { match s { S::A => 1, _ => 0 } }";
    let off = compile_with(code, "clippy-driver", &["--edition", "2024"], "clip2");
    // "error"가 없어야 조각이 실제로 컴파일된 것이다(호출이 실패해도 린트 이름은 안 나온다).
    assert!(
        !off.contains("error") && !off.contains("wildcard_enum_match_arm"),
        "{off}"
    );
    let on = compile_with(
        code,
        "clippy-driver",
        &["--edition", "2024", "-W", "clippy::wildcard_enum_match_arm"],
        "clip3",
    );
    // 린트 이름이 틀리면 "unknown lint: ...wildcard..." 경고가 나오므로 린트의 메시지로 확인한다.
    assert!(
        on.contains("wildcard match will also match any future added variants"),
        "{on}"
    );
    println!("clippy: match_like_matches_macro 기본 경고, wildcard_enum_match_arm 켜야 경고");

    println!("ALL CHECKS PASSED");
}