qml_static_analyzer 0.2.0

A static analyzer for QML files
Documentation
//! Build script — embeds pre-generated qt_types JSON files into the binary.
//!
//! Set `INCLUDED_QT_TYPES` to a comma-separated list of paths to JSON files
//! generated by `qml_static_analyzer generate-qt-types`.  If the variable is
//! not set (or is empty), the build succeeds without any built-in Qt types —
//! only `--qt-types-json` will be available at runtime.
//!
//! Example:
//!   INCLUDED_QT_TYPES="qt_types_6.3.2.json,qt_types_6.8.3.json" cargo build

fn main() {
    println!("cargo:rerun-if-env-changed=INCLUDED_QT_TYPES");

    let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
    let mut entries: Vec<(String, String)> = vec![];

    if let Ok(paths_str) = std::env::var("INCLUDED_QT_TYPES") {
        for path_str in paths_str.split(',') {
            let path_str = path_str.trim();
            if path_str.is_empty() {
                continue;
            }

            println!("cargo:rerun-if-changed={path_str}");

            let json = std::fs::read_to_string(path_str)
                .unwrap_or_else(|e| panic!("INCLUDED_QT_TYPES: cannot read '{path_str}': {e}"));

            let version = extract_version_from_json(&json)
                .or_else(|| extract_version_from_filename(path_str))
                .unwrap_or_else(|| {
                    panic!(
                        "Cannot detect Qt version from '{path_str}'. \
                         Use a filename like qt_types_6.3.2.json, or run \
                         `generate-qt-types` which adds a _version field."
                    )
                });

            let safe_id = version.replace('.', "_");
            let dest_name = format!("qt_builtin_{safe_id}.json");
            let dest_path = format!("{out_dir}/{dest_name}");
            std::fs::write(&dest_path, &json).unwrap_or_else(|e| panic!("Cannot write '{dest_path}': {e}"));

            println!("cargo:warning=Embedded Qt {version} types from {path_str}");
            entries.push((version, dest_name));
        }
    }

    // Generate qt_types_builtin.rs — static slice of (version, json_content) pairs.
    let mut code = String::from(
        "/// Built-in Qt type databases compiled into the binary.\n\
         /// Each entry: (version_string, json_content).\n\
         pub static BUILTIN_QT_TYPES: &[(&str, &str)] = &[\n",
    );
    for (version, filename) in &entries {
        code.push_str(&format!(
            "    ({version:?}, include_str!(concat!(env!(\"OUT_DIR\"), \"/{filename}\"))),\n"
        ));
    }
    code.push_str("];\n");

    let out_rs = format!("{out_dir}/qt_types_builtin.rs");
    std::fs::write(&out_rs, &code).unwrap_or_else(|e| panic!("Cannot write '{out_rs}': {e}"));
}

/// Try to extract Qt version from `"_version": "X.Y.Z"` inside the JSON.
fn extract_version_from_json(json: &str) -> Option<String> {
    let start = json.find("\"_version\"")?;
    let after = json[start + "\"_version\"".len()..].trim_start();
    let after = after.strip_prefix(':')?;
    let after = after.trim_start().strip_prefix('"')?;
    let end = after.find('"')?;
    let ver = &after[..end];
    ver.contains('.').then(|| ver.to_string())
}

/// Try to extract a version like `6.3.2` from the filename
/// (e.g. `qt_types_6.3.2.json` → `6.3.2`).
fn extract_version_from_filename(path: &str) -> Option<String> {
    let stem = std::path::Path::new(path).file_stem()?.to_str()?;
    for seg in stem.split('_') {
        let parts: Vec<&str> = seg.split('.').collect();
        if parts.len() >= 2
            && parts
                .iter()
                .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
        {
            return Some(seg.to_string());
        }
    }
    None
}