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();
assert!(!m
.concepts
.iter()
.any(|c| c.kind == ConceptKind::IgnoredFailure));
}
}