miniextendr_lint/
helpers.rs1use std::path::Path;
4
5use syn::Attribute;
6
7pub fn has_miniextendr_attr(attrs: &[Attribute]) -> bool {
9 attrs.iter().any(|attr| {
10 attr.path()
11 .segments
12 .last()
13 .is_some_and(|seg| seg.ident == "miniextendr")
14 })
15}
16
17pub fn impl_type_name(ty: &syn::Type) -> Option<String> {
19 match ty {
20 syn::Type::Path(type_path) => type_path
21 .path
22 .segments
23 .last()
24 .map(|seg| seg.ident.to_string()),
25 syn::Type::Reference(type_ref) => impl_type_name(&type_ref.elem),
26 _ => None,
27 }
28}
29
30pub fn has_derive(attrs: &[Attribute], name: &str) -> bool {
34 attrs.iter().any(|attr| {
35 if !attr.path().is_ident("derive") {
36 return false;
37 }
38 let syn::Meta::List(meta_list) = &attr.meta else {
39 return false;
40 };
41 let Ok(paths) = meta_list.parse_args_with(
42 syn::punctuated::Punctuated::<syn::Path, ::syn::token::Commasyn::Token![,]>::parse_terminated,
43 ) else {
44 return false;
45 };
46 paths
47 .iter()
48 .any(|p| p.segments.last().is_some_and(|seg| seg.ident == name))
49 })
50}
51
52pub fn should_skip_dir(path: &Path) -> bool {
54 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
55 return false;
56 };
57 #[allow(non_exhaustive_omitted_patterns)] match name {
"target" | "ra_target" | ".cargo" | ".git" | "vendor" => true,
_ => false,
}matches!(name, "target" | "ra_target" | ".cargo" | ".git" | "vendor")
58}
59
60#[derive(#[automatically_derived]
impl ::core::fmt::Debug for MiniextendrImplAttrs {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field4_finish(f,
"MiniextendrImplAttrs", "class_system", &self.class_system,
"internal", &self.internal, "noexport", &self.noexport, "strict",
&&self.strict)
}
}Debug, #[automatically_derived]
impl ::core::default::Default for MiniextendrImplAttrs {
#[inline]
fn default() -> Self {
MiniextendrImplAttrs {
class_system: ::core::default::Default::default(),
internal: ::core::default::Default::default(),
noexport: ::core::default::Default::default(),
strict: ::core::default::Default::default(),
}
}
}Default)]
62pub struct MiniextendrImplAttrs {
63 pub class_system: Option<String>,
65 pub internal: bool,
67 pub noexport: bool,
69 pub strict: bool,
71}
72
73const CLASS_SYSTEMS: [&str; 6] = ["env", "r6", "s3", "s4", "s7", "vctrs"];
75
76pub fn parse_miniextendr_impl_attrs(attrs: &[Attribute]) -> MiniextendrImplAttrs {
78 let mut result = MiniextendrImplAttrs::default();
79
80 for attr in attrs {
81 if attr
82 .path()
83 .segments
84 .last()
85 .is_none_or(|seg| seg.ident != "miniextendr")
86 {
87 continue;
88 }
89
90 if let syn::Meta::List(meta_list) = &attr.meta {
91 let tokens = meta_list.tokens.to_string();
92 let tokens = tokens.trim();
93 if tokens.is_empty() {
94 continue;
95 }
96
97 for part in split_top_level_commas(tokens) {
101 let part = part.trim();
102 if part.is_empty() {
103 continue;
104 }
105
106 if part == "internal" {
107 result.internal = true;
108 } else if part == "noexport" {
109 result.noexport = true;
110 } else if part == "strict" {
111 result.strict = true;
112 } else if !part.contains('=') || part.contains('(') {
113 let base = part
119 .find(|c: char| !c.is_alphanumeric() && c != '_')
120 .map(|i| &part[..i])
121 .unwrap_or(part);
122 if CLASS_SYSTEMS.contains(&base) {
123 result.class_system = Some(base.to_string());
124 }
125 }
126 }
127 }
128 }
129
130 result
131}
132
133fn split_top_level_commas(s: &str) -> impl Iterator<Item = &str> {
135 SplitTopLevelCommas { remaining: s }
136}
137
138struct SplitTopLevelCommas<'a> {
139 remaining: &'a str,
140}
141
142impl<'a> Iterator for SplitTopLevelCommas<'a> {
143 type Item = &'a str;
144
145 fn next(&mut self) -> Option<&'a str> {
146 if self.remaining.is_empty() {
147 return None;
148 }
149 let mut depth: usize = 0;
150 let mut in_str = false;
151 let bytes = self.remaining.as_bytes();
152 for (i, &b) in bytes.iter().enumerate() {
153 match b {
154 b'"' if !in_str => in_str = true,
155 b'"' if in_str => in_str = false,
156 b'(' | b'[' | b'{' if !in_str => depth += 1,
157 b')' | b']' | b'}' if !in_str && depth > 0 => depth -= 1,
158 b',' if !in_str && depth == 0 => {
159 let item = &self.remaining[..i];
160 self.remaining = &self.remaining[i + 1..];
161 return Some(item);
162 }
163 _ => {}
164 }
165 }
166 let item = self.remaining;
168 self.remaining = "";
169 Some(item)
170 }
171}
172
173pub fn extract_path_attr(attrs: &[Attribute]) -> Option<String> {
175 attrs.iter().find_map(|attr| {
176 if !attr.path().is_ident("path") {
177 return None;
178 }
179 if let syn::Meta::NameValue(nv) = &attr.meta
180 && let syn::Expr::Lit(expr_lit) = &nv.value
181 && let syn::Lit::Str(lit_str) = &expr_lit.lit
182 {
183 return Some(lit_str.value());
184 }
185 None
186 })
187}
188
189pub fn extract_cfg_attrs(attrs: &[Attribute]) -> Vec<String> {
191 attrs
192 .iter()
193 .filter(|attr| attr.path().is_ident("cfg"))
194 .map(|attr| attr.meta.to_token_stream_string())
195 .collect()
196}
197
198pub fn extract_roxygen_tags(attrs: &[Attribute]) -> Vec<String> {
203 let mut tags = Vec::new();
204 for attr in attrs {
205 if !attr.path().is_ident("doc") {
206 continue;
207 }
208 if let syn::Meta::NameValue(nv) = &attr.meta
209 && let syn::Expr::Lit(expr_lit) = &nv.value
210 && let syn::Lit::Str(lit_str) = &expr_lit.lit
211 {
212 let text = lit_str.value();
213 for word in text.split_whitespace() {
214 if let Some(tag) = word.strip_prefix('@')
215 && !tag.is_empty()
216 {
217 tags.push(tag.to_string());
218 }
219 }
220 }
221 }
222 tags
223}
224
225pub fn is_altrep_struct(item: &syn::ItemStruct) -> bool {
230 let field_count = match &item.fields {
231 syn::Fields::Named(f) => f.named.len(),
232 syn::Fields::Unnamed(f) => f.unnamed.len(),
233 syn::Fields::Unit => 0,
234 };
235
236 if field_count != 1 {
238 return false;
239 }
240
241 for attr in &item.attrs {
243 if attr
244 .path()
245 .segments
246 .last()
247 .is_none_or(|seg| seg.ident != "miniextendr")
248 {
249 continue;
250 }
251
252 if let syn::Meta::List(meta_list) = &attr.meta {
253 let tokens = meta_list.tokens.to_string();
254 for part in split_top_level_commas(&tokens) {
255 let part = part.trim();
256 if #[allow(non_exhaustive_omitted_patterns)] match part {
"list" | "dataframe" | "externalptr" => true,
_ => false,
}matches!(part, "list" | "dataframe" | "externalptr") {
258 return false;
259 }
260 }
261 }
262 }
263
264 true
265}
266
267trait MetaToString {
269 fn to_token_stream_string(&self) -> String;
270}
271
272impl MetaToString for syn::Meta {
273 fn to_token_stream_string(&self) -> String {
274 use quote::ToTokens;
275 self.to_token_stream().to_string()
276 }
277}