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
76pub(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}