hunyi 0.4.0

渾儀 (Hunyi) — Tianheng's semantic (AST/syn) observation dimension, the complement of the static import boundary. Declare in Rust how a module's public surface must behave: what its API must not expose (types — including named public re-exports and, opt-in, a trait impl's impl-site positions — and no dyn / impl Trait or async fn seam), where a trait may be implemented, that it declares no bare pub, and which markers a type must not acquire — observed via syn, reacted in CI. The heavy syn dependency is quarantined here, never in the core.
Documentation
//! Visibility (`semantic-visibility-boundary`): a module must not declare bare-`pub` items. Scan
//! the module's direct items and react to those declared bare-`pub`.

use std::path::{Path, PathBuf};

use serde_json::Value;
use xuanji::{Outcome, Violation};

use crate::driver::run_boundaries;
use crate::dsl::VisibilityBoundary;
use crate::emit::{SingleModuleViolationContext, push_single_module_violations};
use crate::errors::unknown_module_error;
use crate::file_scope::{is_anchor_absent_from_unit, resolve_crate_units};
use crate::finding::{SemanticFact, sort_faceted_facts};
use crate::module_resolve::resolve_module_items_with_files;
use crate::syn_util::item_observation;

/// Run the visibility boundaries against the Cargo workspace at `manifest_path`.
///
/// Mirrors [`crate::check`]: resolve each boundary's crate and module anchor, scan the module's
/// direct items for bare-`pub` declarations, and return the outcome. An unresolvable crate
/// or module (or an unreadable/unparseable source) is a constitution error (exit 2), never
/// a silent pass.
pub fn check_visibility(boundaries: &[VisibilityBoundary], manifest_path: &Path) -> Outcome {
    run_boundaries(boundaries, manifest_path, check_visibility_boundary)
}

pub(crate) fn check_visibility_boundary(
    metadata: &Value,
    boundary: &VisibilityBoundary,
    violations: &mut Vec<Violation>,
) -> Result<(), String> {
    let (_package, units) = resolve_crate_units(metadata, &boundary.crate_package)?;
    // Each of a package's crate roots is its own compilation unit: same module path `crate`,
    // separate module graph. Evaluated once per unit so an exposure in a `bin` beside a library
    // is observed, with the unit carried into each finding's identity.
    let mut governed_somewhere = false;
    let mut deferred: Option<String> = None;
    for (root_file, src_dir, unit) in &units {
        let unit_outcome = (|| -> Result<(), String> {
            let src_dir = src_dir.as_path();
            let unit = unit.as_str();

            let findings = visibility_findings(
                src_dir,
                root_file,
                &boundary.module,
                &boundary.crate_package,
                boundary.ceiling().rank(),
            )?;

            push_single_module_violations(
                violations,
                SingleModuleViolationContext {
                    module: &boundary.module,
                    rule: boundary.ceiling().rule(),
                    rule_key: boundary.rule_key(),
                    reason: &boundary.reason,
                    severity: boundary.severity,
                    anchor: boundary.anchor(),
                    crate_package: &boundary.crate_package,
                    unit,
                },
                findings,
            );
            Ok(())
        })();
        match unit_outcome {
            Ok(()) => governed_somewhere = true,
            Err(reason)
                if is_anchor_absent_from_unit(
                    &reason,
                    &unknown_module_error(&boundary.module, &boundary.crate_package),
                ) =>
            {
                if deferred.is_none() {
                    deferred = Some(reason);
                }
            }
            Err(reason) => return Err(reason),
        }
    }
    match deferred {
        Some(reason) if !governed_somewhere => Err(reason),
        _ => Ok(()),
    }
}

/// The pure heart, testable without spawning `cargo`: resolve the module's direct items and
/// return the sorted, deduplicated descriptions of those whose declared-visibility rank exceeds
/// `ceiling_rank` (the boundary's ceiling — `Crate`=2, `Super`=1, `Module`=0). Each finding pairs
/// with the real file its own item's branch was resolved from — never a single first-branch file
/// for the whole module, which would misattribute a finding produced by a non-first `#[cfg]`-split
/// branch.
pub(crate) fn visibility_findings(
    src_dir: &Path,
    root_file: &Path,
    module: &str,
    crate_package: &str,
    ceiling_rank: u8,
) -> Result<Vec<(SemanticFact, PathBuf)>, String> {
    let items_with_files =
        resolve_module_items_with_files(src_dir, root_file, module, crate_package)?;
    let mut findings: Vec<(SemanticFact, PathBuf)> = items_with_files
        .iter()
        .flat_map(|(item, file, _branch)| {
            item_observation(item, ceiling_rank)
                .into_iter()
                .map(move |obs| (obs, file))
        })
        .map(|((visibility, item_kind, item_name), file)| {
            (
                SemanticFact::Visibility {
                    visibility,
                    item_kind,
                    item_name,
                },
                file.clone(),
            )
        })
        .collect();
    sort_faceted_facts(&mut findings)?;
    Ok(findings)
}