Skip to main content

wowlab_tidy/languages/
workspace.rs

1use ra_ap_syntax::{
2    AstNode, SyntaxKind,
3    ast::{self, HasArgList, HasGenericParams, HasName},
4};
5use wowlab_fs::checksum::{self, Checksum};
6use wowlab_types::sim::FastSet;
7
8use crate::{Config, FileCtx, infra::ignore::Suppressions};
9
10#[derive(Debug)]
11pub(crate) struct WorkspaceCtx<'a> {
12    pub(crate) files: &'a [WorkspaceRustFile],
13    pub(crate) manifests: &'a [WorkspaceManifest],
14    pub(crate) config: &'a Config,
15}
16
17#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
18pub(crate) struct WorkspaceRustFile {
19    pub(crate) rel: String,
20    pub(crate) structs: Vec<StructRecord>,
21    pub(crate) functions: Vec<FunctionRecord>,
22    pub(crate) strings: Vec<StringRecord>,
23    pub(crate) crate_roots: FastSet<String>,
24    pub(crate) suppressions: Suppressions,
25}
26
27#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
28pub(crate) struct WorkspaceManifest {
29    pub(crate) rel: String,
30    pub(crate) dependencies: Vec<DependencyRecord>,
31}
32
33#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
34pub(crate) struct DependencyRecord {
35    pub(crate) name: String,
36    pub(crate) root: String,
37    pub(crate) line: usize,
38}
39
40#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
41pub(crate) struct StructRecord {
42    pub(crate) name: String,
43    pub(crate) line: usize,
44    pub(crate) generics_arity: usize,
45    pub(crate) fields: Vec<(String, String)>,
46}
47
48#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
49pub(crate) struct FunctionRecord {
50    pub(crate) name: String,
51    pub(crate) line: usize,
52    pub(crate) body_token_count: usize,
53    pub(crate) body_checksum: Checksum,
54    pub(crate) body_shingles: Box<[ShingleFingerprint]>,
55    pub(crate) params: Vec<(String, String)>,
56    pub(crate) pass_through_calls: Box<[Box<[String]>]>,
57}
58
59#[derive(
60    Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize,
61)]
62pub(crate) struct ShingleFingerprint([u8; SHINGLE_FINGERPRINT_BYTES]);
63
64#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
65pub(crate) struct StringRecord {
66    pub(crate) value: String,
67    pub(crate) line: usize,
68}
69
70const GENERATED_HEADER_LINES: usize = 8;
71pub(crate) const FUNCTION_SHINGLE_SIZE: usize = 5;
72const SHINGLE_FINGERPRINT_BYTES: usize = 16;
73const QUALIFIED_PATH_TOKEN_COUNT: usize = 2;
74const SPACED_QUALIFIED_PATH_TOKEN_COUNT: usize = 3;
75
76// #t(fn: rust_cyclomatic_complexity) one typed walk extracts the records requested by all workspace rules
77pub(crate) fn extract(
78    file: &FileCtx<'_>,
79    root: &ast::SourceFile,
80    suppressions: Suppressions,
81) -> WorkspaceRustFile {
82    let crate_roots = crate_roots(root);
83
84    if file
85        .contents
86        .lines()
87        .take(GENERATED_HEADER_LINES)
88        .any(|line| line.contains("@generated"))
89    {
90        return WorkspaceRustFile {
91            rel: file.rel.to_owned(),
92            structs: Vec::new(),
93            functions: Vec::new(),
94            strings: Vec::new(),
95            crate_roots,
96            suppressions,
97        };
98    }
99
100    let line_index = line_index::LineIndex::new(file.contents);
101    let line_of = |node: &ra_ap_syntax::SyntaxNode| {
102        line_index.line_col(node.text_range().start()).line as usize + 1
103    };
104    let structs = root
105        .syntax()
106        .descendants()
107        .filter_map(ast::Struct::cast)
108        .filter(|item| !in_test(item.syntax()))
109        .filter_map(|item| {
110            let fields = item.field_list()?;
111            let ast::FieldList::RecordFieldList(fields) = fields else {
112                return None;
113            };
114            let name = item.name()?;
115
116            Some(StructRecord {
117                name: name.text().to_string(),
118                line: line_of(name.syntax()),
119                generics_arity: item
120                    .generic_param_list()
121                    .map_or(0, |params| params.generic_params().count()),
122                fields: fields
123                    .fields()
124                    .filter_map(|field| {
125                        Some((
126                            field.name()?.text().to_string(),
127                            normalized(field.ty()?.syntax()),
128                        ))
129                    })
130                    .collect(),
131            })
132        })
133        .collect();
134    let functions = root
135        .syntax()
136        .descendants()
137        .filter_map(ast::Fn::cast)
138        .filter(|function| !in_test(function.syntax()) && !in_trait_impl(function))
139        .filter_map(|function| {
140            let name_node = function.name()?;
141            let name = name_node.text().to_string();
142            let body = function.body()?;
143            let params: Vec<(String, String)> = function
144                .param_list()?
145                .params()
146                .filter_map(|param| {
147                    let ast::Pat::IdentPat(pattern) = param.pat()? else {
148                        return None;
149                    };
150
151                    Some((
152                        pattern.name()?.text().to_string(),
153                        normalized(param.ty()?.syntax()),
154                    ))
155                })
156                .collect();
157            let own_names: FastSet<&str> = params.iter().map(|(name, _)| name.as_str()).collect();
158            let pass_through_calls: Vec<Box<[String]>> = body
159                .syntax()
160                .descendants()
161                .filter_map(ast::CallExpr::cast)
162                .filter_map(|call| {
163                    let args: Vec<String> = call
164                        .arg_list()?
165                        .args()
166                        .filter_map(|arg| {
167                            let ast::Expr::PathExpr(path) = arg else {
168                                return None;
169                            };
170                            let name = path.path()?.as_single_name_ref()?.text().to_string();
171
172                            own_names.contains(name.as_str()).then_some(name)
173                        })
174                        .collect();
175
176                    (!args.is_empty()).then(|| args.into_boxed_slice())
177                })
178                .collect();
179
180            let (body_token_count, body_checksum, body_shingles) = body_summary(&body);
181
182            Some(FunctionRecord {
183                name,
184                line: line_of(name_node.syntax()),
185                body_token_count,
186                body_checksum,
187                body_shingles,
188                params,
189                pass_through_calls: pass_through_calls.into_boxed_slice(),
190            })
191        })
192        .collect();
193    let string_tokens = root
194        .syntax()
195        .descendants_with_tokens()
196        .filter_map(ra_ap_syntax::NodeOrToken::into_token)
197        .filter(|token| token.kind() == SyntaxKind::STRING);
198    let strings = string_tokens
199        .filter(|token| {
200            !token
201                .parent_ancestors()
202                .filter_map(ast::Const::cast)
203                .any(|item| item.name().is_some_and(|name| name.text() == "EXAMPLES"))
204        })
205        .map(|token| StringRecord {
206            value: token.text().to_owned(),
207            line: line_index.line_col(token.text_range().start()).line as usize + 1,
208        })
209        .collect();
210
211    WorkspaceRustFile {
212        rel: file.rel.to_owned(),
213        structs,
214        functions,
215        strings,
216        crate_roots,
217        suppressions,
218    }
219}
220
221impl ShingleFingerprint {
222    fn from_tokens(tokens: &[String]) -> Self {
223        let checksum = token_checksum(tokens);
224        let mut fingerprint = [0_u8; SHINGLE_FINGERPRINT_BYTES];
225
226        for (target, source) in fingerprint.iter_mut().zip(checksum.as_bytes()) {
227            *target = *source;
228        }
229
230        Self(fingerprint)
231    }
232}
233
234fn body_summary(body: &ast::BlockExpr) -> (usize, Checksum, Box<[ShingleFingerprint]>) {
235    let body_tokens: Box<[String]> = body
236        .syntax()
237        .descendants_with_tokens()
238        .filter_map(|element| {
239            let token = element.into_token()?;
240
241            (!token.kind().is_trivia()).then(|| token.text().to_owned())
242        })
243        .collect::<Vec<_>>()
244        .into_boxed_slice();
245    let checksum = token_checksum(&body_tokens);
246    let mut shingles = body_tokens
247        .windows(FUNCTION_SHINGLE_SIZE)
248        .map(ShingleFingerprint::from_tokens)
249        .collect::<Vec<_>>();
250
251    shingles.sort_unstable();
252    shingles.dedup();
253
254    (body_tokens.len(), checksum, shingles.into_boxed_slice())
255}
256
257fn token_checksum(tokens: &[String]) -> Checksum {
258    let byte_count = tokens
259        .iter()
260        .map(|token| token.len().saturating_add(size_of::<u64>()))
261        .sum();
262    let mut encoded = Vec::with_capacity(byte_count);
263
264    for token in tokens {
265        let len = u64::try_from(token.len()).unwrap_or(u64::MAX);
266
267        encoded.extend_from_slice(&len.to_le_bytes());
268        encoded.extend_from_slice(token.as_bytes());
269    }
270
271    checksum::bytes(encoded)
272}
273
274fn crate_roots(root: &ast::SourceFile) -> FastSet<String> {
275    let mut roots: FastSet<String> = root
276        .syntax()
277        .descendants()
278        .filter_map(ast::Path::cast)
279        .filter(|path| path.qualifier().is_none())
280        .filter_map(|path| {
281            path.segment()?
282                .name_ref()
283                .map(|name| name.text().trim_start_matches("r#").to_owned())
284        })
285        .collect();
286
287    let tokens: Vec<_> = root
288        .syntax()
289        .descendants_with_tokens()
290        .filter_map(ra_ap_syntax::NodeOrToken::into_token)
291        .filter(|token| !token.kind().is_trivia())
292        .collect();
293
294    for pair in tokens.windows(QUALIFIED_PATH_TOKEN_COUNT) {
295        if pair[0].kind() == SyntaxKind::IDENT && pair[1].text() == "::" {
296            roots.insert(pair[0].text().trim_start_matches("r#").to_owned());
297        }
298    }
299
300    for triple in tokens.windows(SPACED_QUALIFIED_PATH_TOKEN_COUNT) {
301        let [root, first_separator, second_separator] = triple else {
302            continue;
303        };
304
305        if root.kind() == SyntaxKind::IDENT
306            && first_separator.text() == ":"
307            && second_separator.text() == ":"
308        {
309            roots.insert(root.text().trim_start_matches("r#").to_owned());
310        }
311    }
312
313    roots
314}
315
316fn normalized(node: &ra_ap_syntax::SyntaxNode) -> String {
317    node.descendants_with_tokens()
318        .filter_map(ra_ap_syntax::NodeOrToken::into_token)
319        .filter(|token| !token.kind().is_trivia())
320        .map(|token| token.text().to_owned())
321        .collect()
322}
323
324fn in_test(node: &ra_ap_syntax::SyntaxNode) -> bool {
325    node.ancestors().filter_map(ast::Item::cast).any(|item| {
326        use ra_ap_syntax::ast::HasAttrs;
327
328        item.attrs()
329            .any(|attr| attr.syntax().text().to_string().contains("cfg(test)"))
330    })
331}
332
333fn in_trait_impl(function: &ast::Fn) -> bool {
334    function
335        .syntax()
336        .ancestors()
337        .find_map(ast::Impl::cast)
338        .is_some_and(|item_impl| item_impl.trait_().is_some())
339}