Skip to main content

backhopper_core/compat/
added_lines.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! The added lines of a patch's hunks as one blob plus a map from blob
6//! line to file line. The target-axis resolvers scan the blob and
7//! report a blob line; the map turns it into the file line the reason
8//! shows.
9
10use crate::compat::call_sites::{AttrCtxScanner, strip_line_comment};
11use crate::compat::patch::{Hunk, HunkLine};
12use crate::model::names::RelativePath;
13use crate::model::symbol::LineClass;
14
15/// One touched source file projected for the target-axis resolvers: its
16/// source-relative path, the text of its added lines where new symbols
17/// appear, and the blob-line to file-line map for the reported line.
18#[derive(Debug, Clone, Copy)]
19pub struct AddedLinesSubject<'a> {
20    pub source_path: &'a RelativePath,
21    pub added_text: &'a str,
22    pub line_map: &'a [u32],
23}
24
25/// Added lines joined into one `\n`-terminated blob, paired with a map
26/// from blob line (1-based) to new-file line, derived from each hunk's
27/// `new_start`. A removed line does not advance the new-file counter;
28/// a context line does but is not added to the blob.
29pub fn added_lines_with_offsets(hunks: &[Hunk]) -> (String, Vec<u32>) {
30    let mut blob = String::new();
31    let mut line_map = Vec::new();
32    for hunk in hunks {
33        let mut new_line = hunk.new_start as u32;
34        for line in &hunk.lines {
35            match line {
36                HunkLine::Added(s) => {
37                    blob.push_str(s);
38                    blob.push('\n');
39                    line_map.push(new_line);
40                    new_line += 1;
41                }
42                HunkLine::Context(_) => new_line += 1,
43                HunkLine::Removed(_) => {}
44            }
45        }
46    }
47    (blob, line_map)
48}
49
50/// What the attribute-region classifier reads to classify added lines.
51#[derive(Debug, Clone, Copy)]
52pub enum LineContextSource<'a> {
53    /// The file as the patch leaves it, so a multi-line `-spec`, `-type`,
54    /// or `-record` opened above the hunk still classifies its lines.
55    PostImage(&'a str),
56    /// The hunk lines alone, for a patch with no tree to read.
57    HunksOnly,
58}
59
60/// Added-line text and line map, like `added_lines_with_offsets`, plus
61/// each blob line's attribute-region classification. A post-image that
62/// disagrees with an added line is not the file the hunks came from, and
63/// the hunks are classified on their own instead.
64pub fn added_lines_with_context(
65    hunks: &[Hunk],
66    source: LineContextSource<'_>,
67) -> (String, Vec<u32>, Vec<LineClass>) {
68    let (blob, line_map) = added_lines_with_offsets(hunks);
69    let ctx = match source {
70        LineContextSource::PostImage(text) => post_image_classes(text, &blob, &line_map),
71        LineContextSource::HunksOnly => None,
72    }
73    .unwrap_or_else(|| hunk_classes(hunks));
74    (blob, line_map, ctx)
75}
76
77fn post_image_classes(text: &str, blob: &str, line_map: &[u32]) -> Option<Vec<LineClass>> {
78    let mut scanner = AttrCtxScanner::new();
79    let classified: Vec<(&str, LineClass)> = text
80        .lines()
81        .map(|line| (line, scanner.classify(strip_line_comment(line))))
82        .collect();
83    blob.lines()
84        .zip(line_map)
85        .map(|(added, &file_line)| {
86            let (line, class) = classified.get((file_line as usize).checked_sub(1)?)?;
87            (line.trim_end() == added.trim_end()).then(|| class.clone())
88        })
89        .collect()
90}
91
92/// The classifier walks every hunk line, `Context` included, so a
93/// continuation line whose opener is inside the hunk still classifies
94/// against it. Only `Added` lines produce a class.
95fn hunk_classes(hunks: &[Hunk]) -> Vec<LineClass> {
96    let mut ctx = Vec::new();
97    let mut scanner = AttrCtxScanner::new();
98    for hunk in hunks {
99        for line in &hunk.lines {
100            match line {
101                HunkLine::Added(s) => ctx.push(scanner.classify(strip_line_comment(s))),
102                HunkLine::Context(s) => {
103                    scanner.classify(strip_line_comment(s));
104                }
105                HunkLine::Removed(_) => {}
106            }
107        }
108    }
109    ctx
110}
111
112/// Translate a 1-based blob line to its file line. An empty or
113/// too-short map yields the blob line unchanged, so a caller that does
114/// not thread offsets gets the old blob-relative behavior.
115pub fn file_line(line_map: &[u32], blob_line: u32) -> u32 {
116    line_map
117        .get(blob_line.saturating_sub(1) as usize)
118        .copied()
119        .unwrap_or(blob_line)
120}