Skip to main content

xbp_analysis/languages/rust/
adapter.rs

1use super::extractor::extract_concepts;
2use crate::domain::capabilities::CapabilitySet;
3use crate::domain::model::{FileModel, LanguageModel, ParseResult};
4use crate::domain::types::{LanguageId, SourceFile};
5use crate::ports::inbound::LanguageAdapter;
6use std::path::Path;
7
8#[derive(Default)]
9pub struct RustAdapter;
10
11impl LanguageAdapter for RustAdapter {
12    fn id(&self) -> LanguageId {
13        LanguageId::rust()
14    }
15
16    fn capabilities(&self) -> CapabilitySet {
17        CapabilitySet::rust_syn_default()
18    }
19
20    fn detect(&self, path: &Path) -> bool {
21        path.extension().and_then(|e| e.to_str()) == Some("rs")
22    }
23
24    fn parse(&self, source: &SourceFile) -> ParseResult {
25        let ok = syn::parse_file(&source.content).is_ok();
26        ParseResult {
27            file: source.clone(),
28            ok,
29            diagnostics: if ok {
30                vec![]
31            } else {
32                vec!["syn parse failed".into()]
33            },
34        }
35    }
36
37    fn build_model(&self, sources: &[SourceFile]) -> Result<LanguageModel, String> {
38        let mut model = LanguageModel::new(LanguageId::rust());
39        for src in sources {
40            model.files.push(self.extract_file_model(src)?);
41        }
42        Ok(model)
43    }
44
45    fn extract_file_model(&self, source: &SourceFile) -> Result<FileModel, String> {
46        let generated = looks_generated(source);
47        let concepts = extract_concepts(source)?;
48        Ok(FileModel {
49            path: source.path.clone(),
50            language: LanguageId::rust(),
51            concepts,
52            control_flow: Default::default(),
53            error_paths: vec![],
54            generated,
55        })
56    }
57}
58
59fn looks_generated(source: &SourceFile) -> bool {
60    let path = source.path.to_string_lossy();
61    path.contains("generated")
62        || path.ends_with(".pb.rs")
63        || source.content.starts_with("// @generated")
64        || source.content.contains("DO NOT EDIT")
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::domain::concepts::ConceptKind;
71    use std::path::PathBuf;
72
73    fn src(code: &str) -> SourceFile {
74        SourceFile {
75            path: PathBuf::from("fixture.rs"),
76            content: code.into(),
77            language: LanguageId::rust(),
78        }
79    }
80
81    #[test]
82    fn detects_unwrap() {
83        let a = RustAdapter;
84        let m = a
85            .extract_file_model(&src("fn f() { let x = Ok::<(), ()>(()).unwrap(); }"))
86            .unwrap();
87        assert!(m
88            .concepts
89            .iter()
90            .any(|c| c.kind == ConceptKind::Termination && c.tag == "unwrap"));
91    }
92
93    #[test]
94    fn detects_ignored_let_underscore() {
95        let a = RustAdapter;
96        let m = a
97            .extract_file_model(&src(
98                r#"fn f() -> Result<(), ()> { let _ = std::fs::read("x"); Ok(()) }"#,
99            ))
100            .unwrap();
101        assert!(m
102            .concepts
103            .iter()
104            .any(|c| c.kind == ConceptKind::IgnoredFailure));
105    }
106
107    #[test]
108    fn detects_panic_macro() {
109        let a = RustAdapter;
110        let m = a
111            .extract_file_model(&src(r#"fn f() { panic!("boom"); }"#))
112            .unwrap();
113        assert!(m
114            .concepts
115            .iter()
116            .any(|c| c.tag == "panic" && c.kind == ConceptKind::Termination));
117    }
118
119    #[test]
120    fn detects_todo() {
121        let a = RustAdapter;
122        let m = a
123            .extract_file_model(&src(r#"fn f() { todo!("later"); }"#))
124            .unwrap();
125        assert!(m.concepts.iter().any(|c| c.kind == ConceptKind::Placeholder));
126    }
127
128    #[test]
129    fn detects_spawn() {
130        let a = RustAdapter;
131        let m = a
132            .extract_file_model(&src(
133                r#"fn f() { tokio::spawn(async { }); }"#,
134            ))
135            .unwrap();
136        assert!(m
137            .concepts
138            .iter()
139            .any(|c| c.kind == ConceptKind::UnobservedTask));
140    }
141
142    #[test]
143    fn negative_ok_path() {
144        let a = RustAdapter;
145        let m = a
146            .extract_file_model(&src(
147                r#"fn f() -> Result<(), std::io::Error> { let _data = std::fs::read("x")?; Ok(()) }"#,
148            ))
149            .unwrap();
150        // `?` alone should not be IgnoredFailure
151        assert!(!m
152            .concepts
153            .iter()
154            .any(|c| c.kind == ConceptKind::IgnoredFailure));
155    }
156}