From aab66416c71bee57ef1a5637084e6b0dc725e50b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 9 Feb 2026 10:47:56 +0100 Subject: [PATCH 1/7] refactor(r8): Replace no-line resolution with base entry grouping Rewrite the no-line frame resolution logic in both mapper and cache paths to properly handle different types of base entries (0:0 range vs bare method vs single-line mappings). This replaces the simple NoLineSelection enum with richer grouping that considers has_minified_range and has_line_mapping flags to determine correct output lines. - Remove NoLineSelection enum and select_no_line_members from cache - Add pending_frames buffer to cache RemappedFrameIter for multi-frame base entry resolution - Add resolve_base_entries method to mapper for grouped base entry handling - Update iterate_with_lines in cache to use has_line_mapping for endline==0 output line determination - Remove unused resolve_no_line_output_line from utils Co-Authored-By: Claude Opus 4.6 --- src/cache/mod.rs | 315 +++++++++++++++++++++++++++++------------------ src/mapper.rs | 312 ++++++++++++++++++++++++++++++---------------- src/utils.rs | 16 --- 3 files changed, 402 insertions(+), 241 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index a8f38f2..693acc1 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -80,10 +80,7 @@ use thiserror::Error; use crate::builder::{RewriteAction, RewriteCondition, RewriteRule}; use crate::mapper::{format_cause, format_frames, format_throwable}; -use crate::utils::{ - class_name_to_descriptor, extract_class_name, resolve_no_line_output_line, - synthesize_source_file, -}; +use crate::utils::{class_name_to_descriptor, extract_class_name, synthesize_source_file}; use crate::{java, stacktrace, DeobfuscatedSignature, StackFrame, StackTrace, Throwable}; pub use raw::{ProguardCache, PRGCACHE_VERSION}; @@ -833,6 +830,8 @@ pub struct RemappedFrameIter<'r, 'data> { )>, /// A single remapped frame fallback (e.g. class-only remapping). fallback: Option>, + /// Buffered frames for multi-frame expansion (e.g. no-line groups). + pending_frames: Vec>, /// Number of frames to skip from rewrite rules. skip_count: usize, /// Whether there were mapping entries (for should_skip determination). @@ -848,6 +847,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { Self { inner: None, fallback: None, + pending_frames: Vec::new(), skip_count: 0, had_mappings: false, has_line_info: false, @@ -867,6 +867,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { Self { inner: Some((cache, frame, members)), fallback: None, + pending_frames: Vec::new(), skip_count, had_mappings, has_line_info, @@ -878,6 +879,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { Self { inner: None, fallback: Some(frame), + pending_frames: Vec::new(), skip_count: 0, had_mappings: false, has_line_info: false, @@ -894,6 +896,11 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { } fn next_inner(&mut self) -> Option> { + // Drain any buffered frames from multi-frame expansion first. + if !self.pending_frames.is_empty() { + return Some(self.pending_frames.remove(0)); + } + if let Some(frame) = self.fallback.take() { return Some(frame); } @@ -904,40 +911,18 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { // If we have no line number, treat it as unknown. If there are base (no-line) mappings // present, prefer those over line-mapped entries. if frame.line.unwrap_or(0) == 0 { - let selection = select_no_line_members(members.as_slice())?; - let mapped = match selection { - NoLineSelection::Single(member) => { - return map_member_without_lines( - cache, - &frame, - member, - self.outer_source_file, - ); - } - NoLineSelection::IterateBase => { - let mut mapped = None; - for member in members.by_ref() { - if member.endline().unwrap_or(0) == 0 { - mapped = map_member_without_lines( - cache, - &frame, - member, - self.outer_source_file, - ); - break; - } - } - mapped - } - NoLineSelection::IterateAll => iterate_without_lines( - cache, - &mut frame, - &mut members, - self.outer_source_file, - ), - }; - self.inner = Some((cache, frame, members)); - return mapped; + let mut frames = resolve_no_line_frames( + cache, + &frame, + members.as_slice(), + self.outer_source_file, + ); + if !frames.is_empty() { + let first = frames.remove(0); + self.pending_frames = frames; + return Some(first); + } + return None; } // With a concrete line number, skip base entries if there are line mappings. @@ -957,8 +942,6 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { mapped }; - // If we returned early for the unambiguous line==0 case above, `self.inner` remains `None` - // which ensures the iterator terminates. out } } @@ -985,29 +968,38 @@ fn iterate_with_lines<'a>( ) -> Option> { let frame_line = frame.line.unwrap_or(0); for member in members { - let member_endline = member.endline().unwrap_or(0) as usize; - let member_startline = member.startline().unwrap_or(0) as usize; // If this method has line mappings, skip base (no-line) entries when we have a concrete line. - if has_line_info && frame_line > 0 && member_endline == 0 { + if has_line_info && frame_line > 0 && member.endline().unwrap_or(0) == 0 { continue; } - // If the mapping entry has no line range, preserve the input line number (if any). - if member_endline == 0 { - return map_member_without_lines(cache, frame, member, outer_source_file); + // If the mapping entry has no line range, determine output line. + if member.endline().unwrap_or(0) == 0 { + let output_line = if member.original_startline().is_none() { + // Bare method mapping: pass through frame line. + frame.line + } else if member.original_startline().unwrap_or(0) > 0 { + Some(member.original_startline().unwrap_or(0) as usize) + } else { + None + }; + return map_member_without_lines(cache, frame, member, outer_source_file, output_line); } // skip any members which do not match our frames line - if member_endline > 0 && (frame_line < member_startline || frame_line > member_endline) { + if member.endline().unwrap_or(0) > 0 + && (frame_line < member.startline().unwrap_or(0) as usize + || frame_line > member.endline().unwrap_or(0) as usize) + { continue; } - let original_startline = member.original_startline().unwrap_or(0) as usize; // parents of inlined frames don't have an `endline`, and // the top inlined frame need to be correctly offset. let line = if member.original_endline == u32::MAX - || member.original_endline as usize == original_startline + || member.original_endline == member.original_startline().unwrap_or(0) as u32 { - original_startline + member.original_startline().unwrap_or(0) as usize } else { - original_startline + frame_line - member_startline + member.original_startline().unwrap_or(0) as usize + frame_line + - member.startline().unwrap_or(0) as usize }; let class = cache @@ -1045,52 +1037,12 @@ fn iterate_with_lines<'a>( None } -/// Selection strategy for line==0 frames. -/// -/// When line info is missing, we prefer base (no-line) mappings if they exist. -/// If all candidates resolve to the same original method, we treat it as -/// unambiguous and return a single mapping. Otherwise we iterate either over -/// base mappings (when present) or all mappings (when only line-mapped entries exist). -enum NoLineSelection<'a> { - Single(&'a raw::Member), - IterateBase, - IterateAll, -} - -fn select_no_line_members<'a>(members: &'a [raw::Member]) -> Option> { - // Prefer base entries (endline == 0) if present. - let mut base_members = members.iter().filter(|m| m.endline().unwrap_or(0) == 0); - if let Some(first_base) = base_members.next() { - let all_same = base_members.all(|m| { - m.original_class_offset == first_base.original_class_offset - && m.original_name_offset == first_base.original_name_offset - }); - - return Some(if all_same { - NoLineSelection::Single(first_base) - } else { - NoLineSelection::IterateBase - }); - } - - let first = members.first()?; - let unambiguous = members.iter().all(|m| { - m.original_class_offset == first.original_class_offset - && m.original_name_offset == first.original_name_offset - }); - - Some(if unambiguous { - NoLineSelection::Single(first) - } else { - NoLineSelection::IterateAll - }) -} - fn map_member_without_lines<'a>( cache: &ProguardCache<'a>, frame: &StackFrame<'a>, member: &raw::Member, outer_source_file: Option<&str>, + output_line: Option, ) -> Option> { let class = cache .read_string(member.original_class_offset) @@ -1098,23 +1050,24 @@ fn map_member_without_lines<'a>( let method = cache.read_string(member.original_name_offset).ok()?; let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned); - let original_startline = member.original_startline().map(|v| v as usize); - Some(StackFrame { class, method, file, - line: Some(resolve_no_line_output_line( - frame.line.unwrap_or(0), - original_startline, - member.startline().map(|v| v as usize), - member.endline().map(|v| v as usize), - )), + line: output_line, parameters: frame.parameters, method_synthesized: member.is_synthesized(), }) } +/// Computes the default output line for a cache member's original_startline. +fn cache_member_output_line(member: &raw::Member) -> Option { + member + .original_startline() + .filter(|&v| v > 0) + .map(|v| v as usize) +} + fn iterate_without_lines<'a>( cache: &ProguardCache<'a>, frame: &mut StackFrame<'a>, @@ -1122,31 +1075,153 @@ fn iterate_without_lines<'a>( outer_source_file: Option<&str>, ) -> Option> { let member = members.next()?; + let output_line = cache_member_output_line(member); + map_member_without_lines(cache, frame, member, outer_source_file, output_line) +} - let class = cache - .read_string(member.original_class_offset) - .unwrap_or(frame.class); +/// Resolves frames for the no-line (frame_line==0) case. +/// +/// When the input frame has no line number, base entries (endline==0) are preferred +/// over line-mapped entries. Base entries are split into two groups by +/// `has_minified_range` and each group's output lines are computed based on +/// whether the group contains range mappings, single-line mappings, or bare methods. +fn resolve_no_line_frames<'a>( + cache: &ProguardCache<'a>, + frame: &StackFrame<'a>, + members: &[raw::Member], + outer_source_file: Option<&str>, +) -> Vec> { + let base_entries: Vec<&raw::Member> = members + .iter() + .filter(|m| m.endline().unwrap_or(0) == 0) + .collect(); + + if !base_entries.is_empty() { + return resolve_base_entries(cache, frame, &base_entries, outer_source_file); + } - let method = cache.read_string(member.original_name_offset).ok()?; + // No base entries — fall back to all entries with output line 0. + let mut frames = Vec::new(); + if let Some(first) = members.first() { + let all_same = members.iter().all(|m| { + m.original_class_offset == first.original_class_offset + && m.original_name_offset == first.original_name_offset + }); + if all_same { + if let Some(f) = + map_member_without_lines(cache, frame, first, outer_source_file, Some(0)) + { + frames.push(f); + } + } else { + for member in members { + if let Some(f) = + map_member_without_lines(cache, frame, member, outer_source_file, Some(0)) + { + frames.push(f); + } + } + } + } + frames +} - // Synthesize from class name (input filename is not reliable) - let file = synthesize_source_file(class, outer_source_file).map(Cow::Owned); +/// Resolves output lines for base (endline==0) entries when the frame has no line number. +/// +/// Entries are split by `has_minified_range`: +/// - **0:0 entries** (`has_minified_range=true`): if any entry has a range +/// (`original_endline != original_startline`), all emit `Some(0)`; +/// otherwise each emits its `original_startline`. +/// - **No-range entries** (`has_minified_range=false`): a single entry uses +/// its `original_startline` (or `Some(0)` for bare methods); multiple entries +/// with the same name collapse to one frame with `Some(0)`; different names +/// each emit `Some(0)` in original order. +fn resolve_base_entries<'a>( + cache: &ProguardCache<'a>, + frame: &StackFrame<'a>, + base_entries: &[&raw::Member], + outer_source_file: Option<&str>, +) -> Vec> { + let zero_zero: Vec<&&raw::Member> = base_entries + .iter() + .filter(|m| m.startline().is_some()) + .collect(); + let no_range: Vec<&&raw::Member> = base_entries + .iter() + .filter(|m| m.startline().is_none()) + .collect(); + + let mut frames = Vec::new(); + + // Process 0:0 entries. + if !zero_zero.is_empty() { + let any_has_range = zero_zero.iter().any(|m| { + m.original_endline != u32::MAX + && m.original_endline != m.original_startline().unwrap_or(0) as u32 + }); + if any_has_range { + for member in &zero_zero { + if let Some(f) = + map_member_without_lines(cache, frame, member, outer_source_file, Some(0)) + { + frames.push(f); + } + } + } else { + for member in &zero_zero { + let line = cache_member_output_line(member); + if let Some(f) = + map_member_without_lines(cache, frame, member, outer_source_file, line) + { + frames.push(f); + } + } + } + } - let original_startline = member.original_startline().map(|v| v as usize); + // Process no-range entries. + if !no_range.is_empty() { + if no_range.len() == 1 { + let member = no_range[0]; + let line = if member.original_startline().unwrap_or(0) > 0 { + Some(member.original_startline().unwrap_or(0) as usize) + } else if member.original_startline().is_none() { + Some(0) + } else { + None + }; + if let Some(f) = + map_member_without_lines(cache, frame, member, outer_source_file, line) + { + frames.push(f); + } + } else { + let all_same_name = no_range + .iter() + .all(|m| m.original_name_offset == no_range[0].original_name_offset); + if all_same_name { + if let Some(f) = map_member_without_lines( + cache, + frame, + no_range[0], + outer_source_file, + Some(0), + ) { + frames.push(f); + } + } else { + for member in &no_range { + if let Some(f) = + map_member_without_lines(cache, frame, member, outer_source_file, Some(0)) + { + frames.push(f); + } + } + } + } + } - Some(StackFrame { - class, - method, - file, - line: Some(resolve_no_line_output_line( - frame.line.unwrap_or(0), - original_startline, - member.startline().map(|v| v as usize), - member.endline().map(|v| v as usize), - )), - parameters: frame.parameters, - method_synthesized: member.is_synthesized(), - }) + frames } /// Computes the number of frames to skip based on rewrite rules. diff --git a/src/mapper.rs b/src/mapper.rs index 34673ce..f1ccd92 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -10,10 +10,7 @@ use crate::builder::{ use crate::java; use crate::mapping::ProguardMapping; use crate::stacktrace::{self, StackFrame, StackTrace, Throwable}; -use crate::utils::{ - class_name_to_descriptor, extract_class_name, resolve_no_line_output_line, - synthesize_source_file, -}; +use crate::utils::{class_name_to_descriptor, extract_class_name, synthesize_source_file}; /// A deobfuscated method signature. pub struct DeobfuscatedSignature { @@ -141,21 +138,20 @@ fn map_member_with_lines<'a>( member: &MemberMapping<'a>, ) -> Option> { let frame_line = frame.line.unwrap_or(0); - let member_endline = member.endline.unwrap_or(0); - let member_startline = member.startline.unwrap_or(0); - if member_endline > 0 && (frame_line < member_startline || frame_line > member_endline) { + if member.endline.unwrap_or(0) > 0 + && (frame_line < member.startline.unwrap_or(0) || frame_line > member.endline.unwrap_or(0)) + { return None; } - let original_startline = member.original_startline.unwrap_or(0); // parents of inlined frames don't have an `endline`, and // the top inlined frame need to be correctly offset. let line = if member.original_endline.is_none() - || member.original_endline == Some(original_startline) + || member.original_endline == member.original_startline { - original_startline + member.original_startline.unwrap_or(0) } else { - original_startline + frame_line - member_startline + member.original_startline.unwrap_or(0) + frame_line - member.startline.unwrap_or(0) }; let class = member.original_class.unwrap_or(frame.class); @@ -182,26 +178,23 @@ fn map_member_with_lines<'a>( }) } +/// Builds a remapped frame from a no-line (base) mapping entry. +/// +/// `output_line` is the line number to use in the output frame, already computed +/// by the caller based on group context (has_minified_range, entry count, etc.). fn map_member_without_lines<'a>( frame: &StackFrame<'a>, member: &MemberMapping<'a>, + output_line: Option, ) -> StackFrame<'a> { let class = member.original_class.unwrap_or(frame.class); // Synthesize from class name (input filename is not reliable) let file = synthesize_source_file(class, member.outer_source_file).map(Cow::Owned); - let line = resolve_no_line_output_line( - frame.line.unwrap_or(0), - member.original_startline, - member.startline, - member.endline, - ); StackFrame { class, method: member.original, file, - // Preserve input line if present (e.g. "Unknown Source:7") when the mapping itself - // has no line information. This matches R8 retrace behavior. - line: Some(line), + line: output_line, parameters: frame.parameters, method_synthesized: member.is_synthesized, } @@ -219,46 +212,6 @@ fn remap_class_only<'a>(frame: &StackFrame<'a>, reference_file: Option<&str>) -> } } -/// Selection strategy for line==0 frames. -/// -/// When line info is missing, we prefer base (no-line) mappings if they exist. -/// If all candidates resolve to the same original method, we treat it as -/// unambiguous and return a single mapping. Otherwise we iterate either over -/// base mappings (when present) or all mappings (when only line-mapped entries exist). -enum NoLineSelection<'a> { - Single(&'a MemberMapping<'a>), - IterateAll, - IterateBase, -} - -fn select_no_line_members<'a>( - mapping_entries: &'a [MemberMapping<'a>], - has_line_info: bool, -) -> Option> { - let mut base_members = mapping_entries - .iter() - .filter(|m| m.endline.unwrap_or(0) == 0); - if has_line_info { - if let Some(first_base) = base_members.next() { - let all_same = base_members.all(|m| m.original == first_base.original); - return Some(if all_same { - NoLineSelection::Single(first_base) - } else { - NoLineSelection::IterateBase - }); - } - } - - let first = mapping_entries.first()?; - let unambiguous = mapping_entries.iter().all(|m| m.original == first.original); - - Some(if unambiguous { - NoLineSelection::Single(first) - } else { - NoLineSelection::IterateAll - }) -} - fn apply_rewrite_rules<'s>(collected: &mut CollectedFrames<'s>, thrown_descriptor: Option<&str>) { if collected.frames.is_empty() { return; @@ -298,14 +251,21 @@ fn iterate_with_lines<'a>( ) -> Option> { let frame_line = frame.line.unwrap_or(0); for member in members { - let member_endline = member.endline.unwrap_or(0); // If this method has line mappings, skip base (no-line) entries when we have a concrete line. - if has_line_info && frame_line > 0 && member_endline == 0 { + if has_line_info && frame_line > 0 && member.endline.unwrap_or(0) == 0 { continue; } - // If the mapping entry has no line range, preserve the input line number (if any). - if member_endline == 0 { - return Some(map_member_without_lines(frame, member)); + // If the mapping entry has no line range, determine output line. + if member.endline.unwrap_or(0) == 0 { + let output_line = if member.original_startline.is_none() { + // Bare method mapping: pass through frame line. + frame.line + } else if member.original_startline.unwrap_or(0) > 0 { + member.original_startline + } else { + None + }; + return Some(map_member_without_lines(frame, member, output_line)); } if let Some(mapped) = map_member_with_lines(frame, member) { return Some(mapped); @@ -318,13 +278,157 @@ fn iterate_without_lines<'a>( frame: &mut StackFrame<'a>, members: &mut core::slice::Iter<'_, MemberMapping<'a>>, ) -> Option> { - members - .next() - .map(|member| map_member_without_lines(frame, member)) + members.next().map(|member| { + let output_line = if member.original_startline.unwrap_or(0) > 0 { + member.original_startline + } else { + None + }; + map_member_without_lines(frame, member, output_line) + }) } impl FusedIterator for RemappedFrameIter<'_> {} +/// Resolves frames for the no-line (frame_line==0) case. +/// +/// When the input frame has no line number, base entries (endline==0) are preferred +/// over line-mapped entries. Base entries are split into two groups by +/// `has_minified_range` and each group's output lines are computed based on +/// whether the group contains range mappings, single-line mappings, or bare methods. +fn resolve_no_line_frames<'s>( + frame: &StackFrame<'s>, + mapping_entries: &'s [MemberMapping<'s>], + base_entries: &[&'s MemberMapping<'s>], + collected: &mut CollectedFrames<'s>, +) { + if !base_entries.is_empty() { + resolve_base_entries(frame, base_entries, collected); + return; + } + + // No base entries — fall back to all entries with output line 0. + if mapping_entries.is_empty() { + return; + } + let first = &mapping_entries[0]; + let unambiguous = mapping_entries.iter().all(|m| m.original == first.original); + if unambiguous { + collected + .frames + .push(map_member_without_lines(frame, first, Some(0))); + collected + .rewrite_rules + .extend(first.rewrite_rules.iter()); + } else { + for member in mapping_entries { + collected + .frames + .push(map_member_without_lines(frame, member, Some(0))); + collected + .rewrite_rules + .extend(member.rewrite_rules.iter()); + } + } +} + +/// Resolves output lines for base (endline==0) entries when the frame has no line number. +/// +/// Entries are split by `has_minified_range`: +/// - **0:0 entries** (`has_minified_range=true`): if any entry has a range +/// (`original_endline != original_startline`), all emit `Some(0)`; +/// otherwise each emits its `original_startline`. +/// - **No-range entries** (`has_minified_range=false`): a single entry uses +/// its `original_startline` (or `Some(0)` for bare methods); multiple entries +/// with the same name collapse to one frame with `Some(0)`; different names +/// each emit `Some(0)` in original order. +fn resolve_base_entries<'s>( + frame: &StackFrame<'s>, + base_entries: &[&'s MemberMapping<'s>], + collected: &mut CollectedFrames<'s>, +) { + let zero_zero: Vec<&&MemberMapping<'s>> = base_entries + .iter() + .filter(|m| m.startline.is_some()) + .collect(); + let no_range: Vec<&&MemberMapping<'s>> = base_entries + .iter() + .filter(|m| m.startline.is_none()) + .collect(); + + // Process 0:0 entries. + if !zero_zero.is_empty() { + let any_has_range = zero_zero.iter().any(|m| { + m.original_endline.is_some() && m.original_endline != m.original_startline + }); + if any_has_range { + for member in &zero_zero { + collected + .frames + .push(map_member_without_lines(frame, member, Some(0))); + collected + .rewrite_rules + .extend(member.rewrite_rules.iter()); + } + } else { + for member in &zero_zero { + let line = if member.original_startline.unwrap_or(0) > 0 { + member.original_startline + } else { + None + }; + collected + .frames + .push(map_member_without_lines(frame, member, line)); + collected + .rewrite_rules + .extend(member.rewrite_rules.iter()); + } + } + } + + // Process no-range entries. + if !no_range.is_empty() { + if no_range.len() == 1 { + let member = no_range[0]; + let line = if member.original_startline.unwrap_or(0) > 0 { + member.original_startline + } else if member.original_startline.is_none() { + Some(0) + } else { + None + }; + collected + .frames + .push(map_member_without_lines(frame, member, line)); + collected + .rewrite_rules + .extend(member.rewrite_rules.iter()); + } else { + let all_same_name = no_range + .iter() + .all(|m| m.original == no_range[0].original); + if all_same_name { + collected + .frames + .push(map_member_without_lines(frame, no_range[0], Some(0))); + collected + .rewrite_rules + .extend(no_range[0].rewrite_rules.iter()); + } else { + for member in &no_range { + collected + .frames + .push(map_member_without_lines(frame, member, Some(0))); + collected + .rewrite_rules + .extend(member.rewrite_rules.iter()); + } + } + } + } +} + /// A Proguard Remapper. /// /// This can remap class names, stack frames one at a time, or the complete @@ -586,50 +690,45 @@ impl<'s> ProguardMapper<'s> { if frame.parameters.is_none() { let has_line_info = mapping_entries.iter().any(|m| m.endline.unwrap_or(0) > 0); + let frame_line = frame.line.unwrap_or(0); + + // Base entries are those with endline == 0 (no minified range or 0:0 range). + let base_entries: Vec<&MemberMapping<'s>> = mapping_entries + .iter() + .filter(|m| m.endline.unwrap_or(0) == 0) + .collect(); // If the stacktrace has no line number, treat it as unknown and remap without // applying line filters. If there are base (no-line) mappings present, prefer those. - if frame.line.unwrap_or(0) == 0 { - let selection = select_no_line_members(mapping_entries, has_line_info); - match selection { - Some(NoLineSelection::Single(member)) => { - collected - .frames - .push(map_member_without_lines(&frame, member)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } - Some(NoLineSelection::IterateAll) => { - for member in mapping_entries { - collected - .frames - .push(map_member_without_lines(&frame, member)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } - } - Some(NoLineSelection::IterateBase) => { - for member in mapping_entries - .iter() - .filter(|m| m.endline.unwrap_or(0) == 0) - { - collected - .frames - .push(map_member_without_lines(&frame, member)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } - } - None => return collected, - } + if frame_line == 0 { + resolve_no_line_frames(&frame, mapping_entries, &base_entries, &mut collected); return collected; } + // Frame has a line number > 0. for member in mapping_entries { - if has_line_info && member.endline.unwrap_or(0) == 0 { + if has_line_info && frame_line > 0 && member.endline.unwrap_or(0) == 0 { continue; } if member.endline.unwrap_or(0) == 0 { + // No-range entry with frame_line > 0. + if member.original_startline.is_none() { + // Bare method mapping (no line info) — pass through frame line. + collected + .frames + .push(map_member_without_lines(&frame, member, frame.line)); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); + continue; + } + // Single-line: use original_startline if > 0, else None. + let output_line = if member.original_startline.unwrap_or(0) > 0 { + member.original_startline + } else { + None + }; collected .frames - .push(map_member_without_lines(&frame, member)); + .push(map_member_without_lines(&frame, member, output_line)); collected.rewrite_rules.extend(member.rewrite_rules.iter()); } else if let Some(mapped) = map_member_with_lines(&frame, member) { collected.frames.push(mapped); @@ -638,7 +737,13 @@ impl<'s> ProguardMapper<'s> { } } else { for member in mapping_entries { - let mapped = map_member_without_lines(&frame, member); + // For parameter-based lookups, use original_startline if > 0, else None + let output_line = if member.original_startline.unwrap_or(0) > 0 { + member.original_startline + } else { + None + }; + let mapped = map_member_without_lines(&frame, member, output_line); collected.frames.push(mapped); collected.rewrite_rules.extend(member.rewrite_rules.iter()); } @@ -700,10 +805,7 @@ impl<'s> ProguardMapper<'s> { members.all_mappings.iter() }; - let has_line_info = members - .all_mappings - .iter() - .any(|m| m.endline.unwrap_or(0) > 0); + let has_line_info = members.all_mappings.iter().any(|m| m.endline.unwrap_or(0) > 0); RemappedFrameIter::members(frame, mappings, has_line_info) } diff --git a/src/utils.rs b/src/utils.rs index a7db5b7..94a5a1a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,21 +1,5 @@ //! Internal helpers shared across modules. -/// For no-line mappings, prefer the original line if the mapping has no minified range. -pub(crate) fn resolve_no_line_output_line( - frame_line: usize, - original_startline: Option, - startline: Option, - endline: Option, -) -> usize { - if frame_line > 0 { - frame_line - } else if startline.unwrap_or(0) == 0 && endline.unwrap_or(0) == 0 { - original_startline.unwrap_or(0) - } else { - 0 - } -} - pub(crate) fn extract_class_name(full_path: &str) -> Option<&str> { let after_last_period = full_path.split('.').next_back()?; // If the class is an inner class, we need to extract the outer class name From fa77332f9e410f95f6b428d54f11de24f20c4457 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 9 Feb 2026 11:25:58 +0100 Subject: [PATCH 2/7] formatting --- src/cache/mod.rs | 10 +++------- src/mapper.rs | 28 +++++++--------------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 693acc1..4d93f03 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1200,13 +1200,9 @@ fn resolve_base_entries<'a>( .iter() .all(|m| m.original_name_offset == no_range[0].original_name_offset); if all_same_name { - if let Some(f) = map_member_without_lines( - cache, - frame, - no_range[0], - outer_source_file, - Some(0), - ) { + if let Some(f) = + map_member_without_lines(cache, frame, no_range[0], outer_source_file, Some(0)) + { frames.push(f); } } else { diff --git a/src/mapper.rs b/src/mapper.rs index f1ccd92..8cc4a4e 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -317,17 +317,13 @@ fn resolve_no_line_frames<'s>( collected .frames .push(map_member_without_lines(frame, first, Some(0))); - collected - .rewrite_rules - .extend(first.rewrite_rules.iter()); + collected.rewrite_rules.extend(first.rewrite_rules.iter()); } else { for member in mapping_entries { collected .frames .push(map_member_without_lines(frame, member, Some(0))); - collected - .rewrite_rules - .extend(member.rewrite_rules.iter()); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); } } } @@ -366,9 +362,7 @@ fn resolve_base_entries<'s>( collected .frames .push(map_member_without_lines(frame, member, Some(0))); - collected - .rewrite_rules - .extend(member.rewrite_rules.iter()); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); } } else { for member in &zero_zero { @@ -380,9 +374,7 @@ fn resolve_base_entries<'s>( collected .frames .push(map_member_without_lines(frame, member, line)); - collected - .rewrite_rules - .extend(member.rewrite_rules.iter()); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); } } } @@ -401,13 +393,9 @@ fn resolve_base_entries<'s>( collected .frames .push(map_member_without_lines(frame, member, line)); - collected - .rewrite_rules - .extend(member.rewrite_rules.iter()); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); } else { - let all_same_name = no_range - .iter() - .all(|m| m.original == no_range[0].original); + let all_same_name = no_range.iter().all(|m| m.original == no_range[0].original); if all_same_name { collected .frames @@ -420,9 +408,7 @@ fn resolve_base_entries<'s>( collected .frames .push(map_member_without_lines(frame, member, Some(0))); - collected - .rewrite_rules - .extend(member.rewrite_rules.iter()); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); } } } From 70ab753e28e461a478c0d71f70032f0a65aaca2e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 9 Feb 2026 16:39:31 +0100 Subject: [PATCH 3/7] refactor: Simplify resolve_base_entries in mapper and cache - Rename cache_member_output_line to compute_member_output_line - Collapse any_has_range if-else into single loop with conditional line - Collapse no_range len==1 / all_same_name into single all_same_name check - Remove separate zero_zero/no_range vectors; pre-compute aggregates in a single pass and use one emission loop over base_entries Co-Authored-By: Claude Opus 4.6 --- src/cache/mod.rs | 99 +++++++++++++++++++++--------------------------- src/mapper.rs | 92 ++++++++++++++++++++------------------------ 2 files changed, 84 insertions(+), 107 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 4d93f03..1779968 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1061,7 +1061,7 @@ fn map_member_without_lines<'a>( } /// Computes the default output line for a cache member's original_startline. -fn cache_member_output_line(member: &raw::Member) -> Option { +fn compute_member_output_line(member: &raw::Member) -> Option { member .original_startline() .filter(|&v| v > 0) @@ -1075,7 +1075,7 @@ fn iterate_without_lines<'a>( outer_source_file: Option<&str>, ) -> Option> { let member = members.next()?; - let output_line = cache_member_output_line(member); + let output_line = compute_member_output_line(member); map_member_without_lines(cache, frame, member, outer_source_file, output_line) } @@ -1142,78 +1142,65 @@ fn resolve_base_entries<'a>( base_entries: &[&raw::Member], outer_source_file: Option<&str>, ) -> Vec> { - let zero_zero: Vec<&&raw::Member> = base_entries - .iter() - .filter(|m| m.startline().is_some()) - .collect(); - let no_range: Vec<&&raw::Member> = base_entries - .iter() - .filter(|m| m.startline().is_none()) - .collect(); - - let mut frames = Vec::new(); - - // Process 0:0 entries. - if !zero_zero.is_empty() { - let any_has_range = zero_zero.iter().any(|m| { - m.original_endline != u32::MAX - && m.original_endline != m.original_startline().unwrap_or(0) as u32 - }); - if any_has_range { - for member in &zero_zero { - if let Some(f) = - map_member_without_lines(cache, frame, member, outer_source_file, Some(0)) - { - frames.push(f); - } + // Pre-compute aggregates in a single pass. + let mut any_zero_zero_has_range = false; + let mut no_range_count = 0usize; + let mut first_no_range_offset: Option = None; + let mut all_no_range_same_name = true; + for member in base_entries { + if member.startline().is_some() { + if member.original_endline != u32::MAX + && member.original_endline != member.original_startline().unwrap_or(0) as u32 + { + any_zero_zero_has_range = true; } } else { - for member in &zero_zero { - let line = cache_member_output_line(member); - if let Some(f) = - map_member_without_lines(cache, frame, member, outer_source_file, line) - { - frames.push(f); + no_range_count += 1; + match first_no_range_offset { + None => first_no_range_offset = Some(member.original_name_offset), + Some(first) if member.original_name_offset != first => { + all_no_range_same_name = false } + _ => {} } } } - // Process no-range entries. - if !no_range.is_empty() { - if no_range.len() == 1 { - let member = no_range[0]; - let line = if member.original_startline().unwrap_or(0) > 0 { - Some(member.original_startline().unwrap_or(0) as usize) - } else if member.original_startline().is_none() { + let mut frames = Vec::new(); + let mut no_range_emitted = false; + for member in base_entries { + if member.startline().is_some() { + let line = if any_zero_zero_has_range { Some(0) } else { - None + compute_member_output_line(member) }; - if let Some(f) = - map_member_without_lines(cache, frame, member, outer_source_file, line) + if let Some(f) = map_member_without_lines(cache, frame, member, outer_source_file, line) { frames.push(f); } - } else { - let all_same_name = no_range - .iter() - .all(|m| m.original_name_offset == no_range[0].original_name_offset); - if all_same_name { + } else if all_no_range_same_name { + if !no_range_emitted { + no_range_emitted = true; + let line = if no_range_count > 1 { + Some(0) + } else { + compute_member_output_line(member).or(if member.original_startline().is_none() { + Some(0) + } else { + None + }) + }; if let Some(f) = - map_member_without_lines(cache, frame, no_range[0], outer_source_file, Some(0)) + map_member_without_lines(cache, frame, member, outer_source_file, line) { frames.push(f); } - } else { - for member in &no_range { - if let Some(f) = - map_member_without_lines(cache, frame, member, outer_source_file, Some(0)) - { - frames.push(f); - } - } } + } else if let Some(f) = + map_member_without_lines(cache, frame, member, outer_source_file, Some(0)) + { + frames.push(f); } } diff --git a/src/mapper.rs b/src/mapper.rs index 8cc4a4e..ecbf3c3 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -343,50 +343,35 @@ fn resolve_base_entries<'s>( base_entries: &[&'s MemberMapping<'s>], collected: &mut CollectedFrames<'s>, ) { - let zero_zero: Vec<&&MemberMapping<'s>> = base_entries - .iter() - .filter(|m| m.startline.is_some()) - .collect(); - let no_range: Vec<&&MemberMapping<'s>> = base_entries - .iter() - .filter(|m| m.startline.is_none()) - .collect(); - - // Process 0:0 entries. - if !zero_zero.is_empty() { - let any_has_range = zero_zero.iter().any(|m| { - m.original_endline.is_some() && m.original_endline != m.original_startline - }); - if any_has_range { - for member in &zero_zero { - collected - .frames - .push(map_member_without_lines(frame, member, Some(0))); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); + // Pre-compute aggregates in a single pass. + let mut any_zero_zero_has_range = false; + let mut no_range_count = 0usize; + let mut first_no_range_name: Option<&str> = None; + let mut all_no_range_same_name = true; + for member in base_entries { + if member.startline.is_some() { + if member.original_endline.is_some() + && member.original_endline != member.original_startline + { + any_zero_zero_has_range = true; } } else { - for member in &zero_zero { - let line = if member.original_startline.unwrap_or(0) > 0 { - member.original_startline - } else { - None - }; - collected - .frames - .push(map_member_without_lines(frame, member, line)); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); + no_range_count += 1; + match first_no_range_name { + None => first_no_range_name = Some(member.original), + Some(first) if member.original != first => all_no_range_same_name = false, + _ => {} } } } - // Process no-range entries. - if !no_range.is_empty() { - if no_range.len() == 1 { - let member = no_range[0]; - let line = if member.original_startline.unwrap_or(0) > 0 { - member.original_startline - } else if member.original_startline.is_none() { + let mut no_range_emitted = false; + for member in base_entries { + if member.startline.is_some() { + let line = if any_zero_zero_has_range { Some(0) + } else if member.original_startline.unwrap_or(0) > 0 { + member.original_startline } else { None }; @@ -394,23 +379,28 @@ fn resolve_base_entries<'s>( .frames .push(map_member_without_lines(frame, member, line)); collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } else { - let all_same_name = no_range.iter().all(|m| m.original == no_range[0].original); - if all_same_name { + } else if all_no_range_same_name { + if !no_range_emitted { + no_range_emitted = true; + let line = if no_range_count > 1 { + Some(0) + } else if member.original_startline.unwrap_or(0) > 0 { + member.original_startline + } else if member.original_startline.is_none() { + Some(0) + } else { + None + }; collected .frames - .push(map_member_without_lines(frame, no_range[0], Some(0))); - collected - .rewrite_rules - .extend(no_range[0].rewrite_rules.iter()); - } else { - for member in &no_range { - collected - .frames - .push(map_member_without_lines(frame, member, Some(0))); - collected.rewrite_rules.extend(member.rewrite_rules.iter()); - } + .push(map_member_without_lines(frame, member, line)); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); } + } else { + collected + .frames + .push(map_member_without_lines(frame, member, Some(0))); + collected.rewrite_rules.extend(member.rewrite_rules.iter()); } } } From 9dd106eb1a19c8ebf33122717a7ae84f1ea84f07 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 10 Feb 2026 18:24:45 +0100 Subject: [PATCH 4/7] Fix unnecessary casts and formatting Co-Authored-By: Claude Opus 4.6 --- src/cache/mod.rs | 16 +++++++++------- src/mapper.rs | 5 ++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 1779968..84fa0c4 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -994,7 +994,7 @@ fn iterate_with_lines<'a>( // parents of inlined frames don't have an `endline`, and // the top inlined frame need to be correctly offset. let line = if member.original_endline == u32::MAX - || member.original_endline == member.original_startline().unwrap_or(0) as u32 + || member.original_endline == member.original_startline().unwrap_or(0) { member.original_startline().unwrap_or(0) as usize } else { @@ -1150,7 +1150,7 @@ fn resolve_base_entries<'a>( for member in base_entries { if member.startline().is_some() { if member.original_endline != u32::MAX - && member.original_endline != member.original_startline().unwrap_or(0) as u32 + && member.original_endline != member.original_startline().unwrap_or(0) { any_zero_zero_has_range = true; } @@ -1185,11 +1185,13 @@ fn resolve_base_entries<'a>( let line = if no_range_count > 1 { Some(0) } else { - compute_member_output_line(member).or(if member.original_startline().is_none() { - Some(0) - } else { - None - }) + compute_member_output_line(member).or( + if member.original_startline().is_none() { + Some(0) + } else { + None + }, + ) }; if let Some(f) = map_member_without_lines(cache, frame, member, outer_source_file, line) diff --git a/src/mapper.rs b/src/mapper.rs index ecbf3c3..df3945c 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -781,7 +781,10 @@ impl<'s> ProguardMapper<'s> { members.all_mappings.iter() }; - let has_line_info = members.all_mappings.iter().any(|m| m.endline.unwrap_or(0) > 0); + let has_line_info = members + .all_mappings + .iter() + .any(|m| m.endline.unwrap_or(0) > 0); RemappedFrameIter::members(frame, mappings, has_line_info) } From defa8d8f6d2acc1a1030620dc38abc3064c2482b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 10 Feb 2026 18:29:42 +0100 Subject: [PATCH 5/7] Update doc comments to reflect Option fields instead of boolean flags Co-Authored-By: Claude Opus 4.6 --- src/cache/mod.rs | 18 ++++++++++++------ src/mapper.rs | 20 +++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 84fa0c4..a702224 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1082,9 +1082,10 @@ fn iterate_without_lines<'a>( /// Resolves frames for the no-line (frame_line==0) case. /// /// When the input frame has no line number, base entries (endline==0) are preferred -/// over line-mapped entries. Base entries are split into two groups by -/// `has_minified_range` and each group's output lines are computed based on -/// whether the group contains range mappings, single-line mappings, or bare methods. +/// over line-mapped entries. Base entries are split into two groups by whether +/// `startline` is present (0:0 entries) or absent (no-range entries), and each +/// group's output lines are computed based on whether the group contains range +/// mappings, single-line mappings, or bare methods. fn resolve_no_line_frames<'a>( cache: &ProguardCache<'a>, frame: &StackFrame<'a>, @@ -1128,11 +1129,11 @@ fn resolve_no_line_frames<'a>( /// Resolves output lines for base (endline==0) entries when the frame has no line number. /// -/// Entries are split by `has_minified_range`: -/// - **0:0 entries** (`has_minified_range=true`): if any entry has a range +/// Entries are split by whether `startline` is present: +/// - **0:0 entries** (`startline().is_some()`): if any entry has a range /// (`original_endline != original_startline`), all emit `Some(0)`; /// otherwise each emits its `original_startline`. -/// - **No-range entries** (`has_minified_range=false`): a single entry uses +/// - **No-range entries** (`startline().is_none()`): a single entry uses /// its `original_startline` (or `Some(0)` for bare methods); multiple entries /// with the same name collapse to one frame with `Some(0)`; different names /// each emit `Some(0)` in original order. @@ -1143,9 +1144,13 @@ fn resolve_base_entries<'a>( outer_source_file: Option<&str>, ) -> Vec> { // Pre-compute aggregates in a single pass. + // Whether any 0:0 entry has a multi-line original range (original_endline != original_startline). let mut any_zero_zero_has_range = false; + // Number of no-range (startline().is_none()) entries. let mut no_range_count = 0usize; + // original_name_offset of the first no-range entry, used to detect ambiguity. let mut first_no_range_offset: Option = None; + // Whether all no-range entries map to the same original method name. let mut all_no_range_same_name = true; for member in base_entries { if member.startline().is_some() { @@ -1167,6 +1172,7 @@ fn resolve_base_entries<'a>( } let mut frames = Vec::new(); + // Whether a no-range entry has already been emitted (used to collapse duplicates). let mut no_range_emitted = false; for member in base_entries { if member.startline().is_some() { diff --git a/src/mapper.rs b/src/mapper.rs index df3945c..330f3f8 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -181,7 +181,7 @@ fn map_member_with_lines<'a>( /// Builds a remapped frame from a no-line (base) mapping entry. /// /// `output_line` is the line number to use in the output frame, already computed -/// by the caller based on group context (has_minified_range, entry count, etc.). +/// by the caller based on group context (presence of `startline`, entry count, etc.). fn map_member_without_lines<'a>( frame: &StackFrame<'a>, member: &MemberMapping<'a>, @@ -293,9 +293,10 @@ impl FusedIterator for RemappedFrameIter<'_> {} /// Resolves frames for the no-line (frame_line==0) case. /// /// When the input frame has no line number, base entries (endline==0) are preferred -/// over line-mapped entries. Base entries are split into two groups by -/// `has_minified_range` and each group's output lines are computed based on -/// whether the group contains range mappings, single-line mappings, or bare methods. +/// over line-mapped entries. Base entries are split into two groups by whether +/// `startline` is present (0:0 entries) or absent (no-range entries), and each +/// group's output lines are computed based on whether the group contains range +/// mappings, single-line mappings, or bare methods. fn resolve_no_line_frames<'s>( frame: &StackFrame<'s>, mapping_entries: &'s [MemberMapping<'s>], @@ -330,11 +331,11 @@ fn resolve_no_line_frames<'s>( /// Resolves output lines for base (endline==0) entries when the frame has no line number. /// -/// Entries are split by `has_minified_range`: -/// - **0:0 entries** (`has_minified_range=true`): if any entry has a range +/// Entries are split by whether `startline` is present: +/// - **0:0 entries** (`startline.is_some()`): if any entry has a range /// (`original_endline != original_startline`), all emit `Some(0)`; /// otherwise each emits its `original_startline`. -/// - **No-range entries** (`has_minified_range=false`): a single entry uses +/// - **No-range entries** (`startline.is_none()`): a single entry uses /// its `original_startline` (or `Some(0)` for bare methods); multiple entries /// with the same name collapse to one frame with `Some(0)`; different names /// each emit `Some(0)` in original order. @@ -344,9 +345,13 @@ fn resolve_base_entries<'s>( collected: &mut CollectedFrames<'s>, ) { // Pre-compute aggregates in a single pass. + // Whether any 0:0 entry has a multi-line original range (original_endline != original_startline). let mut any_zero_zero_has_range = false; + // Number of no-range (startline.is_none()) entries. let mut no_range_count = 0usize; + // Original name of the first no-range entry, used to detect ambiguity. let mut first_no_range_name: Option<&str> = None; + // Whether all no-range entries map to the same original method name. let mut all_no_range_same_name = true; for member in base_entries { if member.startline.is_some() { @@ -365,6 +370,7 @@ fn resolve_base_entries<'s>( } } + // Whether a no-range entry has already been emitted (used to collapse duplicates). let mut no_range_emitted = false; for member in base_entries { if member.startline.is_some() { From a9ec960e7a61b6a45d3e3baac5eeef75e8badca2 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 13 Feb 2026 16:59:02 +0100 Subject: [PATCH 6/7] Address PR review feedback - Reverse pending_frames and pop from end instead of remove(0) - Add comment explaining intentional Some(0)/None asymmetry - Use let-else with .first() instead of is_empty + indexing Co-Authored-By: Claude Opus 4.6 --- src/cache/mod.rs | 10 +++++----- src/mapper.rs | 17 ++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index a702224..8032ad0 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -898,7 +898,7 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { fn next_inner(&mut self) -> Option> { // Drain any buffered frames from multi-frame expansion first. if !self.pending_frames.is_empty() { - return Some(self.pending_frames.remove(0)); + return self.pending_frames.pop(); } if let Some(frame) = self.fallback.take() { @@ -917,8 +917,8 @@ impl<'r, 'data> RemappedFrameIter<'r, 'data> { members.as_slice(), self.outer_source_file, ); - if !frames.is_empty() { - let first = frames.remove(0); + frames.reverse(); + if let Some(first) = frames.pop() { self.pending_frames = frames; return Some(first); } @@ -1133,8 +1133,8 @@ fn resolve_no_line_frames<'a>( /// - **0:0 entries** (`startline().is_some()`): if any entry has a range /// (`original_endline != original_startline`), all emit `Some(0)`; /// otherwise each emits its `original_startline`. -/// - **No-range entries** (`startline().is_none()`): a single entry uses -/// its `original_startline` (or `Some(0)` for bare methods); multiple entries +/// - **No-range entries** (`startline().is_none()`): a single entry emits +/// its `original_startline` if > 0, otherwise `Some(0)`; multiple entries /// with the same name collapse to one frame with `Some(0)`; different names /// each emit `Some(0)` in original order. fn resolve_base_entries<'a>( diff --git a/src/mapper.rs b/src/mapper.rs index 330f3f8..c3ad8fa 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -309,10 +309,9 @@ fn resolve_no_line_frames<'s>( } // No base entries — fall back to all entries with output line 0. - if mapping_entries.is_empty() { + let Some(first) = mapping_entries.first() else { return; - } - let first = &mapping_entries[0]; + }; let unambiguous = mapping_entries.iter().all(|m| m.original == first.original); if unambiguous { collected @@ -335,8 +334,8 @@ fn resolve_no_line_frames<'s>( /// - **0:0 entries** (`startline.is_some()`): if any entry has a range /// (`original_endline != original_startline`), all emit `Some(0)`; /// otherwise each emits its `original_startline`. -/// - **No-range entries** (`startline.is_none()`): a single entry uses -/// its `original_startline` (or `Some(0)` for bare methods); multiple entries +/// - **No-range entries** (`startline.is_none()`): a single entry emits +/// its `original_startline` if > 0, otherwise `Some(0)`; multiple entries /// with the same name collapse to one frame with `Some(0)`; different names /// each emit `Some(0)` in original order. fn resolve_base_entries<'s>( @@ -388,14 +387,10 @@ fn resolve_base_entries<'s>( } else if all_no_range_same_name { if !no_range_emitted { no_range_emitted = true; - let line = if no_range_count > 1 { - Some(0) - } else if member.original_startline.unwrap_or(0) > 0 { + let line = if no_range_count == 1 && member.original_startline.unwrap_or(0) > 0 { member.original_startline - } else if member.original_startline.is_none() { - Some(0) } else { - None + Some(0) }; collected .frames From 0129e59077e493d17ebe0bd2215e77f930ddb1a6 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 16 Feb 2026 13:51:13 +0100 Subject: [PATCH 7/7] refactor: Use .or(Some(0)) for no-range line resolution Co-Authored-By: Claude Opus 4.6 --- src/cache/mod.rs | 8 +------- src/mapper.rs | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 8032ad0..6b9bdbb 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1191,13 +1191,7 @@ fn resolve_base_entries<'a>( let line = if no_range_count > 1 { Some(0) } else { - compute_member_output_line(member).or( - if member.original_startline().is_none() { - Some(0) - } else { - None - }, - ) + compute_member_output_line(member).or(Some(0)) }; if let Some(f) = map_member_without_lines(cache, frame, member, outer_source_file, line) diff --git a/src/mapper.rs b/src/mapper.rs index c3ad8fa..7e79612 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -387,8 +387,8 @@ fn resolve_base_entries<'s>( } else if all_no_range_same_name { if !no_range_emitted { no_range_emitted = true; - let line = if no_range_count == 1 && member.original_startline.unwrap_or(0) > 0 { - member.original_startline + let line = if no_range_count == 1 { + member.original_startline.or(Some(0)) } else { Some(0) };