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
10// Scala signature indices fit in u32; casts are intentional
11#![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// ---------------------------------------------------------------------------
23// Public types
24// ---------------------------------------------------------------------------
25
26/// The kind of a Scala class-level entity.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum ScalaClassKind {
29    /// A regular `class`.
30    Class,
31    /// A `trait`.
32    Trait,
33    /// An `object` (singleton).
34    Object,
35    /// A package object (`package object foo`).
36    PackageObject,
37}
38
39/// Visibility of a Scala symbol.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum ScalaVisibility {
42    /// No access modifier (public by default in Scala).
43    Public,
44    /// `private` or `private[scope]`.
45    Private,
46    /// `protected` or `protected[scope]`.
47    Protected,
48}
49
50/// Decoded Scala metadata for a class/trait/object.
51///
52/// Produced by [`decode_scala_signature`] from a [`ScalaSignatureStub`].
53/// When decoding fails or the format is unsupported, the function returns
54/// `None` and the caller falls back to bytecode-only analysis.
55#[derive(Debug, Clone)]
56pub struct ScalaClassMetadata {
57    /// Scala class kind (class, trait, object, package object).
58    pub kind: ScalaClassKind,
59    /// Visibility modifier.
60    pub visibility: ScalaVisibility,
61    /// Whether this is a `case class` or `case object`.
62    pub is_case: bool,
63    /// Whether this is `sealed`.
64    pub is_sealed: bool,
65    /// Whether this is `abstract`.
66    pub is_abstract: bool,
67    /// Superclass name (if found in the signature).
68    pub superclass: Option<String>,
69    /// Mixed-in trait names.
70    pub traits: Vec<String>,
71}
72
73// ---------------------------------------------------------------------------
74// Public API
75// ---------------------------------------------------------------------------
76
77/// Decode Scala metadata from a [`ScalaSignatureStub`].
78///
79/// Returns `None` if the signature format is unsupported or decoding fails.
80/// In those cases the caller should fall back to bytecode-only analysis.
81///
82/// # Errors
83///
84/// This function never panics. All malformed data is handled gracefully by
85/// returning `None`.
86#[must_use]
87pub fn decode_scala_signature(stub: &ScalaSignatureStub) -> Option<ScalaClassMetadata> {
88    // Only support major version 5 (Scala 2.10+).
89    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    // Find the primary class/module symbol. The "primary" symbol is the one
100    // whose name matches the class file (heuristic: not a companion, not a
101    // nested anonymous class). In most cases this is the first CLASSsym or
102    // MODULEsym entry.
103    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    // Find the best candidate: prefer the first symbol that has a
110    // non-anonymous, non-empty name.
111    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    // Determine kind.
117    let kind = determine_kind(primary_entry.tag, flags, &reader, info.name_index);
118
119    // Determine visibility.
120    let visibility = determine_visibility(flags);
121
122    // Extract hierarchy from EXTref / EXTMODCLASSref entries.
123    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
136// ---------------------------------------------------------------------------
137// Internal helpers
138// ---------------------------------------------------------------------------
139
140/// Find the primary symbol among CLASSsym/MODULEsym entries.
141///
142/// Returns the index and entry reference, or `None` if no suitable symbol is
143/// found.
144fn find_primary_symbol<'a>(
145    reader: &'a ScalaSignatureReader,
146    symbols: &[(usize, &'a super::signature::SignatureEntry)],
147) -> Option<(usize, &'a super::signature::SignatureEntry)> {
148    // Prefer a symbol with a non-empty, non-anonymous name that is not a
149    // compiler-generated artifact (names starting with `$`).
150    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    // Fall back to the very first symbol if all are anonymous/generated.
162    symbols.first().map(|&(idx, entry)| (idx, entry))
163}
164
165/// Determine the [`ScalaClassKind`] from the entry tag and flags.
166fn determine_kind(
167    tag: u8,
168    flags: u64,
169    reader: &ScalaSignatureReader,
170    name_index: usize,
171) -> ScalaClassKind {
172    // MODULEsym entries are objects.
173    if tag == TAG_MODULE_SYM {
174        // Check for package object: the name is typically "package".
175        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    // CLASSsym with TRAIT flag → trait.
184    if flags & FLAG_TRAIT != 0 || flags & FLAG_INTERFACE != 0 {
185        return ScalaClassKind::Trait;
186    }
187
188    ScalaClassKind::Class
189}
190
191/// Determine [`ScalaVisibility`] from flags.
192fn 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
202/// Extract superclass and trait names from the signature.
203///
204/// This is a heuristic approach for Tier 1: we look at `EXTref` and
205/// `EXTMODCLASSref` entries to find well-known JVM superclass and trait
206/// references. The actual parent type information is encoded in the type-info
207/// entry, but for Tier 1 we use the simpler approach of scanning external
208/// references.
209///
210/// Returns `(superclass, traits)` where `superclass` is the first non-Object
211/// superclass found, and `traits` are mixed-in trait names.
212fn extract_hierarchy(
213    reader: &ScalaSignatureReader,
214    _primary_index: usize,
215) -> (Option<String>, Vec<String>) {
216    // Known base types to skip.
217    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        // Resolve the name via the name_ref in the EXTref data.
239        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        // Skip well-known base types that don't provide useful hierarchy info.
248        if SKIP_NAMES.contains(&name.as_str()) {
249            continue;
250        }
251
252        // Skip compiler-generated names.
253        if name.starts_with('$') || name.contains("$anon") || name.is_empty() {
254            continue;
255        }
256
257        // Heuristic: EXTMODCLASSref entries tend to be module/object references
258        // while EXTref entries are class/trait references. For Tier 1, we
259        // collect all unique non-skipped names.
260        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            // Module-class references are typically companion objects or
268            // well-known modules. Include them as potential trait references
269            // only if they look like trait names.
270            traits.push(name);
271        }
272    }
273
274    (superclass, traits)
275}
276
277// ---------------------------------------------------------------------------
278// Tests
279// ---------------------------------------------------------------------------
280
281#[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    // -- Test helpers -------------------------------------------------------
290
291    /// Encode a u64 as a Nat (variable-length).
292    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    /// Build a minimal entry: tag + Nat-encoded length + data.
309    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    /// Build a complete signature with version header + entry count + entries.
317    fn build_signature(entries: Vec<Vec<u8>>) -> Vec<u8> {
318        let mut buf = vec![5, 0]; // version 5.0
319        buf.extend(encode_nat(entries.len() as u64));
320        for entry in entries {
321            buf.extend(entry);
322        }
323        buf
324    }
325
326    /// Build a symbol info data block.
327    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    /// Create a `ScalaSignatureStub` from raw entries.
337    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    /// Build a minimal stub with a single `CLASSsym` with the given name and flags.
346    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    /// Build a minimal stub with a single `MODULEsym` with the given name and flags.
355    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    // -- Kind detection tests -----------------------------------------------
364
365    #[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        // Some Scala versions only set INTERFACE without TRAIT.
376        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    // -- Case class / object tests ------------------------------------------
407
408    #[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    // -- Sealed tests -------------------------------------------------------
425
426    #[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    // -- Visibility tests ---------------------------------------------------
448
449    #[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    // -- Abstract class tests -----------------------------------------------
471
472    #[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    // -- Hierarchy extraction tests -----------------------------------------
483
484    #[test]
485    fn extract_superclass_from_ext_ref() {
486        // Build a signature with a class that has an EXTref to a superclass.
487        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        // Add EXTref for superclass "Parent".
493        let parent_name = build_entry(TAG_TERM_NAME, b"Parent");
494        let mut ext_data = Vec::new();
495        ext_data.extend(encode_nat(3)); // name_ref → "Parent" at index 3
496        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        // EXTrefs for "Object" and "AnyRef" should be skipped.
512        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        // No useful superclass found — all are skipped.
534        assert_eq!(meta.superclass, None);
535    }
536
537    // -- Error handling tests -----------------------------------------------
538
539    #[test]
540    fn unsupported_major_version_returns_none() {
541        let stub = ScalaSignatureStub {
542            bytes: vec![5, 0, 0], // version 5.0, 0 entries
543            major_version: 4,     // unsupported
544            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], // claims 1 entry but no entry data
553            major_version: 5,
554            minor_version: 0,
555        };
556        // The reader should fail to parse the incomplete entry table.
557        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        // A signature with only name entries and no CLASSsym/MODULEsym.
573        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        // If all symbols are anonymous, we still return Some with the first one.
581        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        // Should still decode (falls back to first symbol).
588        let meta = decode_scala_signature(&stub);
589        assert!(meta.is_some());
590    }
591
592    // -- Combined modifier tests --------------------------------------------
593
594    #[test]
595    fn sealed_abstract_case_class() {
596        // This is unusual but valid: a sealed abstract case class.
597        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}