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![,]>::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 matches!(name, "target" | "ra_target" | ".cargo" | ".git" | "vendor")
58}
59
60#[derive(Debug, Default)]
62pub struct MiniextendrImplAttrs {
63 pub class_system: Option<String>,
65 pub label: Option<String>,
67 pub internal: bool,
69 pub noexport: bool,
71 pub strict: bool,
73}
74
75pub fn parse_miniextendr_impl_attrs(attrs: &[Attribute]) -> MiniextendrImplAttrs {
77 let mut result = MiniextendrImplAttrs::default();
78
79 for attr in attrs {
80 if attr
81 .path()
82 .segments
83 .last()
84 .is_none_or(|seg| seg.ident != "miniextendr")
85 {
86 continue;
87 }
88
89 if let syn::Meta::List(meta_list) = &attr.meta {
90 let tokens = meta_list.tokens.to_string();
91 let tokens = tokens.trim();
92 if tokens.is_empty() {
93 continue;
94 }
95
96 for part in split_top_level_commas(tokens) {
100 let part = part.trim();
101 if part.is_empty() {
102 continue;
103 }
104
105 if part.starts_with("label") {
106 if let Some(eq_pos) = part.find('=') {
107 let value = part[eq_pos + 1..].trim();
108 let value = value.trim_matches('"').trim_matches('\'');
109 result.label = Some(value.to_string());
110 }
111 } else if part == "internal" {
112 result.internal = true;
113 } else if part == "noexport" {
114 result.noexport = true;
115 } else if part == "strict" {
116 result.strict = true;
117 } else if !part.contains('=') || part.contains('(') {
118 let base = part
122 .find(|c: char| !c.is_alphanumeric() && c != '_')
123 .map(|i| &part[..i])
124 .unwrap_or(part);
125 if !base.is_empty() {
126 result.class_system = Some(base.to_string());
127 }
128 }
129 }
130 }
131 }
132
133 result
134}
135
136fn split_top_level_commas(s: &str) -> impl Iterator<Item = &str> {
138 SplitTopLevelCommas { remaining: s }
139}
140
141struct SplitTopLevelCommas<'a> {
142 remaining: &'a str,
143}
144
145impl<'a> Iterator for SplitTopLevelCommas<'a> {
146 type Item = &'a str;
147
148 fn next(&mut self) -> Option<&'a str> {
149 if self.remaining.is_empty() {
150 return None;
151 }
152 let mut depth: usize = 0;
153 let mut in_str = false;
154 let bytes = self.remaining.as_bytes();
155 for (i, &b) in bytes.iter().enumerate() {
156 match b {
157 b'"' if !in_str => in_str = true,
158 b'"' if in_str => in_str = false,
159 b'(' | b'[' | b'{' if !in_str => depth += 1,
160 b')' | b']' | b'}' if !in_str && depth > 0 => depth -= 1,
161 b',' if !in_str && depth == 0 => {
162 let item = &self.remaining[..i];
163 self.remaining = &self.remaining[i + 1..];
164 return Some(item);
165 }
166 _ => {}
167 }
168 }
169 let item = self.remaining;
171 self.remaining = "";
172 Some(item)
173 }
174}
175
176pub fn extract_path_attr(attrs: &[Attribute]) -> Option<String> {
178 attrs.iter().find_map(|attr| {
179 if !attr.path().is_ident("path") {
180 return None;
181 }
182 if let syn::Meta::NameValue(nv) = &attr.meta
183 && let syn::Expr::Lit(expr_lit) = &nv.value
184 && let syn::Lit::Str(lit_str) = &expr_lit.lit
185 {
186 return Some(lit_str.value());
187 }
188 None
189 })
190}
191
192pub fn extract_cfg_attrs(attrs: &[Attribute]) -> Vec<String> {
194 attrs
195 .iter()
196 .filter(|attr| attr.path().is_ident("cfg"))
197 .map(|attr| attr.meta.to_token_stream_string())
198 .collect()
199}
200
201pub fn extract_roxygen_tags(attrs: &[Attribute]) -> Vec<String> {
206 let mut tags = Vec::new();
207 for attr in attrs {
208 if !attr.path().is_ident("doc") {
209 continue;
210 }
211 if let syn::Meta::NameValue(nv) = &attr.meta
212 && let syn::Expr::Lit(expr_lit) = &nv.value
213 && let syn::Lit::Str(lit_str) = &expr_lit.lit
214 {
215 let text = lit_str.value();
216 for word in text.split_whitespace() {
217 if let Some(tag) = word.strip_prefix('@')
218 && !tag.is_empty()
219 {
220 tags.push(tag.to_string());
221 }
222 }
223 }
224 }
225 tags
226}
227
228pub fn is_altrep_struct(item: &syn::ItemStruct) -> bool {
233 let field_count = match &item.fields {
234 syn::Fields::Named(f) => f.named.len(),
235 syn::Fields::Unnamed(f) => f.unnamed.len(),
236 syn::Fields::Unit => 0,
237 };
238
239 if field_count != 1 {
241 return false;
242 }
243
244 for attr in &item.attrs {
246 if attr
247 .path()
248 .segments
249 .last()
250 .is_none_or(|seg| seg.ident != "miniextendr")
251 {
252 continue;
253 }
254
255 if let syn::Meta::List(meta_list) = &attr.meta {
256 let tokens = meta_list.tokens.to_string();
257 for part in split_top_level_commas(&tokens) {
258 let part = part.trim();
259 if matches!(part, "list" | "dataframe" | "externalptr") {
261 return false;
262 }
263 }
264 }
265 }
266
267 true
268}
269
270trait MetaToString {
272 fn to_token_stream_string(&self) -> String;
273}
274
275impl MetaToString for syn::Meta {
276 fn to_token_stream_string(&self) -> String {
277 use quote::ToTokens;
278 self.to_token_stream().to_string()
279 }
280}