use crate::ast::ASTRegistry;
use crate::context::ImHashMap;
use crate::symbol::{SymbolId, SymbolRegistry};
use crate::SymbolKind;
use ryo_source::pure::{
PureEnum, PureFields, PureFile, PureFn, PureGenerics, PureImpl, PureItem, PureParam,
PureStruct, PureTrait, PureType, PureVis,
};
use ryo_symbol::{SymbolPathResolver, WorkspaceFilePath};
use serde::{Deserialize, Serialize};
use slotmap::SecondaryMap;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenericInfo {
pub type_params: Vec<String>,
pub lifetimes: Vec<String>,
pub const_params: Vec<(String, String)>,
}
impl GenericInfo {
pub fn is_empty(&self) -> bool {
self.type_params.is_empty() && self.lifetimes.is_empty() && self.const_params.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ParamInfo {
pub name: String,
pub ty: String,
pub is_self: bool,
pub is_mut: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldInfo {
pub name: String,
pub ty: String,
pub is_public: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionDetail {
pub is_async: bool,
pub is_const: bool,
pub is_unsafe: bool,
pub params: Vec<ParamInfo>,
pub return_type: Option<String>,
pub generics: GenericInfo,
pub is_method: bool,
pub self_ty: Option<String>,
pub trait_impl: Option<String>,
pub has_self: bool,
#[serde(default)]
pub attrs: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StructKind {
Named,
Tuple,
Unit,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StructDetail {
pub fields: Vec<FieldInfo>,
pub kind: StructKind,
pub generics: GenericInfo,
#[serde(default)]
pub attrs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VariantInfo {
pub name: String,
pub fields: Vec<FieldInfo>,
pub discriminant: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnumDetail {
pub variants: Vec<VariantInfo>,
pub generics: GenericInfo,
#[serde(default)]
pub attrs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TraitDetail {
pub is_unsafe: bool,
pub is_auto: bool,
pub supertraits: Vec<String>,
pub methods: Vec<String>,
pub types: Vec<String>,
pub generics: GenericInfo,
#[serde(default)]
pub attrs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImplDetail {
pub is_unsafe: bool,
pub self_ty: String,
pub trait_: Option<String>,
pub methods: Vec<String>,
pub generics: GenericInfo,
#[serde(default)]
pub attrs: Vec<String>,
}
#[derive(Clone, Serialize)]
pub struct DetailStore {
functions: SecondaryMap<SymbolId, FunctionDetail>,
structs: SecondaryMap<SymbolId, StructDetail>,
enums: SecondaryMap<SymbolId, EnumDetail>,
traits: SecondaryMap<SymbolId, TraitDetail>,
impls: SecondaryMap<SymbolId, ImplDetail>,
}
impl DetailStore {
pub fn new() -> Self {
Self {
functions: SecondaryMap::new(),
structs: SecondaryMap::new(),
enums: SecondaryMap::new(),
traits: SecondaryMap::new(),
impls: SecondaryMap::new(),
}
}
#[inline]
pub fn function(&self, id: SymbolId) -> Option<&FunctionDetail> {
self.functions.get(id)
}
#[inline]
pub fn struct_(&self, id: SymbolId) -> Option<&StructDetail> {
self.structs.get(id)
}
#[inline]
pub fn enum_(&self, id: SymbolId) -> Option<&EnumDetail> {
self.enums.get(id)
}
#[inline]
pub fn trait_(&self, id: SymbolId) -> Option<&TraitDetail> {
self.traits.get(id)
}
#[inline]
pub fn impl_(&self, id: SymbolId) -> Option<&ImplDetail> {
self.impls.get(id)
}
#[deprecated(
since = "0.1.0",
note = "Use rebuild_for_symbols() for incremental updates. This is only for initial construction."
)]
pub fn build_all_workspace(
registry: &SymbolRegistry,
files: &HashMap<WorkspaceFilePath, PureFile>,
crate_name: &str,
) -> Self {
let mut store = Self::new();
let resolver = SymbolPathResolver::new(crate_name);
let module_file_map: HashMap<String, (&WorkspaceFilePath, &PureFile)> = files
.iter()
.map(|(path, file)| {
let mod_path = resolver.module_path_str(path);
(mod_path, (path, file))
})
.collect();
for (id, path) in registry.iter() {
if let Some(kind) = registry.kind(id) {
let symbol_name = path.name();
let parent_path = path.parent().map(|p| p.to_string()).unwrap_or_default();
let module_path = if parent_path.contains("<impl") {
path.parent()
.and_then(|p| p.parent())
.map(|p| p.to_string())
.unwrap_or_default()
} else {
parent_path
};
if let Some((_path, file)) = module_file_map.get(&module_path) {
store.extract_detail_direct(id, kind, symbol_name, file);
}
}
}
store
}
#[deprecated(
since = "0.1.0",
note = "Use rebuild_for_symbols() for incremental updates. This is only for initial construction."
)]
pub fn build_from_arc_files(
registry: &SymbolRegistry,
files: &ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
crate_name: &str,
) -> Self {
let mut store = Self::new();
let resolver = SymbolPathResolver::new(crate_name);
let module_file_map: HashMap<String, &PureFile> = files
.iter()
.map(|(path, file)| {
let mod_path = resolver.module_path_str(path);
(mod_path, file.as_ref())
})
.collect();
for (id, path) in registry.iter() {
if let Some(kind) = registry.kind(id) {
let symbol_name = path.name();
let parent_path = path.parent().map(|p| p.to_string()).unwrap_or_default();
let module_path = if parent_path.contains("<impl") {
path.parent()
.and_then(|p| p.parent())
.map(|p| p.to_string())
.unwrap_or_default()
} else {
parent_path
};
if let Some(file) = module_file_map.get(&module_path) {
store.extract_detail_direct(id, kind, symbol_name, file);
}
}
}
store
}
pub fn rebuild_affected_workspace(
&mut self,
affected: &[SymbolId],
registry: &SymbolRegistry,
files: &ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
crate_name: &str,
) {
let resolver = SymbolPathResolver::new(crate_name);
let module_file_map: HashMap<String, &PureFile> = files
.iter()
.map(|(path, file)| {
let mod_path = resolver.module_path_str(path);
(mod_path, file.as_ref())
})
.collect();
for &id in affected {
self.remove(id);
if let Some(kind) = registry.kind(id) {
if let Some(path) = registry.resolve(id) {
let symbol_name = path.name();
let parent_path = path.parent().map(|p| p.to_string()).unwrap_or_default();
let module_path = if parent_path.contains("<impl") {
path.parent()
.and_then(|p| p.parent())
.map(|p| p.to_string())
.unwrap_or_default()
} else {
parent_path
};
if let Some(file) = module_file_map.get(&module_path) {
self.extract_detail_direct(id, kind, symbol_name, file);
}
}
}
}
}
pub fn remove(&mut self, id: SymbolId) {
self.functions.remove(id);
self.structs.remove(id);
self.enums.remove(id);
self.traits.remove(id);
self.impls.remove(id);
}
pub fn rebuild_for_symbols(&mut self, affected_ids: &[SymbolId], ast_registry: &ASTRegistry) {
for &id in affected_ids {
self.remove(id);
if let Some(item) = ast_registry.get(id) {
self.extract_from_item(id, item);
}
}
}
fn extract_from_item(&mut self, id: SymbolId, item: &PureItem) {
match item {
PureItem::Fn(f) => {
let detail = build_function_detail(f, None, None);
self.functions.insert(id, detail);
}
PureItem::Struct(s) => {
let detail = build_struct_detail(s);
self.structs.insert(id, detail);
}
PureItem::Enum(e) => {
let detail = build_enum_detail(e);
self.enums.insert(id, detail);
}
PureItem::Trait(t) => {
let detail = build_trait_detail(t);
self.traits.insert(id, detail);
}
PureItem::Impl(i) => {
let detail = build_impl_detail(i);
self.impls.insert(id, detail);
}
_ => {}
}
}
fn extract_detail_direct(
&mut self,
id: SymbolId,
kind: SymbolKind,
symbol_name: &str,
file: &PureFile,
) {
match kind {
SymbolKind::Function => {
if let Some(detail) = extract_toplevel_function_detail(file, symbol_name) {
self.functions.insert(id, detail);
}
}
SymbolKind::Method => {
if let Some(detail) = extract_method_detail(file, symbol_name) {
self.functions.insert(id, detail);
}
}
SymbolKind::Struct => {
if let Some(detail) = extract_struct_detail(file, symbol_name) {
self.structs.insert(id, detail);
}
}
SymbolKind::Enum => {
if let Some(detail) = extract_enum_detail(file, symbol_name) {
self.enums.insert(id, detail);
}
}
SymbolKind::Trait => {
if let Some(detail) = extract_trait_detail(file, symbol_name) {
self.traits.insert(id, detail);
}
}
SymbolKind::Impl => {
if let Some(detail) = extract_impl_detail(file, symbol_name) {
self.impls.insert(id, detail);
}
}
_ => {}
}
}
pub fn len(&self) -> usize {
self.functions.len()
+ self.structs.len()
+ self.enums.len()
+ self.traits.len()
+ self.impls.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for DetailStore {
fn default() -> Self {
Self::new()
}
}
fn convert_generics(generics: &PureGenerics) -> GenericInfo {
let mut info = GenericInfo::default();
for param in &generics.params {
match param {
ryo_source::pure::PureGenericParam::Type { name, .. } => {
info.type_params.push(name.clone());
}
ryo_source::pure::PureGenericParam::Lifetime { name, .. } => {
info.lifetimes.push(name.clone());
}
ryo_source::pure::PureGenericParam::Const { name, ty } => {
info.const_params.push((name.clone(), ty.clone()));
}
}
}
info
}
fn type_to_string(ty: &PureType) -> String {
match ty {
PureType::Path(p) => p.clone(),
PureType::Ref {
lifetime,
is_mut,
ty,
} => {
let lt = lifetime
.as_ref()
.map(|l| format!("{} ", l))
.unwrap_or_default();
let m = if *is_mut { "mut " } else { "" };
format!("&{}{}{}", lt, m, type_to_string(ty))
}
PureType::Tuple(types) => {
let inner: Vec<_> = types.iter().map(type_to_string).collect();
format!("({})", inner.join(", "))
}
PureType::Array { ty, len } => format!("[{}; {}]", type_to_string(ty), len),
PureType::Slice(ty) => format!("[{}]", type_to_string(ty)),
PureType::Fn { params, ret } => {
let ps: Vec<_> = params.iter().map(type_to_string).collect();
let r = ret
.as_ref()
.map(|t| format!(" -> {}", type_to_string(t)))
.unwrap_or_default();
format!("fn({}){}", ps.join(", "), r)
}
PureType::ImplTrait(bounds) => format!("impl {}", bounds.join(" + ")),
PureType::TraitObject(bounds) => format!("dyn {}", bounds.join(" + ")),
PureType::Infer => "_".to_string(),
PureType::Never => "!".to_string(),
PureType::Other(s) => s.clone(),
}
}
fn is_public(vis: &PureVis) -> bool {
matches!(vis, PureVis::Public | PureVis::Crate)
}
fn extract_toplevel_function_detail(file: &PureFile, symbol_name: &str) -> Option<FunctionDetail> {
for item in &file.items {
if let PureItem::Fn(f) = item {
if f.name == symbol_name {
return Some(build_function_detail(f, None, None));
}
}
}
None
}
fn extract_method_detail(file: &PureFile, symbol_name: &str) -> Option<FunctionDetail> {
for item in &file.items {
if let PureItem::Impl(impl_block) = item {
for impl_item in &impl_block.items {
if let ryo_source::pure::PureImplItem::Fn(f) = impl_item {
if f.name == symbol_name {
return Some(build_function_detail(
f,
Some(&impl_block.self_ty),
impl_block.trait_.as_deref(),
));
}
}
}
}
if let PureItem::Trait(trait_block) = item {
for trait_item in &trait_block.items {
if let ryo_source::pure::PureTraitItem::Fn(f) = trait_item {
if f.name == symbol_name {
return Some(build_function_detail(f, None, Some(&trait_block.name)));
}
}
}
}
}
None
}
#[cfg(test)]
fn extract_function_detail(
file: &PureFile,
symbol_name: &str,
self_ty: Option<&str>,
trait_impl: Option<&str>,
) -> Option<FunctionDetail> {
for item in &file.items {
if let PureItem::Fn(f) = item {
if f.name == symbol_name {
return Some(build_function_detail(f, self_ty, trait_impl));
}
}
if let PureItem::Impl(impl_block) = item {
for impl_item in &impl_block.items {
if let ryo_source::pure::PureImplItem::Fn(f) = impl_item {
if f.name == symbol_name {
return Some(build_function_detail(
f,
Some(&impl_block.self_ty),
impl_block.trait_.as_deref(),
));
}
}
}
}
if let PureItem::Trait(trait_block) = item {
for trait_item in &trait_block.items {
if let ryo_source::pure::PureTraitItem::Fn(f) = trait_item {
if f.name == symbol_name {
return Some(build_function_detail(f, None, Some(&trait_block.name)));
}
}
}
}
}
None
}
fn build_function_detail(
f: &PureFn,
self_ty: Option<&str>,
trait_impl: Option<&str>,
) -> FunctionDetail {
let mut params = Vec::new();
let mut has_self = false;
for param in &f.params {
match param {
PureParam::SelfValue { is_ref, is_mut } => {
has_self = true;
let ty = if *is_ref {
if *is_mut {
"&mut Self"
} else {
"&Self"
}
} else {
"Self"
};
params.push(ParamInfo {
name: "self".to_string(),
ty: ty.to_string(),
is_self: true,
is_mut: *is_mut,
});
}
PureParam::Typed { name, ty } => {
params.push(ParamInfo {
name: name.clone(),
ty: type_to_string(ty),
is_self: false,
is_mut: false,
});
}
}
}
FunctionDetail {
is_async: f.is_async,
is_const: f.is_const,
is_unsafe: f.is_unsafe,
params,
return_type: f.ret.as_ref().map(type_to_string),
generics: convert_generics(&f.generics),
is_method: self_ty.is_some(),
self_ty: self_ty.map(String::from),
trait_impl: trait_impl.map(String::from),
has_self,
attrs: extract_attr_paths(&f.attrs),
}
}
fn extract_struct_detail(file: &PureFile, symbol_name: &str) -> Option<StructDetail> {
for item in &file.items {
if let PureItem::Struct(s) = item {
if s.name == symbol_name {
return Some(build_struct_detail(s));
}
}
}
None
}
fn build_struct_detail(s: &PureStruct) -> StructDetail {
let (fields, kind) = match &s.fields {
PureFields::Named(fs) => {
let fields = fs
.iter()
.map(|f| FieldInfo {
name: f.name.clone(),
ty: type_to_string(&f.ty),
is_public: is_public(&f.vis),
})
.collect();
(fields, StructKind::Named)
}
PureFields::Tuple(types) => {
let fields = types
.iter()
.enumerate()
.map(|(i, ty)| FieldInfo {
name: i.to_string(),
ty: type_to_string(ty),
is_public: true, })
.collect();
(fields, StructKind::Tuple)
}
PureFields::Unit => (Vec::new(), StructKind::Unit),
};
StructDetail {
fields,
kind,
generics: convert_generics(&s.generics),
attrs: extract_attr_paths(&s.attrs),
}
}
fn extract_enum_detail(file: &PureFile, symbol_name: &str) -> Option<EnumDetail> {
for item in &file.items {
if let PureItem::Enum(e) = item {
if e.name == symbol_name {
return Some(build_enum_detail(e));
}
}
}
None
}
fn build_enum_detail(e: &PureEnum) -> EnumDetail {
let variants = e
.variants
.iter()
.map(|v| {
let fields = match &v.fields {
PureFields::Named(fs) => fs
.iter()
.map(|f| FieldInfo {
name: f.name.clone(),
ty: type_to_string(&f.ty),
is_public: is_public(&f.vis),
})
.collect(),
PureFields::Tuple(types) => types
.iter()
.enumerate()
.map(|(i, ty)| FieldInfo {
name: i.to_string(),
ty: type_to_string(ty),
is_public: true,
})
.collect(),
PureFields::Unit => Vec::new(),
};
VariantInfo {
name: v.name.clone(),
fields,
discriminant: v.discriminant.clone(),
}
})
.collect();
EnumDetail {
variants,
generics: convert_generics(&e.generics),
attrs: extract_attr_paths(&e.attrs),
}
}
fn extract_trait_detail(file: &PureFile, symbol_name: &str) -> Option<TraitDetail> {
for item in &file.items {
if let PureItem::Trait(t) = item {
if t.name == symbol_name {
return Some(build_trait_detail(t));
}
}
}
None
}
fn build_trait_detail(t: &PureTrait) -> TraitDetail {
let mut methods = Vec::new();
let mut types = Vec::new();
for item in &t.items {
match item {
ryo_source::pure::PureTraitItem::Fn(f) => methods.push(f.name.clone()),
ryo_source::pure::PureTraitItem::Type { name, .. } => types.push(name.clone()),
_ => {}
}
}
TraitDetail {
is_unsafe: t.is_unsafe,
is_auto: t.is_auto,
supertraits: t.supertraits.clone(),
methods,
types,
generics: convert_generics(&t.generics),
attrs: extract_attr_paths(&t.attrs),
}
}
fn extract_impl_detail(file: &PureFile, symbol_name: &str) -> Option<ImplDetail> {
for item in &file.items {
if let PureItem::Impl(i) = item {
let impl_name = if let Some(ref trait_name) = i.trait_ {
format!("<impl {} for {}>", trait_name, i.self_ty)
} else {
format!("<impl {}>", i.self_ty)
};
if impl_name == symbol_name {
return Some(build_impl_detail(i));
}
}
}
None
}
fn build_impl_detail(i: &PureImpl) -> ImplDetail {
let methods = i
.items
.iter()
.filter_map(|item| {
if let ryo_source::pure::PureImplItem::Fn(f) = item {
Some(f.name.clone())
} else {
None
}
})
.collect();
ImplDetail {
is_unsafe: i.is_unsafe,
self_ty: i.self_ty.clone(),
trait_: i.trait_.clone(),
methods,
generics: convert_generics(&i.generics),
attrs: extract_attr_paths(&i.attrs),
}
}
fn extract_attr_paths(attrs: &[ryo_source::pure::PureAttribute]) -> Vec<String> {
use ryo_source::pure::PureAttrMeta;
let mut result = Vec::new();
for attr in attrs {
result.push(attr.path.clone());
match &attr.meta {
PureAttrMeta::List(args) if !args.is_empty() => {
result.push(format!("{}({})", attr.path, args));
}
PureAttrMeta::NameValue(value) if !value.is_empty() => {
result.push(format!("{} = {}", attr.path, value));
}
_ => {}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generic_info_is_empty() {
let info = GenericInfo::default();
assert!(info.is_empty());
let info = GenericInfo {
type_params: vec!["T".to_string()],
..Default::default()
};
assert!(!info.is_empty());
}
#[test]
fn test_type_to_string() {
assert_eq!(
type_to_string(&PureType::Path("String".to_string())),
"String"
);
assert_eq!(type_to_string(&PureType::Infer), "_");
assert_eq!(type_to_string(&PureType::Never), "!");
}
#[test]
fn test_detail_store_new() {
let store = DetailStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
}
#[test]
fn test_extract_method_with_mut_self() {
use ryo_source::pure::PureFile;
let source = r#"
struct Foo;
impl Foo {
pub fn get_mut(&mut self) -> &mut i32 {
&mut self.0
}
}
"#;
let file = PureFile::from_source(source).unwrap();
let detail = extract_function_detail(&file, "get_mut", None, None);
assert!(detail.is_some(), "get_mut should be found");
let detail = detail.unwrap();
assert!(detail.has_self, "get_mut should have self");
assert!(!detail.params.is_empty(), "get_mut should have params");
let first_param = &detail.params[0];
assert!(first_param.is_self, "first param should be self");
assert!(first_param.is_mut, "first param should be mut");
assert_eq!(first_param.ty, "&mut Self", "self type should be &mut Self");
}
#[test]
fn test_extract_toplevel_vs_method_same_name() {
use ryo_source::pure::PureFile;
let source = r#"
struct Executor;
impl Executor {
pub fn execute(&self, cmd: &str) -> bool {
true
}
}
pub fn execute(cmd: &str, config: &str) -> bool {
false
}
"#;
let file = PureFile::from_source(source).unwrap();
let toplevel_detail = extract_toplevel_function_detail(&file, "execute");
assert!(
toplevel_detail.is_some(),
"toplevel execute should be found"
);
let toplevel_detail = toplevel_detail.unwrap();
assert!(
!toplevel_detail.has_self,
"toplevel execute should NOT have self"
);
assert!(
!toplevel_detail.params.iter().any(|p| p.is_self),
"toplevel execute params should not contain self"
);
let method_detail = extract_method_detail(&file, "execute");
assert!(method_detail.is_some(), "method execute should be found");
let method_detail = method_detail.unwrap();
assert!(method_detail.has_self, "method execute SHOULD have self");
assert!(
method_detail.params.iter().any(|p| p.is_self),
"method execute params should contain self"
);
assert_eq!(
method_detail.params[0].ty, "&Self",
"method self should be &Self"
);
}
#[test]
fn test_extract_method_with_ref_self() {
use ryo_source::pure::PureFile;
let source = r#"
struct Reader;
impl Reader {
pub fn read(&self, buf: &mut [u8]) -> usize {
0
}
}
"#;
let file = PureFile::from_source(source).unwrap();
let detail = extract_method_detail(&file, "read");
assert!(detail.is_some(), "read should be found");
let detail = detail.unwrap();
assert!(detail.has_self, "read should have self");
assert!(!detail.params.is_empty(), "read should have params");
let first_param = &detail.params[0];
assert!(first_param.is_self, "first param should be self");
assert!(!first_param.is_mut, "first param should NOT be mut");
assert_eq!(first_param.ty, "&Self", "self type should be &Self");
}
#[test]
fn test_extract_method_with_owned_self() {
use ryo_source::pure::PureFile;
let source = r#"
struct Consumer;
impl Consumer {
pub fn consume(self) -> i32 {
42
}
}
"#;
let file = PureFile::from_source(source).unwrap();
let detail = extract_method_detail(&file, "consume");
assert!(detail.is_some(), "consume should be found");
let detail = detail.unwrap();
assert!(detail.has_self, "consume should have self");
let first_param = &detail.params[0];
assert!(first_param.is_self, "first param should be self");
assert!(!first_param.is_mut, "first param should NOT be mut");
assert_eq!(first_param.ty, "Self", "self type should be Self (owned)");
}
#[test]
fn test_extract_attrs_from_function() {
use ryo_source::pure::PureFile;
let source = r#"
#[deprecated(note = "use new_function instead")]
#[inline]
pub fn old_function() {}
"#;
let file = PureFile::from_source(source).unwrap();
let detail = extract_toplevel_function_detail(&file, "old_function");
assert!(detail.is_some(), "old_function should be found");
let detail = detail.unwrap();
assert!(
detail.attrs.contains(&"deprecated".to_string()),
"should have deprecated attr"
);
assert!(
detail.attrs.contains(&"inline".to_string()),
"should have inline attr"
);
}
#[test]
fn test_extract_attrs_from_struct() {
use ryo_source::pure::PureFile;
let source = r#"
#[derive(Debug, Clone)]
#[repr(C)]
pub struct Config {
pub name: String,
}
"#;
let file = PureFile::from_source(source).unwrap();
let detail = extract_struct_detail(&file, "Config");
assert!(detail.is_some(), "Config should be found");
let detail = detail.unwrap();
assert!(
detail.attrs.contains(&"derive".to_string()),
"should have derive attr"
);
assert!(
detail.attrs.contains(&"repr".to_string()),
"should have repr attr"
);
}
}