xbp-analysis 10.57.0

Language-agnostic static analysis (error-handling / Aspirator-style) for XBP
Documentation
use super::extractor::extract_concepts;
use crate::domain::capabilities::CapabilitySet;
use crate::domain::model::{FileModel, LanguageModel, ParseResult};
use crate::domain::types::{LanguageId, SourceFile};
use crate::ports::inbound::LanguageAdapter;
use std::path::Path;

#[derive(Default)]
pub struct RustAdapter;

impl LanguageAdapter for RustAdapter {
    fn id(&self) -> LanguageId {
        LanguageId::rust()
    }

    fn capabilities(&self) -> CapabilitySet {
        CapabilitySet::rust_syn_default()
    }

    fn detect(&self, path: &Path) -> bool {
        path.extension().and_then(|e| e.to_str()) == Some("rs")
    }

    fn parse(&self, source: &SourceFile) -> ParseResult {
        let ok = syn::parse_file(&source.content).is_ok();
        ParseResult {
            file: source.clone(),
            ok,
            diagnostics: if ok {
                vec![]
            } else {
                vec!["syn parse failed".into()]
            },
        }
    }

    fn build_model(&self, sources: &[SourceFile]) -> Result<LanguageModel, String> {
        let mut model = LanguageModel::new(LanguageId::rust());
        for src in sources {
            model.files.push(self.extract_file_model(src)?);
        }
        Ok(model)
    }

    fn extract_file_model(&self, source: &SourceFile) -> Result<FileModel, String> {
        let generated = looks_generated(source);
        let concepts = extract_concepts(source)?;
        Ok(FileModel {
            path: source.path.clone(),
            language: LanguageId::rust(),
            concepts,
            control_flow: Default::default(),
            error_paths: vec![],
            generated,
        })
    }
}

fn looks_generated(source: &SourceFile) -> bool {
    let path = source.path.to_string_lossy();
    path.contains("generated")
        || path.ends_with(".pb.rs")
        || source.content.starts_with("// @generated")
        || source.content.contains("DO NOT EDIT")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::concepts::ConceptKind;
    use std::path::PathBuf;

    fn src(code: &str) -> SourceFile {
        SourceFile {
            path: PathBuf::from("fixture.rs"),
            content: code.into(),
            language: LanguageId::rust(),
        }
    }

    #[test]
    fn detects_unwrap() {
        let a = RustAdapter;
        let m = a
            .extract_file_model(&src("fn f() { let x = Ok::<(), ()>(()).unwrap(); }"))
            .unwrap();
        assert!(m
            .concepts
            .iter()
            .any(|c| c.kind == ConceptKind::Termination && c.tag == "unwrap"));
    }

    #[test]
    fn detects_ignored_let_underscore() {
        let a = RustAdapter;
        let m = a
            .extract_file_model(&src(
                r#"fn f() -> Result<(), ()> { let _ = std::fs::read("x"); Ok(()) }"#,
            ))
            .unwrap();
        assert!(m
            .concepts
            .iter()
            .any(|c| c.kind == ConceptKind::IgnoredFailure));
    }

    #[test]
    fn detects_panic_macro() {
        let a = RustAdapter;
        let m = a
            .extract_file_model(&src(r#"fn f() { panic!("boom"); }"#))
            .unwrap();
        assert!(m
            .concepts
            .iter()
            .any(|c| c.tag == "panic" && c.kind == ConceptKind::Termination));
    }

    #[test]
    fn detects_todo() {
        let a = RustAdapter;
        let m = a
            .extract_file_model(&src(r#"fn f() { todo!("later"); }"#))
            .unwrap();
        assert!(m.concepts.iter().any(|c| c.kind == ConceptKind::Placeholder));
    }

    #[test]
    fn detects_spawn() {
        let a = RustAdapter;
        let m = a
            .extract_file_model(&src(
                r#"fn f() { tokio::spawn(async { }); }"#,
            ))
            .unwrap();
        assert!(m
            .concepts
            .iter()
            .any(|c| c.kind == ConceptKind::UnobservedTask));
    }

    #[test]
    fn negative_ok_path() {
        let a = RustAdapter;
        let m = a
            .extract_file_model(&src(
                r#"fn f() -> Result<(), std::io::Error> { let _data = std::fs::read("x")?; Ok(()) }"#,
            ))
            .unwrap();
        // `?` alone should not be IgnoredFailure
        assert!(!m
            .concepts
            .iter()
            .any(|c| c.kind == ConceptKind::IgnoredFailure));
    }
}