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_external_ptr_derive(attrs: &[Attribute]) -> bool {
33 attrs.iter().any(|attr| {
34 if !attr.path().is_ident("derive") {
35 return false;
36 }
37 let syn::Meta::List(meta_list) = &attr.meta else {
38 return false;
39 };
40 let Ok(paths) = meta_list.parse_args_with(
41 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
42 ) else {
43 return false;
44 };
45 paths.iter().any(|p| {
46 p.segments
47 .last()
48 .is_some_and(|seg| seg.ident == "ExternalPtr")
49 })
50 })
51}
52
53pub fn has_altrep_derive(attrs: &[Attribute]) -> bool {
56 attrs.iter().any(|attr| {
57 if !attr.path().is_ident("derive") {
58 return false;
59 }
60 let syn::Meta::List(meta_list) = &attr.meta else {
61 return false;
62 };
63 let Ok(paths) = meta_list.parse_args_with(
64 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
65 ) else {
66 return false;
67 };
68 paths
69 .iter()
70 .any(|p| p.segments.last().is_some_and(|seg| seg.ident == "Altrep"))
71 })
72}
73
74pub fn has_vctrs_derive(attrs: &[Attribute]) -> bool {
77 attrs.iter().any(|attr| {
78 if !attr.path().is_ident("derive") {
79 return false;
80 }
81 let syn::Meta::List(meta_list) = &attr.meta else {
82 return false;
83 };
84 let Ok(paths) = meta_list.parse_args_with(
85 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
86 ) else {
87 return false;
88 };
89 paths
90 .iter()
91 .any(|p| p.segments.last().is_some_and(|seg| seg.ident == "Vctrs"))
92 })
93}
94
95pub fn should_skip_dir(path: &Path) -> bool {
97 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
98 return false;
99 };
100 matches!(name, "target" | "ra_target" | ".cargo" | ".git" | "vendor")
101}
102
103#[derive(Debug, Default)]
105pub struct MiniextendrImplAttrs {
106 pub class_system: Option<String>,
108 pub label: Option<String>,
110 pub internal: bool,
112 pub noexport: bool,
114 pub strict: bool,
116}
117
118pub fn parse_miniextendr_impl_attrs(attrs: &[Attribute]) -> MiniextendrImplAttrs {
120 let mut result = MiniextendrImplAttrs::default();
121
122 for attr in attrs {
123 if attr
124 .path()
125 .segments
126 .last()
127 .is_none_or(|seg| seg.ident != "miniextendr")
128 {
129 continue;
130 }
131
132 if let syn::Meta::List(meta_list) = &attr.meta {
133 let tokens = meta_list.tokens.to_string();
134 let tokens = tokens.trim();
135 if tokens.is_empty() {
136 continue;
137 }
138
139 for part in split_top_level_commas(tokens) {
143 let part = part.trim();
144 if part.is_empty() {
145 continue;
146 }
147
148 if part.starts_with("label") {
149 if let Some(eq_pos) = part.find('=') {
150 let value = part[eq_pos + 1..].trim();
151 let value = value.trim_matches('"').trim_matches('\'');
152 result.label = Some(value.to_string());
153 }
154 } else if part == "internal" {
155 result.internal = true;
156 } else if part == "noexport" {
157 result.noexport = true;
158 } else if part == "strict" {
159 result.strict = true;
160 } else if !part.contains('=') || part.contains('(') {
161 let base = part
165 .find(|c: char| !c.is_alphanumeric() && c != '_')
166 .map(|i| &part[..i])
167 .unwrap_or(part);
168 if !base.is_empty() {
169 result.class_system = Some(base.to_string());
170 }
171 }
172 }
173 }
174 }
175
176 result
177}
178
179fn split_top_level_commas(s: &str) -> impl Iterator<Item = &str> {
181 SplitTopLevelCommas { remaining: s }
182}
183
184struct SplitTopLevelCommas<'a> {
185 remaining: &'a str,
186}
187
188impl<'a> Iterator for SplitTopLevelCommas<'a> {
189 type Item = &'a str;
190
191 fn next(&mut self) -> Option<&'a str> {
192 if self.remaining.is_empty() {
193 return None;
194 }
195 let mut depth: usize = 0;
196 let mut in_str = false;
197 let bytes = self.remaining.as_bytes();
198 for (i, &b) in bytes.iter().enumerate() {
199 match b {
200 b'"' if !in_str => in_str = true,
201 b'"' if in_str => in_str = false,
202 b'(' | b'[' | b'{' if !in_str => depth += 1,
203 b')' | b']' | b'}' if !in_str && depth > 0 => depth -= 1,
204 b',' if !in_str && depth == 0 => {
205 let item = &self.remaining[..i];
206 self.remaining = &self.remaining[i + 1..];
207 return Some(item);
208 }
209 _ => {}
210 }
211 }
212 let item = self.remaining;
214 self.remaining = "";
215 Some(item)
216 }
217}
218
219pub fn extract_path_attr(attrs: &[Attribute]) -> Option<String> {
221 attrs.iter().find_map(|attr| {
222 if !attr.path().is_ident("path") {
223 return None;
224 }
225 if let syn::Meta::NameValue(nv) = &attr.meta
226 && let syn::Expr::Lit(expr_lit) = &nv.value
227 && let syn::Lit::Str(lit_str) = &expr_lit.lit
228 {
229 return Some(lit_str.value());
230 }
231 None
232 })
233}
234
235pub fn extract_cfg_attrs(attrs: &[Attribute]) -> Vec<String> {
237 attrs
238 .iter()
239 .filter(|attr| attr.path().is_ident("cfg"))
240 .map(|attr| attr.meta.to_token_stream_string())
241 .collect()
242}
243
244pub fn extract_roxygen_tags(attrs: &[Attribute]) -> Vec<String> {
249 let mut tags = Vec::new();
250 for attr in attrs {
251 if !attr.path().is_ident("doc") {
252 continue;
253 }
254 if let syn::Meta::NameValue(nv) = &attr.meta
255 && let syn::Expr::Lit(expr_lit) = &nv.value
256 && let syn::Lit::Str(lit_str) = &expr_lit.lit
257 {
258 let text = lit_str.value();
259 for word in text.split_whitespace() {
260 if let Some(tag) = word.strip_prefix('@')
261 && !tag.is_empty()
262 {
263 tags.push(tag.to_string());
264 }
265 }
266 }
267 }
268 tags
269}
270
271pub fn is_altrep_struct(item: &syn::ItemStruct) -> bool {
276 let field_count = match &item.fields {
277 syn::Fields::Named(f) => f.named.len(),
278 syn::Fields::Unnamed(f) => f.unnamed.len(),
279 syn::Fields::Unit => 0,
280 };
281
282 if field_count != 1 {
284 return false;
285 }
286
287 for attr in &item.attrs {
289 if attr
290 .path()
291 .segments
292 .last()
293 .is_none_or(|seg| seg.ident != "miniextendr")
294 {
295 continue;
296 }
297
298 if let syn::Meta::List(meta_list) = &attr.meta {
299 let tokens = meta_list.tokens.to_string();
300 for part in split_top_level_commas(&tokens) {
301 let part = part.trim();
302 if matches!(part, "list" | "dataframe" | "externalptr") {
304 return false;
305 }
306 }
307 }
308 }
309
310 true
311}
312
313trait MetaToString {
315 fn to_token_stream_string(&self) -> String;
316}
317
318impl MetaToString for syn::Meta {
319 fn to_token_stream_string(&self) -> String {
320 use quote::ToTokens;
321 self.to_token_stream().to_string()
322 }
323}