Skip to main content

sqry_classpath/bytecode/
lambda.rs

1//! Lambda/method-reference target extraction from the `BootstrapMethods`
2//! attribute.
3//!
4//! JVM compilers (javac, kotlinc, scalac, etc.) compile lambda expressions and
5//! method references into `invokedynamic` instructions whose bootstrap method
6//! is [`java/lang/invoke/LambdaMetafactory.metafactory`][metafactory] (or
7//! `altMetafactory`). The third bootstrap argument (index 2) of such entries
8//! is a `CONSTANT_MethodHandle_info` that points to the **actual target
9//! method** being captured.
10//!
11//! This module extracts those targets from a parsed [`cafebabe::ClassFile`] and
12//! returns them as [`LambdaTargetStub`] records for inclusion in the class
13//! stub.
14//!
15//! [metafactory]: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/invoke/LambdaMetafactory.html
16
17use cafebabe::attributes::AttributeData;
18use cafebabe::constant_pool::{
19    BootstrapArgument, MethodHandle, ReferenceKind as CafeReferenceKind,
20};
21
22use crate::stub::model::{LambdaTargetStub, ReferenceKind};
23
24use super::constants::class_name_to_fqn;
25
26// ---------------------------------------------------------------------------
27// Constants
28// ---------------------------------------------------------------------------
29
30/// The internal class name of `LambdaMetafactory`.
31const LAMBDA_METAFACTORY_CLASS: &str = "java/lang/invoke/LambdaMetafactory";
32
33/// The standard bootstrap method name for lambda expressions.
34const METAFACTORY_METHOD: &str = "metafactory";
35
36/// The alternative bootstrap method name for complex lambda expressions
37/// (serialisable lambdas, intersection-type target, etc.).
38const ALT_METAFACTORY_METHOD: &str = "altMetafactory";
39
40/// Index of the implementation method handle within the bootstrap arguments.
41/// For `LambdaMetafactory.metafactory`, the arguments are:
42///   0 — samMethodType  (MethodType)
43///   1 — implMethod     (MethodHandle) — **sometimes**
44///   2 — implMethod     (MethodHandle) — **standard position**
45///
46/// Per the JVM spec and `LambdaMetafactory` javadoc, argument index 2 is the
47/// implementation `MethodHandle`.
48const IMPL_METHOD_ARG_INDEX: usize = 2;
49
50// ---------------------------------------------------------------------------
51// Public API
52// ---------------------------------------------------------------------------
53
54/// Extract lambda/method-reference targets from a parsed class file.
55///
56/// Iterates class-level attributes, finds the `BootstrapMethods` attribute, and
57/// filters entries whose bootstrap method handle points to
58/// `LambdaMetafactory.metafactory` or `altMetafactory`. For each matching
59/// entry, the third bootstrap argument (a `MethodHandle`) is converted to a
60/// [`LambdaTargetStub`].
61///
62/// # Returns
63///
64/// A `Vec` of targets, potentially empty if no `BootstrapMethods` attribute
65/// exists or none of the entries are `LambdaMetafactory` invocations.
66///
67/// Non-`LambdaMetafactory` bootstrap entries are silently skipped. Malformed
68/// entries (e.g., fewer than 3 arguments, wrong argument type at index 2) are
69/// logged as warnings and skipped.
70pub fn extract_lambda_targets(class: &cafebabe::ClassFile<'_>) -> Vec<LambdaTargetStub> {
71    let mut targets = Vec::new();
72
73    for attr in &class.attributes {
74        if let AttributeData::BootstrapMethods(entries) = &attr.data {
75            for (idx, entry) in entries.iter().enumerate() {
76                // Check if the bootstrap method points to LambdaMetafactory.
77                if !is_lambda_metafactory(&entry.method) {
78                    continue;
79                }
80
81                // Extract the implementation MethodHandle from argument index 2.
82                match extract_impl_handle(idx, &entry.arguments) {
83                    Some(stub) => targets.push(stub),
84                    None => continue,
85                }
86            }
87        }
88    }
89
90    targets
91}
92
93// ---------------------------------------------------------------------------
94// Internals
95// ---------------------------------------------------------------------------
96
97/// Check whether a bootstrap method handle points to `LambdaMetafactory`.
98///
99/// The handle's class name must be `java/lang/invoke/LambdaMetafactory` and its
100/// method name must be `metafactory` or `altMetafactory`.
101fn is_lambda_metafactory(handle: &MethodHandle<'_>) -> bool {
102    handle.class_name.as_ref() == LAMBDA_METAFACTORY_CLASS
103        && (handle.member_ref.name.as_ref() == METAFACTORY_METHOD
104            || handle.member_ref.name.as_ref() == ALT_METAFACTORY_METHOD)
105}
106
107/// Extract the implementation `MethodHandle` from bootstrap arguments and
108/// convert it to a [`LambdaTargetStub`].
109///
110/// Returns `None` (with a warning log) if the arguments are too few or the
111/// third argument is not a `MethodHandle`.
112fn extract_impl_handle(
113    bootstrap_idx: usize,
114    arguments: &[BootstrapArgument<'_>],
115) -> Option<LambdaTargetStub> {
116    if arguments.len() <= IMPL_METHOD_ARG_INDEX {
117        log::warn!(
118            "BootstrapMethods entry {bootstrap_idx}: expected at least {} arguments, \
119             found {}; skipping",
120            IMPL_METHOD_ARG_INDEX + 1,
121            arguments.len(),
122        );
123        return None;
124    }
125
126    match &arguments[IMPL_METHOD_ARG_INDEX] {
127        BootstrapArgument::MethodHandle(handle) => {
128            let reference_kind = match convert_reference_kind(handle.kind) {
129                Some(kind) => kind,
130                None => {
131                    log::warn!(
132                        "BootstrapMethods entry {bootstrap_idx}: \
133                         unsupported reference kind {:?}; skipping",
134                        handle.kind,
135                    );
136                    return None;
137                }
138            };
139
140            Some(LambdaTargetStub {
141                owner_fqn: class_name_to_fqn(handle.class_name.as_ref()),
142                method_name: handle.member_ref.name.to_string(),
143                method_descriptor: handle.member_ref.descriptor.to_string(),
144                reference_kind,
145            })
146        }
147        other => {
148            log::warn!(
149                "BootstrapMethods entry {bootstrap_idx}: expected MethodHandle at \
150                 argument index {IMPL_METHOD_ARG_INDEX}, found {kind}; skipping",
151                kind = bootstrap_arg_kind_name(other),
152            );
153            None
154        }
155    }
156}
157
158/// Convert a cafebabe [`CafeReferenceKind`] to our model [`ReferenceKind`].
159fn convert_reference_kind(kind: CafeReferenceKind) -> Option<ReferenceKind> {
160    Some(match kind {
161        CafeReferenceKind::GetField => ReferenceKind::GetField,
162        CafeReferenceKind::GetStatic => ReferenceKind::GetStatic,
163        CafeReferenceKind::PutField => ReferenceKind::PutField,
164        CafeReferenceKind::PutStatic => ReferenceKind::PutStatic,
165        CafeReferenceKind::InvokeVirtual => ReferenceKind::InvokeVirtual,
166        CafeReferenceKind::InvokeStatic => ReferenceKind::InvokeStatic,
167        CafeReferenceKind::InvokeSpecial => ReferenceKind::InvokeSpecial,
168        CafeReferenceKind::NewInvokeSpecial => ReferenceKind::NewInvokeSpecial,
169        CafeReferenceKind::InvokeInterface => ReferenceKind::InvokeInterface,
170    })
171}
172
173/// Return a human-readable name for a [`BootstrapArgument`] variant (for log
174/// messages).
175fn bootstrap_arg_kind_name(arg: &BootstrapArgument<'_>) -> &'static str {
176    match arg {
177        BootstrapArgument::LiteralConstant(_) => "LiteralConstant",
178        BootstrapArgument::ClassInfo(_) => "ClassInfo",
179        BootstrapArgument::MethodHandle(_) => "MethodHandle",
180        BootstrapArgument::MethodType(_) => "MethodType",
181    }
182}
183
184// ---------------------------------------------------------------------------
185// Tests
186// ---------------------------------------------------------------------------
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use cafebabe::attributes::{AttributeData, AttributeInfo, BootstrapMethodEntry};
192    use cafebabe::constant_pool::{
193        BootstrapArgument, MemberKind, MethodHandle, NameAndType,
194        ReferenceKind as CafeReferenceKind,
195    };
196    use std::borrow::Cow;
197
198    // -- Test helpers ---------------------------------------------------------
199
200    /// Build a `MethodHandle` pointing to `LambdaMetafactory.metafactory`.
201    fn metafactory_handle<'a>() -> MethodHandle<'a> {
202        MethodHandle {
203            kind: CafeReferenceKind::InvokeStatic,
204            class_name: Cow::Borrowed(LAMBDA_METAFACTORY_CLASS),
205            member_kind: MemberKind::Method,
206            member_ref: NameAndType {
207                name: Cow::Borrowed(METAFACTORY_METHOD),
208                descriptor: Cow::Borrowed(
209                    "(Ljava/lang/invoke/MethodHandles$Lookup;\
210                     Ljava/lang/String;\
211                     Ljava/lang/invoke/MethodType;\
212                     Ljava/lang/invoke/MethodType;\
213                     Ljava/lang/invoke/MethodHandle;\
214                     Ljava/lang/invoke/MethodType;\
215                     )Ljava/lang/invoke/CallSite;",
216                ),
217            },
218        }
219    }
220
221    /// Build a `MethodHandle` pointing to `LambdaMetafactory.altMetafactory`.
222    fn alt_metafactory_handle<'a>() -> MethodHandle<'a> {
223        MethodHandle {
224            kind: CafeReferenceKind::InvokeStatic,
225            class_name: Cow::Borrowed(LAMBDA_METAFACTORY_CLASS),
226            member_kind: MemberKind::Method,
227            member_ref: NameAndType {
228                name: Cow::Borrowed(ALT_METAFACTORY_METHOD),
229                descriptor: Cow::Borrowed(
230                    "(Ljava/lang/invoke/MethodHandles$Lookup;\
231                     Ljava/lang/String;\
232                     Ljava/lang/invoke/MethodType;\
233                     [Ljava/lang/Object;\
234                     )Ljava/lang/invoke/CallSite;",
235                ),
236            },
237        }
238    }
239
240    /// Build a non-lambda bootstrap handle (e.g., `StringConcatFactory`).
241    fn string_concat_handle<'a>() -> MethodHandle<'a> {
242        MethodHandle {
243            kind: CafeReferenceKind::InvokeStatic,
244            class_name: Cow::Borrowed("java/lang/invoke/StringConcatFactory"),
245            member_kind: MemberKind::Method,
246            member_ref: NameAndType {
247                name: Cow::Borrowed("makeConcatWithConstants"),
248                descriptor: Cow::Borrowed(
249                    "(Ljava/lang/invoke/MethodHandles$Lookup;\
250                     Ljava/lang/String;\
251                     Ljava/lang/invoke/MethodType;\
252                     Ljava/lang/String;\
253                     [Ljava/lang/Object;\
254                     )Ljava/lang/invoke/CallSite;",
255                ),
256            },
257        }
258    }
259
260    /// Build the standard 3-argument list for a `LambdaMetafactory` entry.
261    ///
262    /// Arguments: [MethodType(sam_descriptor), MethodType(instantiated),
263    /// MethodHandle(impl)].
264    fn lambda_bootstrap_args<'a>(
265        impl_kind: CafeReferenceKind,
266        impl_class: &'a str,
267        impl_name: &'a str,
268        impl_descriptor: &'a str,
269    ) -> Vec<BootstrapArgument<'a>> {
270        vec![
271            // arg 0: SAM method type
272            BootstrapArgument::MethodType(Cow::Borrowed("(Ljava/lang/Object;)Ljava/lang/Object;")),
273            // arg 1: instantiated method type
274            BootstrapArgument::MethodType(Cow::Borrowed("(Ljava/lang/String;)Ljava/lang/String;")),
275            // arg 2: implementation method handle
276            BootstrapArgument::MethodHandle(MethodHandle {
277                kind: impl_kind,
278                class_name: Cow::Borrowed(impl_class),
279                member_kind: MemberKind::Method,
280                member_ref: NameAndType {
281                    name: Cow::Borrowed(impl_name),
282                    descriptor: Cow::Borrowed(impl_descriptor),
283                },
284            }),
285        ]
286    }
287
288    /// Parse a real class file and extract lambda targets. This requires
289    /// building a `ClassFile` from bytes, which is the full integration path.
290    /// These unit tests use the component function directly with constructed
291    /// bootstrap entries instead.
292
293    // -- Test 1: No BootstrapMethods attribute → empty result -----------------
294
295    #[test]
296    fn no_bootstrap_methods_returns_empty() {
297        // Simulate a class with no BootstrapMethods attribute by calling the
298        // extraction logic directly on empty attribute lists.
299        let attrs: Vec<AttributeInfo<'_>> = vec![];
300        let targets = extract_lambda_targets_from_attrs(&attrs);
301        assert!(targets.is_empty(), "Expected empty targets");
302    }
303
304    // -- Test 2: Lambda target from stream().map(String::toUpperCase) ---------
305
306    #[test]
307    fn lambda_target_from_method_reference() {
308        let entries = vec![BootstrapMethodEntry {
309            method: metafactory_handle(),
310            arguments: lambda_bootstrap_args(
311                CafeReferenceKind::InvokeVirtual,
312                "java/lang/String",
313                "toUpperCase",
314                "()Ljava/lang/String;",
315            ),
316        }];
317
318        let attrs = vec![AttributeInfo {
319            name: Cow::Borrowed("BootstrapMethods"),
320            data: AttributeData::BootstrapMethods(entries),
321        }];
322
323        let targets = extract_lambda_targets_from_attrs(&attrs);
324
325        assert_eq!(targets.len(), 1);
326        assert_eq!(targets[0].owner_fqn, "java.lang.String");
327        assert_eq!(targets[0].method_name, "toUpperCase");
328        assert_eq!(targets[0].method_descriptor, "()Ljava/lang/String;");
329        assert_eq!(targets[0].reference_kind, ReferenceKind::InvokeVirtual);
330    }
331
332    // -- Test 3: Method reference target correctly identified -----------------
333
334    #[test]
335    fn method_reference_with_invoke_static() {
336        let entries = vec![BootstrapMethodEntry {
337            method: metafactory_handle(),
338            arguments: lambda_bootstrap_args(
339                CafeReferenceKind::InvokeStatic,
340                "java/lang/Integer",
341                "parseInt",
342                "(Ljava/lang/String;)I",
343            ),
344        }];
345
346        let attrs = vec![AttributeInfo {
347            name: Cow::Borrowed("BootstrapMethods"),
348            data: AttributeData::BootstrapMethods(entries),
349        }];
350
351        let targets = extract_lambda_targets_from_attrs(&attrs);
352
353        assert_eq!(targets.len(), 1);
354        assert_eq!(targets[0].owner_fqn, "java.lang.Integer");
355        assert_eq!(targets[0].method_name, "parseInt");
356        assert_eq!(targets[0].method_descriptor, "(Ljava/lang/String;)I");
357        assert_eq!(targets[0].reference_kind, ReferenceKind::InvokeStatic);
358    }
359
360    // -- Test 4: Non-LambdaMetafactory bootstrap entries skipped --------------
361
362    #[test]
363    fn non_lambda_metafactory_skipped() {
364        let entries = vec![BootstrapMethodEntry {
365            method: string_concat_handle(),
366            arguments: vec![BootstrapArgument::LiteralConstant(
367                cafebabe::constant_pool::LiteralConstant::String(Cow::Borrowed("\u{1}Hello \u{1}")),
368            )],
369        }];
370
371        let attrs = vec![AttributeInfo {
372            name: Cow::Borrowed("BootstrapMethods"),
373            data: AttributeData::BootstrapMethods(entries),
374        }];
375
376        let targets = extract_lambda_targets_from_attrs(&attrs);
377        assert!(
378            targets.is_empty(),
379            "Non-LambdaMetafactory should be skipped"
380        );
381    }
382
383    // -- Test 5: Multiple lambda targets in one class -------------------------
384
385    #[test]
386    fn multiple_lambda_targets() {
387        let entries = vec![
388            // Entry 0: String::toUpperCase method reference
389            BootstrapMethodEntry {
390                method: metafactory_handle(),
391                arguments: lambda_bootstrap_args(
392                    CafeReferenceKind::InvokeVirtual,
393                    "java/lang/String",
394                    "toUpperCase",
395                    "()Ljava/lang/String;",
396                ),
397            },
398            // Entry 1: StringConcatFactory (not lambda — should be skipped)
399            BootstrapMethodEntry {
400                method: string_concat_handle(),
401                arguments: vec![],
402            },
403            // Entry 2: Constructor reference (NewInvokeSpecial)
404            BootstrapMethodEntry {
405                method: metafactory_handle(),
406                arguments: lambda_bootstrap_args(
407                    CafeReferenceKind::NewInvokeSpecial,
408                    "java/util/ArrayList",
409                    "<init>",
410                    "()V",
411                ),
412            },
413            // Entry 3: altMetafactory — serialisable lambda
414            BootstrapMethodEntry {
415                method: alt_metafactory_handle(),
416                arguments: lambda_bootstrap_args(
417                    CafeReferenceKind::InvokeStatic,
418                    "com/example/Service",
419                    "lambda$process$0",
420                    "(Ljava/lang/Object;)V",
421                ),
422            },
423        ];
424
425        let attrs = vec![AttributeInfo {
426            name: Cow::Borrowed("BootstrapMethods"),
427            data: AttributeData::BootstrapMethods(entries),
428        }];
429
430        let targets = extract_lambda_targets_from_attrs(&attrs);
431
432        // 3 lambda entries (indices 0, 2, 3); index 1 is StringConcatFactory.
433        assert_eq!(
434            targets.len(),
435            3,
436            "Expected 3 lambda targets, got {}",
437            targets.len()
438        );
439
440        assert_eq!(targets[0].owner_fqn, "java.lang.String");
441        assert_eq!(targets[0].method_name, "toUpperCase");
442        assert_eq!(targets[0].reference_kind, ReferenceKind::InvokeVirtual);
443
444        assert_eq!(targets[1].owner_fqn, "java.util.ArrayList");
445        assert_eq!(targets[1].method_name, "<init>");
446        assert_eq!(targets[1].reference_kind, ReferenceKind::NewInvokeSpecial);
447
448        assert_eq!(targets[2].owner_fqn, "com.example.Service");
449        assert_eq!(targets[2].method_name, "lambda$process$0");
450        assert_eq!(targets[2].reference_kind, ReferenceKind::InvokeStatic);
451    }
452
453    // -- Test 6: Reference kind correctly mapped for all variants -------------
454
455    #[test]
456    fn reference_kind_mapping_exhaustive() {
457        let cafe_to_model = [
458            (CafeReferenceKind::GetField, ReferenceKind::GetField),
459            (CafeReferenceKind::GetStatic, ReferenceKind::GetStatic),
460            (CafeReferenceKind::PutField, ReferenceKind::PutField),
461            (CafeReferenceKind::PutStatic, ReferenceKind::PutStatic),
462            (
463                CafeReferenceKind::InvokeVirtual,
464                ReferenceKind::InvokeVirtual,
465            ),
466            (CafeReferenceKind::InvokeStatic, ReferenceKind::InvokeStatic),
467            (
468                CafeReferenceKind::InvokeSpecial,
469                ReferenceKind::InvokeSpecial,
470            ),
471            (
472                CafeReferenceKind::NewInvokeSpecial,
473                ReferenceKind::NewInvokeSpecial,
474            ),
475            (
476                CafeReferenceKind::InvokeInterface,
477                ReferenceKind::InvokeInterface,
478            ),
479        ];
480
481        for (cafe_kind, expected_model_kind) in &cafe_to_model {
482            let result = convert_reference_kind(*cafe_kind);
483            assert_eq!(
484                result,
485                Some(*expected_model_kind),
486                "Mapping failed for {cafe_kind:?}"
487            );
488        }
489    }
490
491    // -- Test 7: Malformed entry — too few arguments --------------------------
492
493    #[test]
494    fn too_few_arguments_skipped_with_warning() {
495        // Only 2 arguments instead of the required 3.
496        let entries = vec![BootstrapMethodEntry {
497            method: metafactory_handle(),
498            arguments: vec![
499                BootstrapArgument::MethodType(Cow::Borrowed(
500                    "(Ljava/lang/Object;)Ljava/lang/Object;",
501                )),
502                BootstrapArgument::MethodType(Cow::Borrowed(
503                    "(Ljava/lang/String;)Ljava/lang/String;",
504                )),
505            ],
506        }];
507
508        let attrs = vec![AttributeInfo {
509            name: Cow::Borrowed("BootstrapMethods"),
510            data: AttributeData::BootstrapMethods(entries),
511        }];
512
513        let targets = extract_lambda_targets_from_attrs(&attrs);
514        assert!(targets.is_empty(), "Malformed entry should be skipped");
515    }
516
517    // -- Test 8: Wrong argument type at index 2 -------------------------------
518
519    #[test]
520    fn wrong_argument_type_at_index_2_skipped() {
521        // Argument index 2 is a MethodType instead of a MethodHandle.
522        let entries = vec![BootstrapMethodEntry {
523            method: metafactory_handle(),
524            arguments: vec![
525                BootstrapArgument::MethodType(Cow::Borrowed(
526                    "(Ljava/lang/Object;)Ljava/lang/Object;",
527                )),
528                BootstrapArgument::MethodType(Cow::Borrowed(
529                    "(Ljava/lang/String;)Ljava/lang/String;",
530                )),
531                BootstrapArgument::MethodType(Cow::Borrowed("()V")),
532            ],
533        }];
534
535        let attrs = vec![AttributeInfo {
536            name: Cow::Borrowed("BootstrapMethods"),
537            data: AttributeData::BootstrapMethods(entries),
538        }];
539
540        let targets = extract_lambda_targets_from_attrs(&attrs);
541        assert!(
542            targets.is_empty(),
543            "Wrong type at index 2 should be skipped"
544        );
545    }
546
547    // -- Test 9: Interface method reference -----------------------------------
548
549    #[test]
550    fn interface_method_reference() {
551        let entries = vec![BootstrapMethodEntry {
552            method: metafactory_handle(),
553            arguments: lambda_bootstrap_args(
554                CafeReferenceKind::InvokeInterface,
555                "java/util/List",
556                "size",
557                "()I",
558            ),
559        }];
560
561        let attrs = vec![AttributeInfo {
562            name: Cow::Borrowed("BootstrapMethods"),
563            data: AttributeData::BootstrapMethods(entries),
564        }];
565
566        let targets = extract_lambda_targets_from_attrs(&attrs);
567
568        assert_eq!(targets.len(), 1);
569        assert_eq!(targets[0].owner_fqn, "java.util.List");
570        assert_eq!(targets[0].method_name, "size");
571        assert_eq!(targets[0].reference_kind, ReferenceKind::InvokeInterface);
572    }
573
574    // -- Test 10: FQN conversion from internal format -------------------------
575
576    #[test]
577    fn fqn_conversion_internal_to_dotted() {
578        let entries = vec![BootstrapMethodEntry {
579            method: metafactory_handle(),
580            arguments: lambda_bootstrap_args(
581                CafeReferenceKind::InvokeStatic,
582                "com/example/deeply/nested/ServiceImpl",
583                "handle",
584                "(Ljava/lang/Object;)V",
585            ),
586        }];
587
588        let attrs = vec![AttributeInfo {
589            name: Cow::Borrowed("BootstrapMethods"),
590            data: AttributeData::BootstrapMethods(entries),
591        }];
592
593        let targets = extract_lambda_targets_from_attrs(&attrs);
594
595        assert_eq!(targets.len(), 1);
596        assert_eq!(
597            targets[0].owner_fqn,
598            "com.example.deeply.nested.ServiceImpl"
599        );
600    }
601
602    // -- Test helper: extract from attributes without a full ClassFile ---------
603
604    /// Helper that mirrors `extract_lambda_targets` but operates on a bare
605    /// attribute slice so tests don't need to construct a full `ClassFile`.
606    fn extract_lambda_targets_from_attrs(attrs: &[AttributeInfo<'_>]) -> Vec<LambdaTargetStub> {
607        let mut targets = Vec::new();
608        for attr in attrs {
609            if let AttributeData::BootstrapMethods(entries) = &attr.data {
610                for (idx, entry) in entries.iter().enumerate() {
611                    if !is_lambda_metafactory(&entry.method) {
612                        continue;
613                    }
614                    if let Some(stub) = extract_impl_handle(idx, &entry.arguments) {
615                        targets.push(stub);
616                    }
617                }
618            }
619        }
620        targets
621    }
622}