1#![allow(clippy::cast_possible_truncation)]
12
13use log::warn;
14
15use crate::stub::model::ScalaSignatureStub;
16
17use super::signature::{
18 FLAG_ABSTRACT, FLAG_CASE, FLAG_INTERFACE, FLAG_PRIVATE, FLAG_PROTECTED, FLAG_SEALED,
19 FLAG_TRAIT, ScalaSignatureReader, TAG_EXT_MOD_CLASS_REF, TAG_EXT_REF, TAG_MODULE_SYM,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum ScalaClassKind {
29 Class,
31 Trait,
33 Object,
35 PackageObject,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum ScalaVisibility {
42 Public,
44 Private,
46 Protected,
48}
49
50#[derive(Debug, Clone)]
56pub struct ScalaClassMetadata {
57 pub kind: ScalaClassKind,
59 pub visibility: ScalaVisibility,
61 pub is_case: bool,
63 pub is_sealed: bool,
65 pub is_abstract: bool,
67 pub superclass: Option<String>,
69 pub traits: Vec<String>,
71}
72
73#[must_use]
87pub fn decode_scala_signature(stub: &ScalaSignatureStub) -> Option<ScalaClassMetadata> {
88 if stub.major_version != 5 {
90 warn!(
91 "unsupported Scala signature major version {} (expected 5)",
92 stub.major_version
93 );
94 return None;
95 }
96
97 let reader = ScalaSignatureReader::parse(&stub.bytes)?;
98
99 let symbols = reader.class_and_module_symbols();
104 if symbols.is_empty() {
105 warn!("no CLASSsym or MODULEsym entries found in Scala signature");
106 return None;
107 }
108
109 let (primary_index, primary_entry) = find_primary_symbol(&reader, &symbols)?;
112
113 let info = reader.read_symbol_info(primary_entry)?;
114 let flags = info.flags;
115
116 let kind = determine_kind(primary_entry.tag, flags, &reader, info.name_index);
118
119 let visibility = determine_visibility(flags);
121
122 let (superclass, traits) = extract_hierarchy(&reader, primary_index);
124
125 Some(ScalaClassMetadata {
126 kind,
127 visibility,
128 is_case: flags & FLAG_CASE != 0,
129 is_sealed: flags & FLAG_SEALED != 0,
130 is_abstract: flags & FLAG_ABSTRACT != 0,
131 superclass,
132 traits,
133 })
134}
135
136fn find_primary_symbol<'a>(
145 reader: &'a ScalaSignatureReader,
146 symbols: &[(usize, &'a super::signature::SignatureEntry)],
147) -> Option<(usize, &'a super::signature::SignatureEntry)> {
148 for &(idx, entry) in symbols {
151 if let Some(info) = reader.read_symbol_info(entry)
152 && let Some(name) = reader.read_name(info.name_index)
153 && !name.is_empty()
154 && !name.starts_with('$')
155 && !name.contains("$anon")
156 {
157 return Some((idx, entry));
158 }
159 }
160
161 symbols.first().map(|&(idx, entry)| (idx, entry))
163}
164
165fn determine_kind(
167 tag: u8,
168 flags: u64,
169 reader: &ScalaSignatureReader,
170 name_index: usize,
171) -> ScalaClassKind {
172 if tag == TAG_MODULE_SYM {
174 if let Some(name) = reader.read_name(name_index)
176 && name == "package"
177 {
178 return ScalaClassKind::PackageObject;
179 }
180 return ScalaClassKind::Object;
181 }
182
183 if flags & FLAG_TRAIT != 0 || flags & FLAG_INTERFACE != 0 {
185 return ScalaClassKind::Trait;
186 }
187
188 ScalaClassKind::Class
189}
190
191fn determine_visibility(flags: u64) -> ScalaVisibility {
193 if flags & FLAG_PRIVATE != 0 {
194 ScalaVisibility::Private
195 } else if flags & FLAG_PROTECTED != 0 {
196 ScalaVisibility::Protected
197 } else {
198 ScalaVisibility::Public
199 }
200}
201
202fn extract_hierarchy(
213 reader: &ScalaSignatureReader,
214 _primary_index: usize,
215) -> (Option<String>, Vec<String>) {
216 const SKIP_NAMES: &[&str] = &[
218 "Object",
219 "AnyRef",
220 "Any",
221 "Serializable",
222 "Product",
223 "Equals",
224 "<empty>",
225 "java.lang.Object",
226 "scala.AnyRef",
227 ];
228
229 let ext_refs = reader.ext_refs();
230 let mut superclass: Option<String> = None;
231 let mut traits = Vec::new();
232
233 for &(_idx, entry) in &ext_refs {
234 if entry.tag != TAG_EXT_REF && entry.tag != TAG_EXT_MOD_CLASS_REF {
235 continue;
236 }
237
238 let mut pos = 0;
240 let Some(name_ref) = super::signature::read_nat(&entry.data, &mut pos) else {
241 continue;
242 };
243 let Some(name) = reader.read_name(name_ref as usize) else {
244 continue;
245 };
246
247 if SKIP_NAMES.contains(&name.as_str()) {
249 continue;
250 }
251
252 if name.starts_with('$') || name.contains("$anon") || name.is_empty() {
254 continue;
255 }
256
257 if entry.tag == TAG_EXT_REF {
261 if superclass.is_none() {
262 superclass = Some(name);
263 } else if !traits.contains(&name) {
264 traits.push(name);
265 }
266 } else if entry.tag == TAG_EXT_MOD_CLASS_REF && !traits.contains(&name) {
267 traits.push(name);
271 }
272 }
273
274 (superclass, traits)
275}
276
277#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::scala::signature::{
285 FLAG_MODULE, TAG_CLASS_SYM, TAG_EXT_REF, TAG_MODULE_SYM, TAG_NONE_SYM, TAG_TERM_NAME,
286 TAG_TYPE_NAME,
287 };
288
289 fn encode_nat(mut value: u64) -> Vec<u8> {
293 let mut bytes = Vec::new();
294 loop {
295 let mut byte = (value & 0x7F) as u8;
296 value >>= 7;
297 if value != 0 {
298 byte |= 0x80;
299 }
300 bytes.push(byte);
301 if value == 0 {
302 break;
303 }
304 }
305 bytes
306 }
307
308 fn build_entry(tag: u8, data: &[u8]) -> Vec<u8> {
310 let mut entry = vec![tag];
311 entry.extend(encode_nat(data.len() as u64));
312 entry.extend_from_slice(data);
313 entry
314 }
315
316 fn build_signature(entries: Vec<Vec<u8>>) -> Vec<u8> {
318 let mut buf = vec![5, 0]; buf.extend(encode_nat(entries.len() as u64));
320 for entry in entries {
321 buf.extend(entry);
322 }
323 buf
324 }
325
326 fn build_sym_data(name_ref: u64, owner_ref: u64, flags: u64, info_ref: u64) -> Vec<u8> {
328 let mut data = Vec::new();
329 data.extend(encode_nat(name_ref));
330 data.extend(encode_nat(owner_ref));
331 data.extend(encode_nat(flags));
332 data.extend(encode_nat(info_ref));
333 data
334 }
335
336 fn stub_from_entries(entries: Vec<Vec<u8>>) -> ScalaSignatureStub {
338 ScalaSignatureStub {
339 bytes: build_signature(entries),
340 major_version: 5,
341 minor_version: 0,
342 }
343 }
344
345 fn simple_class_stub(name: &str, flags: u64) -> ScalaSignatureStub {
347 let name_entry = build_entry(TAG_TYPE_NAME, name.as_bytes());
348 let owner = build_entry(TAG_NONE_SYM, &[]);
349 let sym_data = build_sym_data(0, 1, flags, 0);
350 let cls = build_entry(TAG_CLASS_SYM, &sym_data);
351 stub_from_entries(vec![name_entry, owner, cls])
352 }
353
354 fn simple_module_stub(name: &str, flags: u64) -> ScalaSignatureStub {
356 let name_entry = build_entry(TAG_TERM_NAME, name.as_bytes());
357 let owner = build_entry(TAG_NONE_SYM, &[]);
358 let sym_data = build_sym_data(0, 1, flags, 0);
359 let module = build_entry(TAG_MODULE_SYM, &sym_data);
360 stub_from_entries(vec![name_entry, owner, module])
361 }
362
363 #[test]
366 fn detect_trait() {
367 let stub = simple_class_stub("Functor", FLAG_TRAIT | FLAG_INTERFACE | FLAG_ABSTRACT);
368 let meta = decode_scala_signature(&stub).unwrap();
369 assert_eq!(meta.kind, ScalaClassKind::Trait);
370 assert!(meta.is_abstract);
371 }
372
373 #[test]
374 fn detect_trait_via_interface_flag_only() {
375 let stub = simple_class_stub("Showable", FLAG_INTERFACE | FLAG_ABSTRACT);
377 let meta = decode_scala_signature(&stub).unwrap();
378 assert_eq!(meta.kind, ScalaClassKind::Trait);
379 }
380
381 #[test]
382 fn detect_object() {
383 let stub = simple_module_stub("Config", FLAG_MODULE);
384 let meta = decode_scala_signature(&stub).unwrap();
385 assert_eq!(meta.kind, ScalaClassKind::Object);
386 assert!(!meta.is_case);
387 }
388
389 #[test]
390 fn detect_package_object() {
391 let stub = simple_module_stub("package", FLAG_MODULE);
392 let meta = decode_scala_signature(&stub).unwrap();
393 assert_eq!(meta.kind, ScalaClassKind::PackageObject);
394 }
395
396 #[test]
397 fn detect_regular_class() {
398 let stub = simple_class_stub("MyService", 0);
399 let meta = decode_scala_signature(&stub).unwrap();
400 assert_eq!(meta.kind, ScalaClassKind::Class);
401 assert!(!meta.is_case);
402 assert!(!meta.is_sealed);
403 assert!(!meta.is_abstract);
404 }
405
406 #[test]
409 fn detect_case_class() {
410 let stub = simple_class_stub("Point", FLAG_CASE);
411 let meta = decode_scala_signature(&stub).unwrap();
412 assert_eq!(meta.kind, ScalaClassKind::Class);
413 assert!(meta.is_case);
414 }
415
416 #[test]
417 fn detect_case_object() {
418 let stub = simple_module_stub("Nil", FLAG_MODULE | FLAG_CASE);
419 let meta = decode_scala_signature(&stub).unwrap();
420 assert_eq!(meta.kind, ScalaClassKind::Object);
421 assert!(meta.is_case);
422 }
423
424 #[test]
427 fn detect_sealed_trait() {
428 let stub = simple_class_stub(
429 "Option",
430 FLAG_SEALED | FLAG_ABSTRACT | FLAG_TRAIT | FLAG_INTERFACE,
431 );
432 let meta = decode_scala_signature(&stub).unwrap();
433 assert_eq!(meta.kind, ScalaClassKind::Trait);
434 assert!(meta.is_sealed);
435 assert!(meta.is_abstract);
436 }
437
438 #[test]
439 fn detect_sealed_class() {
440 let stub = simple_class_stub("Expr", FLAG_SEALED | FLAG_ABSTRACT);
441 let meta = decode_scala_signature(&stub).unwrap();
442 assert_eq!(meta.kind, ScalaClassKind::Class);
443 assert!(meta.is_sealed);
444 assert!(meta.is_abstract);
445 }
446
447 #[test]
450 fn detect_public_visibility() {
451 let stub = simple_class_stub("Public", 0);
452 let meta = decode_scala_signature(&stub).unwrap();
453 assert_eq!(meta.visibility, ScalaVisibility::Public);
454 }
455
456 #[test]
457 fn detect_private_visibility() {
458 let stub = simple_class_stub("Private", FLAG_PRIVATE);
459 let meta = decode_scala_signature(&stub).unwrap();
460 assert_eq!(meta.visibility, ScalaVisibility::Private);
461 }
462
463 #[test]
464 fn detect_protected_visibility() {
465 let stub = simple_class_stub("Protected", FLAG_PROTECTED);
466 let meta = decode_scala_signature(&stub).unwrap();
467 assert_eq!(meta.visibility, ScalaVisibility::Protected);
468 }
469
470 #[test]
473 fn detect_abstract_class() {
474 let stub = simple_class_stub("AbstractBase", FLAG_ABSTRACT);
475 let meta = decode_scala_signature(&stub).unwrap();
476 assert_eq!(meta.kind, ScalaClassKind::Class);
477 assert!(meta.is_abstract);
478 assert!(!meta.is_sealed);
479 assert!(!meta.is_case);
480 }
481
482 #[test]
485 fn extract_superclass_from_ext_ref() {
486 let class_name = build_entry(TAG_TYPE_NAME, b"Child");
488 let owner = build_entry(TAG_NONE_SYM, &[]);
489 let sym_data = build_sym_data(0, 1, 0, 0);
490 let cls = build_entry(TAG_CLASS_SYM, &sym_data);
491
492 let parent_name = build_entry(TAG_TERM_NAME, b"Parent");
494 let mut ext_data = Vec::new();
495 ext_data.extend(encode_nat(3)); let ext = build_entry(TAG_EXT_REF, &ext_data);
497
498 let stub = stub_from_entries(vec![class_name, owner, cls, parent_name, ext]);
499 let meta = decode_scala_signature(&stub).unwrap();
500
501 assert_eq!(meta.superclass, Some("Parent".to_string()));
502 }
503
504 #[test]
505 fn skip_object_and_anyref_in_hierarchy() {
506 let class_name = build_entry(TAG_TYPE_NAME, b"Foo");
507 let owner = build_entry(TAG_NONE_SYM, &[]);
508 let sym_data = build_sym_data(0, 1, 0, 0);
509 let cls = build_entry(TAG_CLASS_SYM, &sym_data);
510
511 let obj_name = build_entry(TAG_TERM_NAME, b"Object");
513 let mut ext1_data = Vec::new();
514 ext1_data.extend(encode_nat(3));
515 let ext1 = build_entry(TAG_EXT_REF, &ext1_data);
516
517 let anyref_name = build_entry(TAG_TERM_NAME, b"AnyRef");
518 let mut ext2_data = Vec::new();
519 ext2_data.extend(encode_nat(5));
520 let ext2 = build_entry(TAG_EXT_REF, &ext2_data);
521
522 let stub = stub_from_entries(vec![
523 class_name,
524 owner,
525 cls,
526 obj_name,
527 ext1,
528 anyref_name,
529 ext2,
530 ]);
531 let meta = decode_scala_signature(&stub).unwrap();
532
533 assert_eq!(meta.superclass, None);
535 }
536
537 #[test]
540 fn unsupported_major_version_returns_none() {
541 let stub = ScalaSignatureStub {
542 bytes: vec![5, 0, 0], major_version: 4, minor_version: 0,
545 };
546 assert!(decode_scala_signature(&stub).is_none());
547 }
548
549 #[test]
550 fn malformed_bytes_returns_none() {
551 let stub = ScalaSignatureStub {
552 bytes: vec![5, 0, 1], major_version: 5,
554 minor_version: 0,
555 };
556 assert!(decode_scala_signature(&stub).is_none());
558 }
559
560 #[test]
561 fn empty_bytes_returns_none() {
562 let stub = ScalaSignatureStub {
563 bytes: vec![],
564 major_version: 5,
565 minor_version: 0,
566 };
567 assert!(decode_scala_signature(&stub).is_none());
568 }
569
570 #[test]
571 fn no_class_symbols_returns_none() {
572 let name = build_entry(TAG_TYPE_NAME, b"Foo");
574 let stub = stub_from_entries(vec![name]);
575 assert!(decode_scala_signature(&stub).is_none());
576 }
577
578 #[test]
579 fn anonymous_class_skipped_for_primary() {
580 let anon_name = build_entry(TAG_TYPE_NAME, b"$anon$1");
582 let owner = build_entry(TAG_NONE_SYM, &[]);
583 let sym_data = build_sym_data(0, 1, 0, 0);
584 let cls = build_entry(TAG_CLASS_SYM, &sym_data);
585
586 let stub = stub_from_entries(vec![anon_name, owner, cls]);
587 let meta = decode_scala_signature(&stub);
589 assert!(meta.is_some());
590 }
591
592 #[test]
595 fn sealed_abstract_case_class() {
596 let stub = simple_class_stub("Weird", FLAG_SEALED | FLAG_ABSTRACT | FLAG_CASE);
598 let meta = decode_scala_signature(&stub).unwrap();
599 assert_eq!(meta.kind, ScalaClassKind::Class);
600 assert!(meta.is_sealed);
601 assert!(meta.is_abstract);
602 assert!(meta.is_case);
603 }
604
605 #[test]
606 fn private_sealed_trait() {
607 let stub = simple_class_stub(
608 "Internal",
609 FLAG_PRIVATE | FLAG_SEALED | FLAG_TRAIT | FLAG_INTERFACE | FLAG_ABSTRACT,
610 );
611 let meta = decode_scala_signature(&stub).unwrap();
612 assert_eq!(meta.kind, ScalaClassKind::Trait);
613 assert_eq!(meta.visibility, ScalaVisibility::Private);
614 assert!(meta.is_sealed);
615 assert!(meta.is_abstract);
616 }
617}