Skip to main content

sqry_classpath/scala/
decoder.rs

1//! High-level decoder that produces structured Scala metadata from raw
2//! `@ScalaSignature` bytes.
3//!
4//! This module bridges the low-level [`ScalaSignatureReader`] (which parses the
5//! binary entry table) and the rest of the classpath pipeline by producing a
6//! [`ScalaClassMetadata`] with Scala-specific information: class kind (class /
7//! trait / object), visibility, case/sealed/abstract modifiers, and basic
8//! superclass/trait hierarchy.
9
10use log::warn;
11
12use crate::stub::model::ScalaSignatureStub;
13
14use super::signature::{
15    FLAG_ABSTRACT, FLAG_CASE, FLAG_INTERFACE, FLAG_PRIVATE, FLAG_PROTECTED, FLAG_SEALED,
16    FLAG_TRAIT, ScalaSignatureReader, TAG_EXT_MOD_CLASS_REF, TAG_EXT_REF, TAG_MODULE_SYM,
17};
18
19// ---------------------------------------------------------------------------
20// Public types
21// ---------------------------------------------------------------------------
22
23/// The kind of a Scala class-level entity.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum ScalaClassKind {
26    /// A regular `class`.
27    Class,
28    /// A `trait`.
29    Trait,
30    /// An `object` (singleton).
31    Object,
32    /// A package object (`package object foo`).
33    PackageObject,
34}
35
36/// Visibility of a Scala symbol.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum ScalaVisibility {
39    /// No access modifier (public by default in Scala).
40    Public,
41    /// `private` or `private[scope]`.
42    Private,
43    /// `protected` or `protected[scope]`.
44    Protected,
45}
46
47/// Decoded Scala metadata for a class/trait/object.
48///
49/// Produced by [`decode_scala_signature`] from a [`ScalaSignatureStub`].
50/// When decoding fails or the format is unsupported, the function returns
51/// `None` and the caller falls back to bytecode-only analysis.
52#[derive(Debug, Clone)]
53pub struct ScalaClassMetadata {
54    /// Scala class kind (class, trait, object, package object).
55    pub kind: ScalaClassKind,
56    /// Visibility modifier.
57    pub visibility: ScalaVisibility,
58    /// Whether this is a `case class` or `case object`.
59    pub is_case: bool,
60    /// Whether this is `sealed`.
61    pub is_sealed: bool,
62    /// Whether this is `abstract`.
63    pub is_abstract: bool,
64    /// Superclass name (if found in the signature).
65    pub superclass: Option<String>,
66    /// Mixed-in trait names.
67    pub traits: Vec<String>,
68}
69
70// ---------------------------------------------------------------------------
71// Public API
72// ---------------------------------------------------------------------------
73
74/// Decode Scala metadata from a [`ScalaSignatureStub`].
75///
76/// Returns `None` if the signature format is unsupported or decoding fails.
77/// In those cases the caller should fall back to bytecode-only analysis.
78///
79/// # Errors
80///
81/// This function never panics. All malformed data is handled gracefully by
82/// returning `None`.
83pub fn decode_scala_signature(stub: &ScalaSignatureStub) -> Option<ScalaClassMetadata> {
84    // Only support major version 5 (Scala 2.10+).
85    if stub.major_version != 5 {
86        warn!(
87            "unsupported Scala signature major version {} (expected 5)",
88            stub.major_version
89        );
90        return None;
91    }
92
93    let reader = ScalaSignatureReader::parse(&stub.bytes)?;
94
95    // Find the primary class/module symbol. The "primary" symbol is the one
96    // whose name matches the class file (heuristic: not a companion, not a
97    // nested anonymous class). In most cases this is the first CLASSsym or
98    // MODULEsym entry.
99    let symbols = reader.class_and_module_symbols();
100    if symbols.is_empty() {
101        warn!("no CLASSsym or MODULEsym entries found in Scala signature");
102        return None;
103    }
104
105    // Find the best candidate: prefer the first symbol that has a
106    // non-anonymous, non-empty name.
107    let (primary_index, primary_entry) = find_primary_symbol(&reader, &symbols)?;
108
109    let info = reader.read_symbol_info(primary_entry)?;
110    let flags = info.flags;
111
112    // Determine kind.
113    let kind = determine_kind(primary_entry.tag, flags, &reader, info.name_index);
114
115    // Determine visibility.
116    let visibility = determine_visibility(flags);
117
118    // Extract hierarchy from EXTref / EXTMODCLASSref entries.
119    let (superclass, traits) = extract_hierarchy(&reader, primary_index);
120
121    Some(ScalaClassMetadata {
122        kind,
123        visibility,
124        is_case: flags & FLAG_CASE != 0,
125        is_sealed: flags & FLAG_SEALED != 0,
126        is_abstract: flags & FLAG_ABSTRACT != 0,
127        superclass,
128        traits,
129    })
130}
131
132// ---------------------------------------------------------------------------
133// Internal helpers
134// ---------------------------------------------------------------------------
135
136/// Find the primary symbol among CLASSsym/MODULEsym entries.
137///
138/// Returns the index and entry reference, or `None` if no suitable symbol is
139/// found.
140fn find_primary_symbol<'a>(
141    reader: &'a ScalaSignatureReader,
142    symbols: &[(usize, &'a super::signature::SignatureEntry)],
143) -> Option<(usize, &'a super::signature::SignatureEntry)> {
144    // Prefer a symbol with a non-empty, non-anonymous name that is not a
145    // compiler-generated artifact (names starting with `$`).
146    for &(idx, entry) in symbols {
147        if let Some(info) = reader.read_symbol_info(entry)
148            && let Some(name) = reader.read_name(info.name_index)
149            && !name.is_empty()
150            && !name.starts_with('$')
151            && !name.contains("$anon")
152        {
153            return Some((idx, entry));
154        }
155    }
156
157    // Fall back to the very first symbol if all are anonymous/generated.
158    symbols.first().map(|&(idx, entry)| (idx, entry))
159}
160
161/// Determine the [`ScalaClassKind`] from the entry tag and flags.
162fn determine_kind(
163    tag: u8,
164    flags: u64,
165    reader: &ScalaSignatureReader,
166    name_index: usize,
167) -> ScalaClassKind {
168    // MODULEsym entries are objects.
169    if tag == TAG_MODULE_SYM {
170        // Check for package object: the name is typically "package".
171        if let Some(name) = reader.read_name(name_index)
172            && name == "package"
173        {
174            return ScalaClassKind::PackageObject;
175        }
176        return ScalaClassKind::Object;
177    }
178
179    // CLASSsym with TRAIT flag → trait.
180    if flags & FLAG_TRAIT != 0 || flags & FLAG_INTERFACE != 0 {
181        return ScalaClassKind::Trait;
182    }
183
184    ScalaClassKind::Class
185}
186
187/// Determine [`ScalaVisibility`] from flags.
188fn determine_visibility(flags: u64) -> ScalaVisibility {
189    if flags & FLAG_PRIVATE != 0 {
190        ScalaVisibility::Private
191    } else if flags & FLAG_PROTECTED != 0 {
192        ScalaVisibility::Protected
193    } else {
194        ScalaVisibility::Public
195    }
196}
197
198/// Extract superclass and trait names from the signature.
199///
200/// This is a heuristic approach for Tier 1: we look at EXTref and
201/// EXTMODCLASSref entries to find well-known JVM superclass and trait
202/// references. The actual parent type information is encoded in the type-info
203/// entry, but for Tier 1 we use the simpler approach of scanning external
204/// references.
205///
206/// Returns `(superclass, traits)` where `superclass` is the first non-Object
207/// superclass found, and `traits` are mixed-in trait names.
208fn extract_hierarchy(
209    reader: &ScalaSignatureReader,
210    _primary_index: usize,
211) -> (Option<String>, Vec<String>) {
212    let ext_refs = reader.ext_refs();
213
214    let mut superclass: Option<String> = None;
215    let mut traits = Vec::new();
216
217    // Known base types to skip.
218    const SKIP_NAMES: &[&str] = &[
219        "Object",
220        "AnyRef",
221        "Any",
222        "Serializable",
223        "Product",
224        "Equals",
225        "<empty>",
226        "java.lang.Object",
227        "scala.AnyRef",
228    ];
229
230    for &(_idx, entry) in &ext_refs {
231        if entry.tag != TAG_EXT_REF && entry.tag != TAG_EXT_MOD_CLASS_REF {
232            continue;
233        }
234
235        // Resolve the name via the name_ref in the EXTref data.
236        let mut pos = 0;
237        let Some(name_ref) = super::signature::read_nat(&entry.data, &mut pos) else {
238            continue;
239        };
240        let Some(name) = reader.read_name(name_ref as usize) else {
241            continue;
242        };
243
244        // Skip well-known base types that don't provide useful hierarchy info.
245        if SKIP_NAMES.contains(&name.as_str()) {
246            continue;
247        }
248
249        // Skip compiler-generated names.
250        if name.starts_with('$') || name.contains("$anon") || name.is_empty() {
251            continue;
252        }
253
254        // Heuristic: EXTMODCLASSref entries tend to be module/object references
255        // while EXTref entries are class/trait references. For Tier 1, we
256        // collect all unique non-skipped names.
257        if entry.tag == TAG_EXT_REF {
258            if superclass.is_none() {
259                superclass = Some(name);
260            } else if !traits.contains(&name) {
261                traits.push(name);
262            }
263        } else if entry.tag == TAG_EXT_MOD_CLASS_REF && !traits.contains(&name) {
264            // Module-class references are typically companion objects or
265            // well-known modules. Include them as potential trait references
266            // only if they look like trait names.
267            traits.push(name);
268        }
269    }
270
271    (superclass, traits)
272}
273
274// ---------------------------------------------------------------------------
275// Tests
276// ---------------------------------------------------------------------------
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use crate::scala::signature::{
282        FLAG_MODULE, TAG_CLASS_SYM, TAG_EXT_REF, TAG_MODULE_SYM, TAG_NONE_SYM, TAG_TERM_NAME,
283        TAG_TYPE_NAME,
284    };
285
286    // -- Test helpers -------------------------------------------------------
287
288    /// Encode a u64 as a Nat (variable-length).
289    fn encode_nat(mut value: u64) -> Vec<u8> {
290        let mut bytes = Vec::new();
291        loop {
292            let mut byte = (value & 0x7F) as u8;
293            value >>= 7;
294            if value != 0 {
295                byte |= 0x80;
296            }
297            bytes.push(byte);
298            if value == 0 {
299                break;
300            }
301        }
302        bytes
303    }
304
305    /// Build a minimal entry: tag + Nat-encoded length + data.
306    fn build_entry(tag: u8, data: &[u8]) -> Vec<u8> {
307        let mut entry = vec![tag];
308        entry.extend(encode_nat(data.len() as u64));
309        entry.extend_from_slice(data);
310        entry
311    }
312
313    /// Build a complete signature with version header + entry count + entries.
314    fn build_signature(entries: Vec<Vec<u8>>) -> Vec<u8> {
315        let mut buf = vec![5, 0]; // version 5.0
316        buf.extend(encode_nat(entries.len() as u64));
317        for entry in entries {
318            buf.extend(entry);
319        }
320        buf
321    }
322
323    /// Build a symbol info data block.
324    fn build_sym_data(name_ref: u64, owner_ref: u64, flags: u64, info_ref: u64) -> Vec<u8> {
325        let mut data = Vec::new();
326        data.extend(encode_nat(name_ref));
327        data.extend(encode_nat(owner_ref));
328        data.extend(encode_nat(flags));
329        data.extend(encode_nat(info_ref));
330        data
331    }
332
333    /// Create a `ScalaSignatureStub` from raw entries.
334    fn stub_from_entries(entries: Vec<Vec<u8>>) -> ScalaSignatureStub {
335        ScalaSignatureStub {
336            bytes: build_signature(entries),
337            major_version: 5,
338            minor_version: 0,
339        }
340    }
341
342    /// Build a minimal stub with a single CLASSsym with the given name and flags.
343    fn simple_class_stub(name: &str, flags: u64) -> ScalaSignatureStub {
344        let name_entry = build_entry(TAG_TYPE_NAME, name.as_bytes());
345        let owner = build_entry(TAG_NONE_SYM, &[]);
346        let sym_data = build_sym_data(0, 1, flags, 0);
347        let cls = build_entry(TAG_CLASS_SYM, &sym_data);
348        stub_from_entries(vec![name_entry, owner, cls])
349    }
350
351    /// Build a minimal stub with a single MODULEsym with the given name and flags.
352    fn simple_module_stub(name: &str, flags: u64) -> ScalaSignatureStub {
353        let name_entry = build_entry(TAG_TERM_NAME, name.as_bytes());
354        let owner = build_entry(TAG_NONE_SYM, &[]);
355        let sym_data = build_sym_data(0, 1, flags, 0);
356        let module = build_entry(TAG_MODULE_SYM, &sym_data);
357        stub_from_entries(vec![name_entry, owner, module])
358    }
359
360    // -- Kind detection tests -----------------------------------------------
361
362    #[test]
363    fn detect_trait() {
364        let stub = simple_class_stub("Functor", FLAG_TRAIT | FLAG_INTERFACE | FLAG_ABSTRACT);
365        let meta = decode_scala_signature(&stub).unwrap();
366        assert_eq!(meta.kind, ScalaClassKind::Trait);
367        assert!(meta.is_abstract);
368    }
369
370    #[test]
371    fn detect_trait_via_interface_flag_only() {
372        // Some Scala versions only set INTERFACE without TRAIT.
373        let stub = simple_class_stub("Showable", FLAG_INTERFACE | FLAG_ABSTRACT);
374        let meta = decode_scala_signature(&stub).unwrap();
375        assert_eq!(meta.kind, ScalaClassKind::Trait);
376    }
377
378    #[test]
379    fn detect_object() {
380        let stub = simple_module_stub("Config", FLAG_MODULE);
381        let meta = decode_scala_signature(&stub).unwrap();
382        assert_eq!(meta.kind, ScalaClassKind::Object);
383        assert!(!meta.is_case);
384    }
385
386    #[test]
387    fn detect_package_object() {
388        let stub = simple_module_stub("package", FLAG_MODULE);
389        let meta = decode_scala_signature(&stub).unwrap();
390        assert_eq!(meta.kind, ScalaClassKind::PackageObject);
391    }
392
393    #[test]
394    fn detect_regular_class() {
395        let stub = simple_class_stub("MyService", 0);
396        let meta = decode_scala_signature(&stub).unwrap();
397        assert_eq!(meta.kind, ScalaClassKind::Class);
398        assert!(!meta.is_case);
399        assert!(!meta.is_sealed);
400        assert!(!meta.is_abstract);
401    }
402
403    // -- Case class / object tests ------------------------------------------
404
405    #[test]
406    fn detect_case_class() {
407        let stub = simple_class_stub("Point", FLAG_CASE);
408        let meta = decode_scala_signature(&stub).unwrap();
409        assert_eq!(meta.kind, ScalaClassKind::Class);
410        assert!(meta.is_case);
411    }
412
413    #[test]
414    fn detect_case_object() {
415        let stub = simple_module_stub("Nil", FLAG_MODULE | FLAG_CASE);
416        let meta = decode_scala_signature(&stub).unwrap();
417        assert_eq!(meta.kind, ScalaClassKind::Object);
418        assert!(meta.is_case);
419    }
420
421    // -- Sealed tests -------------------------------------------------------
422
423    #[test]
424    fn detect_sealed_trait() {
425        let stub = simple_class_stub(
426            "Option",
427            FLAG_SEALED | FLAG_ABSTRACT | FLAG_TRAIT | FLAG_INTERFACE,
428        );
429        let meta = decode_scala_signature(&stub).unwrap();
430        assert_eq!(meta.kind, ScalaClassKind::Trait);
431        assert!(meta.is_sealed);
432        assert!(meta.is_abstract);
433    }
434
435    #[test]
436    fn detect_sealed_class() {
437        let stub = simple_class_stub("Expr", FLAG_SEALED | FLAG_ABSTRACT);
438        let meta = decode_scala_signature(&stub).unwrap();
439        assert_eq!(meta.kind, ScalaClassKind::Class);
440        assert!(meta.is_sealed);
441        assert!(meta.is_abstract);
442    }
443
444    // -- Visibility tests ---------------------------------------------------
445
446    #[test]
447    fn detect_public_visibility() {
448        let stub = simple_class_stub("Public", 0);
449        let meta = decode_scala_signature(&stub).unwrap();
450        assert_eq!(meta.visibility, ScalaVisibility::Public);
451    }
452
453    #[test]
454    fn detect_private_visibility() {
455        let stub = simple_class_stub("Private", FLAG_PRIVATE);
456        let meta = decode_scala_signature(&stub).unwrap();
457        assert_eq!(meta.visibility, ScalaVisibility::Private);
458    }
459
460    #[test]
461    fn detect_protected_visibility() {
462        let stub = simple_class_stub("Protected", FLAG_PROTECTED);
463        let meta = decode_scala_signature(&stub).unwrap();
464        assert_eq!(meta.visibility, ScalaVisibility::Protected);
465    }
466
467    // -- Abstract class tests -----------------------------------------------
468
469    #[test]
470    fn detect_abstract_class() {
471        let stub = simple_class_stub("AbstractBase", FLAG_ABSTRACT);
472        let meta = decode_scala_signature(&stub).unwrap();
473        assert_eq!(meta.kind, ScalaClassKind::Class);
474        assert!(meta.is_abstract);
475        assert!(!meta.is_sealed);
476        assert!(!meta.is_case);
477    }
478
479    // -- Hierarchy extraction tests -----------------------------------------
480
481    #[test]
482    fn extract_superclass_from_ext_ref() {
483        // Build a signature with a class that has an EXTref to a superclass.
484        let class_name = build_entry(TAG_TYPE_NAME, b"Child");
485        let owner = build_entry(TAG_NONE_SYM, &[]);
486        let sym_data = build_sym_data(0, 1, 0, 0);
487        let cls = build_entry(TAG_CLASS_SYM, &sym_data);
488
489        // Add EXTref for superclass "Parent".
490        let parent_name = build_entry(TAG_TERM_NAME, b"Parent");
491        let mut ext_data = Vec::new();
492        ext_data.extend(encode_nat(3)); // name_ref → "Parent" at index 3
493        let ext = build_entry(TAG_EXT_REF, &ext_data);
494
495        let stub = stub_from_entries(vec![class_name, owner, cls, parent_name, ext]);
496        let meta = decode_scala_signature(&stub).unwrap();
497
498        assert_eq!(meta.superclass, Some("Parent".to_string()));
499    }
500
501    #[test]
502    fn skip_object_and_anyref_in_hierarchy() {
503        let class_name = build_entry(TAG_TYPE_NAME, b"Foo");
504        let owner = build_entry(TAG_NONE_SYM, &[]);
505        let sym_data = build_sym_data(0, 1, 0, 0);
506        let cls = build_entry(TAG_CLASS_SYM, &sym_data);
507
508        // EXTrefs for "Object" and "AnyRef" should be skipped.
509        let obj_name = build_entry(TAG_TERM_NAME, b"Object");
510        let mut ext1_data = Vec::new();
511        ext1_data.extend(encode_nat(3));
512        let ext1 = build_entry(TAG_EXT_REF, &ext1_data);
513
514        let anyref_name = build_entry(TAG_TERM_NAME, b"AnyRef");
515        let mut ext2_data = Vec::new();
516        ext2_data.extend(encode_nat(5));
517        let ext2 = build_entry(TAG_EXT_REF, &ext2_data);
518
519        let stub = stub_from_entries(vec![
520            class_name,
521            owner,
522            cls,
523            obj_name,
524            ext1,
525            anyref_name,
526            ext2,
527        ]);
528        let meta = decode_scala_signature(&stub).unwrap();
529
530        // No useful superclass found — all are skipped.
531        assert_eq!(meta.superclass, None);
532    }
533
534    // -- Error handling tests -----------------------------------------------
535
536    #[test]
537    fn unsupported_major_version_returns_none() {
538        let stub = ScalaSignatureStub {
539            bytes: vec![5, 0, 0], // version 5.0, 0 entries
540            major_version: 4,     // unsupported
541            minor_version: 0,
542        };
543        assert!(decode_scala_signature(&stub).is_none());
544    }
545
546    #[test]
547    fn malformed_bytes_returns_none() {
548        let stub = ScalaSignatureStub {
549            bytes: vec![5, 0, 1], // claims 1 entry but no entry data
550            major_version: 5,
551            minor_version: 0,
552        };
553        // The reader should fail to parse the incomplete entry table.
554        assert!(decode_scala_signature(&stub).is_none());
555    }
556
557    #[test]
558    fn empty_bytes_returns_none() {
559        let stub = ScalaSignatureStub {
560            bytes: vec![],
561            major_version: 5,
562            minor_version: 0,
563        };
564        assert!(decode_scala_signature(&stub).is_none());
565    }
566
567    #[test]
568    fn no_class_symbols_returns_none() {
569        // A signature with only name entries and no CLASSsym/MODULEsym.
570        let name = build_entry(TAG_TYPE_NAME, b"Foo");
571        let stub = stub_from_entries(vec![name]);
572        assert!(decode_scala_signature(&stub).is_none());
573    }
574
575    #[test]
576    fn anonymous_class_skipped_for_primary() {
577        // If all symbols are anonymous, we still return Some with the first one.
578        let anon_name = build_entry(TAG_TYPE_NAME, b"$anon$1");
579        let owner = build_entry(TAG_NONE_SYM, &[]);
580        let sym_data = build_sym_data(0, 1, 0, 0);
581        let cls = build_entry(TAG_CLASS_SYM, &sym_data);
582
583        let stub = stub_from_entries(vec![anon_name, owner, cls]);
584        // Should still decode (falls back to first symbol).
585        let meta = decode_scala_signature(&stub);
586        assert!(meta.is_some());
587    }
588
589    // -- Combined modifier tests --------------------------------------------
590
591    #[test]
592    fn sealed_abstract_case_class() {
593        // This is unusual but valid: a sealed abstract case class.
594        let stub = simple_class_stub("Weird", FLAG_SEALED | FLAG_ABSTRACT | FLAG_CASE);
595        let meta = decode_scala_signature(&stub).unwrap();
596        assert_eq!(meta.kind, ScalaClassKind::Class);
597        assert!(meta.is_sealed);
598        assert!(meta.is_abstract);
599        assert!(meta.is_case);
600    }
601
602    #[test]
603    fn private_sealed_trait() {
604        let stub = simple_class_stub(
605            "Internal",
606            FLAG_PRIVATE | FLAG_SEALED | FLAG_TRAIT | FLAG_INTERFACE | FLAG_ABSTRACT,
607        );
608        let meta = decode_scala_signature(&stub).unwrap();
609        assert_eq!(meta.kind, ScalaClassKind::Trait);
610        assert_eq!(meta.visibility, ScalaVisibility::Private);
611        assert!(meta.is_sealed);
612        assert!(meta.is_abstract);
613    }
614}