dvb-si 10.0.0

ETSI EN 300 468 DVB Service Information parser + builder. MPEG-2 PSI included.
Documentation
//! Drift-guard for the spec/field-enum label convention (issue #204).
//!
//! Scans this crate's `src/` for every `pub enum`, subtracts a documented
//! skip-list, and fails if any remaining enum has neither
//! `broadcast_common::impl_spec_display!(Name)` nor a hand-written `Display` impl.
//!
//! Because the project-wide `Display` impl delegates to an inherent
//! `name() -> &'static str`, a present `Display` transitively guarantees
//! `name()` exists (it would not compile otherwise) — so this single coverage
//! check enforces the whole convention and catches the one thing the compiler
//! cannot: a brand-new `pub enum` that nobody labelled.
//!
//! This guard checks **per file**, not over the whole crate concatenated.
//! `ReferenceType` is declared independently by both `tables::protection_message`
//! and `descriptors::announcement_support` (different variant sets), and
//! `LinkType` is declared independently by both `tables::rct` and
//! `descriptors::linkage` — a whole-crate scan would let one module's
//! `impl_spec_display!` satisfy the check for both same-named enums, so
//! dropping either impl would go unnoticed. `impl_spec_display!(Name)`
//! resolves `Name` in the module it is invoked from, so per-file is also the
//! semantically correct scope.

use std::collections::BTreeSet;
use std::fs;
use std::path::Path;

/// Enums that are intentionally **not** spec/field labels. Each is one of:
/// a structured error; a dispatch/registry wrapper or tag enum (the `declare_*`
/// system, `Any*`); a section-kind discriminant; or a data-carrying ADT whose
/// variants hold payloads (a static label would be lossy and add nothing — use
/// the typed variant instead).
const SKIP: &[&str] = &[
    // errors + dispatch wrappers / tag enums
    "Error",
    "AnyDescriptor",
    "AnyAitDescriptor",
    "AnyTableSection",
    "DescriptorTag",
    "TableId",
    "ExtensionTag",
    "SatTableId",
    // data-carrying / wrapper / body ADTs
    "BiopMessage",
    "CarouselObject",
    "TaggedProfile",
    "ObjectKind",
    "UnMessage",
    "CollectError",
    "CompletedEit",
    "SatBody",
    "ProtectionMessageBody",
    "FontInfo",
    "DvbLocatorService",
    "DvbLocatorIdentifier",
    "ExtIterItem",
    "RegisteredExtension",
    "ExtensionBody",
    "TargetId",
    "LinkageData",
    "FormatSpecifier",
    "IdSelector",
    "CridLocation",
    "DepthRangeBody",
    "ImageIconBody",
    "IconLocation",
    "RegionCodes",
    "CellLinkage",
    "VbiService",
    "ShModulationMode",
    "ShInterleaver",
    "PositionSystem",
    "BeamhoppingMode",
    "DecoderConfig",
    // internal section-kind discriminants + config (not wire-field labels)
    "EitKind",
    "NitKind",
    "SdtKind",
    "PacketStride",
];

/// Every `.rs` file under `src/`, as `(display path, contents)`.
fn read_rs(dir: &Path, out: &mut Vec<(String, String)>) {
    for entry in fs::read_dir(dir).expect("read src dir") {
        let path = entry.expect("dir entry").path();
        if path.is_dir() {
            read_rs(&path, out);
        } else if path.extension().is_some_and(|x| x == "rs") {
            let text = fs::read_to_string(&path).expect("read .rs");
            out.push((path.display().to_string(), text));
        }
    }
}

/// True if `name` appears after `prefix` with an identifier boundary, i.e. the
/// match is the whole enum name and not a longer one sharing the prefix.
fn has_impl(src: &str, prefix: &str, name: &str) -> bool {
    let needle = format!("{prefix}{name}");
    let is_boundary =
        |rest: &str| !matches!(rest.chars().next(), Some(c) if c.is_alphanumeric() || c == '_');
    let is_path_or_space =
        |c: char| c.is_whitespace() || c == ':' || c.is_alphanumeric() || c == '_';

    // Strip a leading `ident::` chain (`crate::`, `broadcast_common::`, …) so a
    // re-exported or fully-qualified invocation still counts as reaching the
    // needle from the very start of the line.
    fn strip_path_qualifier(s: &str) -> &str {
        let mut rest = s;
        loop {
            let ident_len = rest
                .chars()
                .take_while(|c| c.is_alphanumeric() || *c == '_')
                .count();
            if ident_len == 0 {
                break;
            }
            match rest[ident_len..].strip_prefix("::") {
                Some(after) => rest = after,
                None => break,
            }
        }
        rest
    }

    for line in src.lines() {
        let trimmed = line.trim_start();

        // The invocation must be the first non-whitespace token on its line —
        // a commented-out `// impl_spec_display!(...)` no longer satisfies
        // this. A leading crate-path qualifier (`crate::`, `broadcast_common::`)
        // is transparent to this check: it is still the first *statement*.
        if let Some(rest) = strip_path_qualifier(trimmed).strip_prefix(&needle)
            && is_boundary(rest)
        {
            return true;
        }

        // A bare `Display for Name` needle (the generic fallback some crates
        // use) also counts when reached from the `impl` keyword through
        // nothing but a module-path qualifier: `impl ::core::fmt::Display for
        // Name`, `impl std::fmt::Display for Name`, `impl fmt::Display for
        // Name`, `impl Display for Name`.
        if !needle.starts_with("impl")
            && let Some(after_impl) = trimmed.strip_prefix("impl")
            && let Some(pos) = after_impl.find(&needle)
        {
            let qualifier = &after_impl[..pos];
            let rest = &after_impl[pos + needle.len()..];
            if qualifier.chars().all(is_path_or_space) && is_boundary(rest) {
                return true;
            }
        }
    }

    false
}

fn pub_enums(src: &str) -> BTreeSet<String> {
    let mut enums = BTreeSet::new();
    for line in src.lines() {
        if let Some(rest) = line.trim_start().strip_prefix("pub enum ") {
            let name: String = rest
                .chars()
                .take_while(|c| c.is_alphanumeric() || *c == '_')
                .collect();
            if !name.is_empty() {
                enums.insert(name);
            }
        }
    }
    enums
}

fn crate_files() -> Vec<(String, String)> {
    let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    let mut files = Vec::new();
    read_rs(&src, &mut files);
    files
}

#[test]
fn every_public_spec_enum_has_a_display_impl() {
    let mut missing: Vec<String> = Vec::new();

    for (path, src) in &crate_files() {
        for e in pub_enums(src) {
            if SKIP.contains(&e.as_str()) {
                continue;
            }
            if !has_impl(src, "impl_spec_display!(", &e) && !has_impl(src, "Display for ", &e) {
                missing.push(format!("{e} (in {path})"));
            }
        }
    }

    assert!(
        missing.is_empty(),
        "pub enum(s) missing a Display impl (issue #204 convention): {missing:?}\n\
         Add `broadcast_common::impl_spec_display!(Name)` plus an inherent `name()`, \
         or add the enum to SKIP if it is not a spec/field label."
    );
}