xbp_analysis/languages/rust/
adapter.rs1use 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 assert!(!m
152 .concepts
153 .iter()
154 .any(|c| c.kind == ConceptKind::IgnoredFailure));
155 }
156}