Same as `get_code` but reads each file at most once per `cache`. Used by `extract_code_blocks` and `merge_adjacent_blocks` so a single `build_context` call doesn't re-read the same source file dozens of times — the old per-node `fs::read_to_string` was the dominant cost when many entry points lived in the same file.
(
&self,
node: &Node,
cache: &mut HashMap<String, Option<String>>,
)
| 106 | /// times — the old per-node `fs::read_to_string` was the dominant cost |
| 107 | /// when many entry points lived in the same file. |
| 108 | fn get_code_cached( |
| 109 | &self, |
| 110 | node: &Node, |
| 111 | cache: &mut HashMap<String, Option<String>>, |
| 112 | ) -> Option<String> { |
| 113 | debug_assert!( |
| 114 | !node.file_path.is_empty(), |
| 115 | "get_code called with empty file_path" |
| 116 | ); |
| 117 | debug_assert!(!node.id.is_empty(), "get_code called with empty node id"); |
| 118 | if node.start_line == 0 || node.end_line == 0 || node.start_line > node.end_line { |
| 119 | return None; |
| 120 | } |
| 121 | |
| 122 | let content = if let Some(slot) = cache.get(&node.file_path) { |
| 123 | slot.clone() |
| 124 | } else { |
| 125 | let file_path = self.project_root.join(&node.file_path); |
| 126 | // Prevent path traversal: ensure the resolved path stays within |
| 127 | // the canonical project root. If the target itself is missing, |
| 128 | // allow the read attempt to fail naturally as `None`. |
| 129 | let allowed = match self.project_root.canonicalize() { |
| 130 | Ok(root) => match file_path.canonicalize() { |
| 131 | Ok(canonical) => canonical.starts_with(&root), |
| 132 | Err(_) => file_path.starts_with(&root), |
| 133 | }, |
| 134 | Err(_) => false, |
| 135 | }; |
| 136 | let loaded = if allowed { |
| 137 | fs::read_to_string(&file_path).ok() |
| 138 | } else { |
| 139 | None |
| 140 | }; |
| 141 | cache.insert(node.file_path.clone(), loaded.clone()); |
| 142 | loaded |
| 143 | }; |
| 144 | let content = content?; |
| 145 | |
| 146 | let lines: Vec<&str> = content.lines().collect(); |
| 147 | let start = (node.start_line as usize).saturating_sub(1); |
| 148 | let end = node.end_line as usize; |
| 149 | if start >= lines.len() { |
| 150 | return None; |
| 151 | } |
| 152 | let end = end.min(lines.len()); |
| 153 | let snippet: String = lines[start..end].join("\n"); |
| 154 | if snippet.is_empty() { |
| 155 | None |
| 156 | } else { |
| 157 | Some(snippet) |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | // ----------------------------------------------------------------------- |
| 162 | // Private helpers |
no test coverage detected