sofie-model 0.1.0

The app model: modules, areas and routes, read from source without compiling it.
Documentation
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use syn::ext::IdentExt;
use syn::{Fields, Item, ItemImpl, Type};

use crate::areas::{associated_type, constant_value};
use crate::handlers::HandlerScope;
use crate::syntax::{last_name, line_of, public_uses, segments, string_literal};
use crate::{Crate, Provision};

pub(crate) struct ImplSite {
    pub(crate) trait_path: Vec<String>,
    pub(crate) ty: Vec<String>,
    pub(crate) module_path: Vec<String>,
    pub(crate) file: PathBuf,
    pub(crate) line: usize,
    pub(crate) vendor: Option<String>,
    pub(crate) settings: Option<String>,
}

pub(crate) fn impl_site(
    implementation: &ItemImpl,
    file: &Path,
    module_path: &[String],
) -> Option<ImplSite> {
    let (trait_path, _) = implementation.trait_.as_ref()?;
    let Type::Path(ty) = implementation.self_ty.as_ref() else {
        return None;
    };
    if !implementation.generics.params.is_empty() {
        return None;
    }
    Some(ImplSite {
        trait_path: segments(trait_path),
        ty: segments(&ty.path),
        module_path: module_path.to_vec(),
        file: file.to_path_buf(),
        line: line_of(implementation),
        vendor: constant_value(implementation, "VENDOR").and_then(string_literal),
        settings: associated_type(implementation, "Settings").and_then(last_name),
    })
}

pub(crate) fn unit_struct(item: &Item, module_path: &[String]) -> Option<Vec<String>> {
    match item {
        Item::Struct(structure) if matches!(structure.fields, Fields::Unit) => {
            let name = structure.ident.unraw().to_string();
            Some([module_path, std::slice::from_ref(&name)].concat())
        }
        _ => None,
    }
}

pub(crate) struct RootExports {
    named: BTreeMap<Vec<String>, String>,
    globs: Vec<Vec<String>>,
}

impl RootExports {
    pub(crate) fn of(root: Option<&syn::File>) -> Self {
        let items = root.map(|root| root.items.as_slice()).unwrap_or_default();
        let mut exports = Self {
            named: BTreeMap::new(),
            globs: Vec::new(),
        };
        for used in public_uses(items) {
            let mut path = used.segments;
            if matches!(path.first().map(String::as_str), Some("crate" | "self")) {
                path.remove(0);
            }
            match used.binding {
                Some(binding) => {
                    exports.named.insert(path, binding);
                }
                None => exports.globs.push(path),
            }
        }
        exports
    }

    pub(crate) fn public(&self, path: &[String]) -> Vec<String> {
        if let Some(name) = self.named.get(path) {
            return vec![name.clone()];
        }
        match path.split_last() {
            Some((name, module)) if self.globs.iter().any(|glob| glob == module) => {
                vec![name.clone()]
            }
            _ => path.to_vec(),
        }
    }
}

pub(crate) fn provisions(
    sites: &[ImplSite],
    units: &[Vec<String>],
    syntax: &BTreeMap<PathBuf, syn::File>,
    exports: &RootExports,
    krate: &Crate,
    contract: Option<&str>,
) -> (Vec<Provision>, Vec<Provision>) {
    let (mut implementations, mut extensions) = (Vec::new(), Vec::new());
    for site in sites {
        let items = syntax.get(&site.file).map_or(&[][..], |file| &file.items);
        let scope = HandlerScope::new(site.module_path.clone(), items);
        let Ok(trait_path) = scope.resolve(&site.trait_path) else {
            continue;
        };
        let ty = scope.within(&site.ty);
        let Some(first) = trait_path.first().filter(|_| trait_path.len() > 1) else {
            continue;
        };
        let provision = Provision {
            unit: units.contains(&ty),
            ty: exports.public(&ty),
            contract: trait_path.clone(),
            file: site.file.clone(),
            line: site.line,
        };
        if Some(first.as_str()) == contract {
            implementations.push(provision);
        } else if site
            .module_path
            .first()
            .is_some_and(|module| module == "extensions")
            && krate
                .dependencies
                .iter()
                .any(|dependency| &dependency.crate_name == first)
        {
            extensions.push(provision);
        }
    }
    (implementations, extensions)
}