backhopper_core/compat/
source_macros.rs1use std::collections::{BTreeMap, BTreeSet};
12use std::path::{Path, PathBuf};
13
14use backhopper_erlang_scan::skip_char_literal_span;
15
16use crate::erlang_macros::{MacroTable, parse_define};
17
18pub trait HeaderResolver {
22 fn resolve(&self, source_dir: Option<&Path>, raw: &Path) -> Option<HeaderBody>;
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct HeaderBody {
27 pub path: PathBuf,
28 pub body: String,
29}
30
31pub fn build_macro_table(
35 source: &str,
36 source_path: &Path,
37 resolver: &dyn HeaderResolver,
38) -> MacroTable {
39 let mut macros = MacroTable::new();
40 let mut seen: BTreeSet<PathBuf> = BTreeSet::new();
41 let mut pending: Vec<PathBuf> = Vec::new();
42 fold_attributes(source, &mut macros, &mut pending);
43 let source_dir = source_path.parent().map(Path::to_path_buf);
44 while let Some(raw) = pending.pop() {
45 let Some(entry) = resolver.resolve(source_dir.as_deref(), &raw) else {
46 continue;
47 };
48 if !seen.insert(entry.path.clone()) {
49 continue;
50 }
51 fold_attributes(&entry.body, &mut macros, &mut pending);
52 }
53 macros
54}
55
56fn fold_attributes(source: &str, macros: &mut MacroTable, pending: &mut Vec<PathBuf>) {
59 let bytes = source.as_bytes();
60 let mut i = 0usize;
61 let mut at_line_start = true;
62 while i < bytes.len() {
63 let c = bytes[i];
64 if c == b'\n' {
65 at_line_start = true;
66 i += 1;
67 continue;
68 }
69 if c == b'%' {
70 while i < bytes.len() && bytes[i] != b'\n' {
71 i += 1;
72 }
73 continue;
74 }
75 if c.is_ascii_whitespace() {
76 i += 1;
77 continue;
78 }
79 if at_line_start && c == b'-' {
80 i += 1;
81 let name_start = i;
82 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
83 i += 1;
84 }
85 let name = &source[name_start..i];
86 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
87 i += 1;
88 }
89 let body_start = i;
90 i = scan_to_terminator(source, i);
91 let body = &source[body_start..i];
92 if i < bytes.len() && bytes[i] == b'.' {
93 i += 1;
94 }
95 match name {
96 "define" => {
97 let trimmed = body
98 .trim()
99 .strip_prefix('(')
100 .map(|x| x.strip_suffix(')').unwrap_or(x))
101 .unwrap_or(body);
102 if let Some((key, value)) = parse_define(trimmed) {
103 macros.insert(key, value);
104 }
105 }
106 "include" | "include_lib" => {
107 if let Some(path) = parse_include_path(body) {
108 pending.push(path);
109 }
110 }
111 _ => {}
112 }
113 at_line_start = false;
114 continue;
115 }
116 at_line_start = false;
117 match c {
118 b'"' => i = skip_string(source, i + 1),
119 b'\'' => i = skip_atom(source, i + 1),
120 _ => i += 1,
121 }
122 }
123}
124
125fn scan_to_terminator(source: &str, start: usize) -> usize {
126 let bytes = source.as_bytes();
127 let mut i = start;
128 let mut depth = 0i32;
129 while i < bytes.len() {
130 let c = bytes[i];
131 match c {
132 b'(' | b'[' | b'{' => {
133 depth += 1;
134 i += 1;
135 }
136 b')' | b']' | b'}' => {
137 depth -= 1;
138 i += 1;
139 }
140 b'.' if depth <= 0 => return i,
141 b'"' => i = skip_string(source, i + 1),
142 b'\'' => i = skip_atom(source, i + 1),
143 b'$' => i += skip_char_literal_span(bytes, i),
144 b'%' => {
145 while i < bytes.len() && bytes[i] != b'\n' {
146 i += 1;
147 }
148 }
149 _ => i += 1,
150 }
151 }
152 i
153}
154
155fn skip_string(source: &str, start: usize) -> usize {
156 let bytes = source.as_bytes();
157 let mut i = start;
158 while i < bytes.len() {
159 match bytes[i] {
160 b'\\' => i = (i + 2).min(bytes.len()),
161 b'"' => return i + 1,
162 _ => i += 1,
163 }
164 }
165 i
166}
167
168fn skip_atom(source: &str, start: usize) -> usize {
169 let bytes = source.as_bytes();
170 let mut i = start;
171 while i < bytes.len() {
172 match bytes[i] {
173 b'\\' => i = (i + 2).min(bytes.len()),
174 b'\'' => return i + 1,
175 _ => i += 1,
176 }
177 }
178 i
179}
180
181fn parse_include_path(body: &str) -> Option<PathBuf> {
182 let s = body
183 .trim()
184 .trim_start_matches('(')
185 .trim_end_matches(')')
186 .trim();
187 let s = s.strip_prefix('"')?;
188 let s = s.strip_suffix('"')?;
189 if s.is_empty() {
190 return None;
191 }
192 Some(PathBuf::from(s))
193}
194
195#[derive(Debug, Default)]
198pub struct FileMap {
199 by_path: BTreeMap<PathBuf, String>,
200 by_basename: BTreeMap<String, Vec<HeaderBody>>,
201 by_include_lib: BTreeMap<String, HeaderBody>,
202}
203
204impl FileMap {
205 pub fn new() -> Self {
206 Self::default()
207 }
208
209 pub fn insert(&mut self, path: PathBuf, body: String) {
210 let base = path.file_name().and_then(|n| n.to_str()).map(str::to_owned);
211 let lib_key = include_lib_key(&path);
212 if let Some(base) = base {
213 self.by_basename.entry(base).or_default().push(HeaderBody {
214 path: path.clone(),
215 body: body.clone(),
216 });
217 }
218 if let Some(key) = lib_key {
219 self.by_include_lib.insert(
220 key,
221 HeaderBody {
222 path: path.clone(),
223 body: body.clone(),
224 },
225 );
226 }
227 self.by_path.insert(path, body);
228 }
229
230 pub fn get(&self, path: &Path) -> Option<&str> {
231 self.by_path.get(path).map(String::as_str)
232 }
233}
234
235impl HeaderResolver for FileMap {
236 fn resolve(&self, source_dir: Option<&Path>, raw: &Path) -> Option<HeaderBody> {
237 if let Some(dir) = source_dir {
238 let candidate = dir.join(raw);
239 if let Some(body) = self.by_path.get(&candidate) {
240 return Some(HeaderBody {
241 path: candidate,
242 body: body.clone(),
243 });
244 }
245 }
246 if let Some(body) = self.by_path.get(raw) {
247 return Some(HeaderBody {
248 path: raw.to_path_buf(),
249 body: body.clone(),
250 });
251 }
252 let key = raw.to_string_lossy();
253 if let Some(entry) = self.by_include_lib.get(key.as_ref()) {
254 return Some(entry.clone());
255 }
256 let base = raw.file_name().and_then(|n| n.to_str())?;
257 self.by_basename.get(base).and_then(|v| v.first().cloned())
258 }
259}
260
261fn include_lib_key(path: &Path) -> Option<String> {
262 let s = path.to_string_lossy();
263 let parts: Vec<&str> = s.split('/').collect();
264 let include_idx = parts.iter().position(|p| *p == "include")?;
265 if include_idx == 0 {
266 return None;
267 }
268 let app = parts[include_idx - 1];
269 let rest = &parts[include_idx..];
270 let tail = rest.join("/");
271 Some(format!("{app}/{tail}"))
272}